Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
C
canifa_note
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Vũ Hoàng Anh
canifa_note
Commits
55231820
Unverified
Commit
55231820
authored
Jan 20, 2026
by
Johnny
Committed by
GitHub
Jan 20, 2026
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
fix: resolve flaky migration tests and add stable upgrade test (#5514)
parent
00f21b86
Changes
5
Expand all
Show whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
140 additions
and
375 deletions
+140
-375
backend-tests.yml
.github/workflows/backend-tests.yml
+5
-2
Dockerfile
store/test/Dockerfile
+0
-13
containers.go
store/test/containers.go
+17
-150
main_test.go
store/test/main_test.go
+1
-16
migrator_test.go
store/test/migrator_test.go
+117
-194
No files found.
.github/workflows/backend-tests.yml
View file @
55231820
...
...
@@ -61,7 +61,10 @@ jobs:
run
:
|
case "${{ matrix.test-group }}" in
store)
go test -v -race -coverprofile=coverage.out -covermode=atomic ./store/...
# Run store tests for all drivers (sqlite, mysql, postgres)
# The TestMain in store/test runs all drivers when DRIVER is not set
# Note: We run without -race for container tests due to testcontainers race issues
go test -v -coverprofile=coverage.out -covermode=atomic ./store/...
;;
server)
go test -v -race -coverprofile=coverage.out -covermode=atomic ./server/...
...
...
@@ -75,7 +78,7 @@ jobs:
;;
esac
env
:
DRIVER
:
sqlite
# Use SQLite for fastest test execution
DRIVER
:
${{ matrix.test-group == 'store' && '' || 'sqlite' }}
-
name
:
Upload coverage
if
:
github.event_name == 'push' && github.ref == 'refs/heads/main'
...
...
store/test/Dockerfile
deleted
100644 → 0
View file @
00f21b86
FROM
golang:1.25-alpine AS backend
WORKDIR
/backend-build
COPY
. .
RUN
go build
-o
memos ./cmd/memos
FROM
alpine:latest
WORKDIR
/usr/local/memos
COPY
--from=backend /backend-build/memos /usr/local/memos/
EXPOSE
5230
RUN
mkdir
-p
/var/opt/memos
ENV
MEMOS_MODE="prod"
ENV
MEMOS_PORT="5230"
ENTRYPOINT
["./memos"]
store/test/containers.go
View file @
55231820
...
...
@@ -30,18 +30,10 @@ const (
// Memos container settings for migration testing.
MemosDockerImage
=
"neosmemo/memos"
StableMemosVersion
=
"stable"
StableMemosVersion
=
"stable"
// Always points to the latest stable release
)
var
(
// MemosStartupWaitStrategy defines the wait strategy for Memos container startup.
// It waits for the "started" log message (compatible with both old and new versions)
// and checks if port 5230 is listening.
MemosStartupWaitStrategy
=
wait
.
ForAll
(
wait
.
ForLog
(
"started"
),
wait
.
ForListeningPort
(
"5230/tcp"
),
)
.
WithDeadline
(
180
*
time
.
Second
)
mysqlContainer
atomic
.
Pointer
[
mysql
.
MySQLContainer
]
postgresContainer
atomic
.
Pointer
[
postgres
.
PostgresContainer
]
mysqlOnce
sync
.
Once
...
...
@@ -235,105 +227,6 @@ func GetPostgresDSN(t *testing.T) string {
return
strings
.
Replace
(
dsn
,
"/init_db?"
,
"/"
+
dbName
+
"?"
,
1
)
}
// GetDedicatedMySQLDSN starts a dedicated MySQL container for migration testing.
// This is needed because older Memos versions have bugs when connecting to a MySQL
// server that has other initialized databases (they incorrectly query migration_history
// on a fresh database without checking if the DB is initialized).
// Returns: DSN for host access, container hostname for internal network access, cleanup function.
func
GetDedicatedMySQLDSN
(
t
*
testing
.
T
)
(
dsn
string
,
containerHost
string
,
cleanup
func
())
{
ctx
:=
context
.
Background
()
nw
,
err
:=
getTestNetwork
(
ctx
)
if
err
!=
nil
{
t
.
Fatalf
(
"failed to create test network: %v"
,
err
)
}
container
,
err
:=
mysql
.
Run
(
ctx
,
"mysql:8"
,
mysql
.
WithDatabase
(
"memos"
),
mysql
.
WithUsername
(
"root"
),
mysql
.
WithPassword
(
testPassword
),
testcontainers
.
WithEnv
(
map
[
string
]
string
{
"MYSQL_ROOT_PASSWORD"
:
testPassword
,
}),
testcontainers
.
WithWaitStrategy
(
wait
.
ForAll
(
wait
.
ForLog
(
"ready for connections"
)
.
WithOccurrence
(
2
),
wait
.
ForListeningPort
(
"3306/tcp"
),
)
.
WithDeadline
(
120
*
time
.
Second
),
),
network
.
WithNetwork
(
nil
,
nw
),
)
if
err
!=
nil
{
t
.
Fatalf
(
"failed to start dedicated MySQL container: %v"
,
err
)
}
hostDSN
,
err
:=
container
.
ConnectionString
(
ctx
,
"multiStatements=true"
)
if
err
!=
nil
{
container
.
Terminate
(
ctx
)
t
.
Fatalf
(
"failed to get MySQL connection string: %v"
,
err
)
}
if
err
:=
waitForDB
(
"mysql"
,
hostDSN
,
30
*
time
.
Second
);
err
!=
nil
{
container
.
Terminate
(
ctx
)
t
.
Fatalf
(
"MySQL not ready for connections: %v"
,
err
)
}
name
,
_
:=
container
.
Name
(
ctx
)
host
:=
strings
.
TrimPrefix
(
name
,
"/"
)
return
hostDSN
,
host
,
func
()
{
container
.
Terminate
(
ctx
)
}
}
// GetDedicatedPostgresDSN starts a dedicated PostgreSQL container for migration testing.
// This is needed for isolation when testing migrations with older Memos versions.
// Returns: DSN for host access, container hostname for internal network access, cleanup function.
func
GetDedicatedPostgresDSN
(
t
*
testing
.
T
)
(
dsn
string
,
containerHost
string
,
cleanup
func
())
{
ctx
:=
context
.
Background
()
nw
,
err
:=
getTestNetwork
(
ctx
)
if
err
!=
nil
{
t
.
Fatalf
(
"failed to create test network: %v"
,
err
)
}
container
,
err
:=
postgres
.
Run
(
ctx
,
"postgres:18"
,
postgres
.
WithDatabase
(
"memos"
),
postgres
.
WithUsername
(
testUser
),
postgres
.
WithPassword
(
testPassword
),
testcontainers
.
WithWaitStrategy
(
wait
.
ForAll
(
wait
.
ForLog
(
"database system is ready to accept connections"
)
.
WithOccurrence
(
2
),
wait
.
ForListeningPort
(
"5432/tcp"
),
)
.
WithDeadline
(
120
*
time
.
Second
),
),
network
.
WithNetwork
(
nil
,
nw
),
)
if
err
!=
nil
{
t
.
Fatalf
(
"failed to start dedicated PostgreSQL container: %v"
,
err
)
}
hostDSN
,
err
:=
container
.
ConnectionString
(
ctx
,
"sslmode=disable"
)
if
err
!=
nil
{
container
.
Terminate
(
ctx
)
t
.
Fatalf
(
"failed to get PostgreSQL connection string: %v"
,
err
)
}
if
err
:=
waitForDB
(
"postgres"
,
hostDSN
,
30
*
time
.
Second
);
err
!=
nil
{
container
.
Terminate
(
ctx
)
t
.
Fatalf
(
"PostgreSQL not ready for connections: %v"
,
err
)
}
name
,
_
:=
container
.
Name
(
ctx
)
host
:=
strings
.
TrimPrefix
(
name
,
"/"
)
return
hostDSN
,
host
,
func
()
{
container
.
Terminate
(
ctx
)
}
}
// TerminateContainers cleans up all running containers and network.
// This is typically called from TestMain.
func
TerminateContainers
()
{
...
...
@@ -349,45 +242,28 @@ func TerminateContainers() {
}
}
// GetMySQLContainerHost returns the MySQL container hostname for use within the Docker network.
func
GetMySQLContainerHost
()
string
{
container
:=
mysqlContainer
.
Load
()
if
container
==
nil
{
return
""
}
name
,
_
:=
container
.
Name
(
context
.
Background
())
// Remove leading slash from container name
return
strings
.
TrimPrefix
(
name
,
"/"
)
}
// GetPostgresContainerHost returns the PostgreSQL container hostname for use within the Docker network.
func
GetPostgresContainerHost
()
string
{
container
:=
postgresContainer
.
Load
()
if
container
==
nil
{
return
""
}
name
,
_
:=
container
.
Name
(
context
.
Background
())
return
strings
.
TrimPrefix
(
name
,
"/"
)
}
// MemosContainerConfig holds configuration for starting a Memos container.
type
MemosContainerConfig
struct
{
Version
string
// Memos version tag (e.g., "0.2
5
")
Version
string
// Memos version tag (e.g., "0.2
4.0
")
Driver
string
// Database driver: sqlite, mysql, postgres
DSN
string
// Database DSN (for mysql/postgres)
DataDir
string
// Host directory to mount for SQLite data
}
// MemosStartupWaitStrategy defines the wait strategy for Memos container startup.
// Uses regex to match various log message formats across versions.
var
MemosStartupWaitStrategy
=
wait
.
ForAll
(
wait
.
ForLog
(
"(started successfully|has been started on port)"
)
.
AsRegexp
(),
wait
.
ForListeningPort
(
"5230/tcp"
),
)
.
WithDeadline
(
180
*
time
.
Second
)
// StartMemosContainer starts a Memos container for migration testing.
// For SQLite, it mounts the dataDir to /var/opt/memos.
// For MySQL/PostgreSQL, it connects to the provided DSN via the test network.
// If Version is "local", builds the image from the local Dockerfile.
func
StartMemosContainer
(
ctx
context
.
Context
,
cfg
MemosContainerConfig
)
(
testcontainers
.
Container
,
error
)
{
env
:=
map
[
string
]
string
{
"MEMOS_MODE"
:
"prod"
,
}
var
mounts
[]
testcontainers
.
ContainerMount
var
opts
[]
testcontainers
.
ContainerCustomizer
switch
cfg
.
Driver
{
...
...
@@ -396,37 +272,28 @@ func StartMemosContainer(ctx context.Context, cfg MemosContainerConfig) (testcon
opts
=
append
(
opts
,
testcontainers
.
WithHostConfigModifier
(
func
(
hc
*
container
.
HostConfig
)
{
hc
.
Binds
=
append
(
hc
.
Binds
,
fmt
.
Sprintf
(
"%s:%s"
,
cfg
.
DataDir
,
"/var/opt/memos"
))
}))
case
"mysql"
:
env
[
"MEMOS_DRIVER"
]
=
"mysql"
env
[
"MEMOS_DSN"
]
=
cfg
.
DSN
opts
=
append
(
opts
,
network
.
WithNetwork
(
nil
,
testDockerNetwork
.
Load
()))
case
"postgres"
:
env
[
"MEMOS_DRIVER"
]
=
"postgres"
env
[
"MEMOS_DSN"
]
=
cfg
.
DSN
opts
=
append
(
opts
,
network
.
WithNetwork
(
nil
,
testDockerNetwork
.
Load
()))
default
:
return
nil
,
errors
.
Errorf
(
"unsupported driver: %s"
,
cfg
.
Driver
)
return
nil
,
errors
.
Errorf
(
"unsupported driver
for migration testing
: %s"
,
cfg
.
Driver
)
}
req
:=
testcontainers
.
ContainerRequest
{
Image
:
fmt
.
Sprintf
(
"%s:%s"
,
MemosDockerImage
,
cfg
.
Version
),
Env
:
env
,
Mounts
:
testcontainers
.
Mounts
(
mounts
...
),
ExposedPorts
:
[]
string
{
"5230/tcp"
},
WaitingFor
:
MemosStartupWaitStrategy
,
User
:
fmt
.
Sprintf
(
"%d:%d"
,
os
.
Getuid
(),
os
.
Getgid
()),
}
// Use local
Dockerfile build or remote image
// Use local
image if specified
if
cfg
.
Version
==
"local"
{
if
os
.
Getenv
(
"MEMOS_TEST_IMAGE_BUILT"
)
==
"1"
{
req
.
Image
=
"memos-test:local"
}
else
{
req
.
FromDockerfile
=
testcontainers
.
FromDockerfile
{
Context
:
"../../"
,
Dockerfile
:
"
store/test/Dockerfile"
,
// Simple Dockerfile without BuildKit requirements
Dockerfile
:
"
Dockerfile"
,
}
}
}
else
{
req
.
Image
=
fmt
.
Sprintf
(
"%s:%s"
,
MemosDockerImage
,
cfg
.
Version
)
}
genericReq
:=
testcontainers
.
GenericContainerRequest
{
...
...
@@ -434,17 +301,17 @@ func StartMemosContainer(ctx context.Context, cfg MemosContainerConfig) (testcon
Started
:
true
,
}
// Apply
network
options
// Apply options
for
_
,
opt
:=
range
opts
{
if
err
:=
opt
.
Customize
(
&
genericReq
);
err
!=
nil
{
return
nil
,
errors
.
Wrap
(
err
,
"failed to apply container option"
)
}
}
c
ontaine
r
,
err
:=
testcontainers
.
GenericContainer
(
ctx
,
genericReq
)
c
t
r
,
err
:=
testcontainers
.
GenericContainer
(
ctx
,
genericReq
)
if
err
!=
nil
{
return
nil
,
errors
.
Wrap
(
err
,
"failed to start memos container"
)
}
return
c
ontaine
r
,
nil
return
c
t
r
,
nil
}
store/test/main_test.go
View file @
55231820
...
...
@@ -26,28 +26,13 @@ func runAllDrivers() {
_
,
currentFile
,
_
,
_
:=
runtime
.
Caller
(
0
)
projectRoot
:=
filepath
.
Dir
(
filepath
.
Dir
(
filepath
.
Dir
(
currentFile
)))
// Build the docker image once for all tests to use
fmt
.
Println
(
"Building memos docker image for tests (memos-test:local)..."
)
buildCmd
:=
exec
.
Command
(
"docker"
,
"build"
,
"-f"
,
"store/test/Dockerfile"
,
"-t"
,
"memos-test:local"
,
"."
)
buildCmd
.
Dir
=
projectRoot
buildCmd
.
Stdout
=
os
.
Stdout
buildCmd
.
Stderr
=
os
.
Stderr
if
err
:=
buildCmd
.
Run
();
err
!=
nil
{
fmt
.
Printf
(
"Failed to build docker image: %v
\n
"
,
err
)
// We don't exit here, we let the tests try to run (and maybe fail or rebuild)
// strictly speaking we should probably fail, but let's be robust.
// Actually, if build fails, tests relying on it will fail or try to rebuild.
// Let's exit to be clear.
panic
(
fmt
.
Sprintf
(
"failed to build docker image: %v"
,
err
))
}
var
failed
[]
string
for
_
,
driver
:=
range
drivers
{
fmt
.
Printf
(
"
\n
==================== %s ====================
\n\n
"
,
driver
)
cmd
:=
exec
.
Command
(
"go"
,
"test"
,
"-v"
,
"-count=1"
,
"./store/test/..."
)
cmd
.
Dir
=
projectRoot
cmd
.
Env
=
append
(
os
.
Environ
(),
"DRIVER="
+
driver
,
"MEMOS_TEST_IMAGE_BUILT=1"
)
cmd
.
Env
=
append
(
os
.
Environ
(),
"DRIVER="
+
driver
)
cmd
.
Stdout
=
os
.
Stdout
cmd
.
Stderr
=
os
.
Stderr
...
...
store/test/migrator_test.go
View file @
55231820
This diff is collapsed.
Click to expand it.
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment