Skip to content

Commit 1d24166

Browse files
Merge pull request #3278 from SwiftPackageIndex/update-postgres-db
Bump to 16
2 parents 18e22dd + 07d66a4 commit 1d24166

File tree

9 files changed

+149
-201
lines changed

9 files changed

+149
-201
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ jobs:
2929
options: --privileged
3030
services:
3131
postgres:
32-
image: postgres:13.11-alpine
32+
image: postgres:16-alpine
3333
env:
3434
POSTGRES_DB: spi_test
3535
POSTGRES_USER: spi_test
@@ -61,7 +61,7 @@ jobs:
6161
# runs-on: macOS-latest
6262
# services:
6363
# postgres:
64-
# image: postgres:13.11
64+
# image: postgres:16-alpine
6565
# env:
6666
# POSTGRES_DB: spi_dev
6767
# POSTGRES_USER: spi_dev

LOCAL_DEVELOPMENT_SETUP.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,6 @@ Close the scheme editor and run the application by selecting "Run" from the Xcod
4040

4141
When working locally, it's helpful to have a database with pre-populated data from the live system. [Talk to us on Discord](https://discord.gg/vQRb6KkYRw), and we'll supply you with a recent database dump that you can load with `./scripts/load-db.sh`.
4242

43-
**Note:** Running the `load-db.sh` script requires that the Postgres command line tools are available on your system. The easiest way to get these is with [brew](https://brew.sh/) by running `brew install postgresql`.
44-
4543
### Setup the Front End
4644

4745
Once the back end is set up and the server is running, the next step is to set up the front end to serve the CSS and JavaScript. We use [esbuild](https://esbuild.github.io) to build our front end files. However, you do not need to install Node or any other tools locally unless you are doing extensive work with the front-end files in this project. For small changes or to get started with front-end changes in this project, use our Docker-based scripts.

Makefile

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,11 @@ analyze:
9595
db-up: db-up-dev db-up-test
9696

9797
db-up-dev:
98-
docker run --name spi_dev -e POSTGRES_DB=spi_dev -e POSTGRES_USER=spi_dev -e POSTGRES_PASSWORD=xxx -p 6432:5432 -d postgres:13.11-alpine
98+
docker run --name spi_dev -e POSTGRES_DB=spi_dev -e POSTGRES_USER=spi_dev -e POSTGRES_PASSWORD=xxx -p 6432:5432 -d postgres:16-alpine
9999

100+
# Keep test db on postgres:13 for now, to make local testing faster. See
101+
# https://github.com/SwiftPackageIndex/SwiftPackageIndex-Server/issues/3360#issuecomment-2331103211
102+
# for details
100103
db-up-test:
101104
docker run --name spi_test \
102105
-e POSTGRES_DB=spi_test \
@@ -106,19 +109,7 @@ db-up-test:
106109
--tmpfs /pgdata:rw,noexec,nosuid,size=1024m \
107110
-p 5432:5432 \
108111
-d \
109-
postgres:13.11-alpine
110-
111-
db-up-test-log-statement:
112-
docker run --name spi_test \
113-
-e POSTGRES_DB=spi_test \
114-
-e POSTGRES_USER=spi_test \
115-
-e POSTGRES_PASSWORD=xxx \
116-
-e PGDATA=/pgdata \
117-
--tmpfs /pgdata:rw,noexec,nosuid,size=1024m \
118-
-p 5432:5432 \
119-
--rm \
120-
postgres:13.11-alpine \
121-
postgres -c log_statement=all
112+
postgres:13-alpine
122113

123114
db-down: db-down-dev db-down-test
124115

Tests/AppTests/AppTestCase.swift

Lines changed: 132 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,13 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15+
@testable import App
16+
17+
import NIOConcurrencyHelpers
18+
import PostgresNIO
1519
import SQLKit
1620
import XCTVapor
17-
@testable import App
21+
1822

1923
class AppTestCase: XCTestCase {
2024
var app: Application!
@@ -23,9 +27,16 @@ class AppTestCase: XCTestCase {
2327
override func setUp() async throws {
2428
try await super.setUp()
2529
app = try await setup(.testing)
30+
31+
// Always start with a baseline mock environment to avoid hitting live resources
32+
Current = .mock(eventLoop: app.eventLoopGroup.next())
33+
2634
Current.setLogger(.init(label: "test", factory: { _ in logger }))
27-
// Silence app logging
28-
app.logger = .init(label: "noop") { _ in SwiftLogNoOpLogHandler() }
35+
}
36+
37+
func setup(_ environment: Environment) async throws -> Application {
38+
try await Self.setupDb(environment)
39+
return try await Self.setupApp(environment)
2940
}
3041

3142
override func tearDown() async throws {
@@ -35,6 +46,98 @@ class AppTestCase: XCTestCase {
3546
}
3647

3748

49+
extension AppTestCase {
50+
51+
static func setupApp(_ environment: Environment) async throws -> Application {
52+
let app = try await Application.make(environment)
53+
try await configure(app)
54+
55+
// Silence app logging
56+
app.logger = .init(label: "noop") { _ in SwiftLogNoOpLogHandler() }
57+
58+
return app
59+
}
60+
61+
62+
static func setupDb(_ environment: Environment) async throws {
63+
await DotEnvFile.load(for: environment, fileio: .init(threadPool: .singleton))
64+
65+
// Ensure DATABASE_HOST is from a restricted set db hostnames and nothing else.
66+
// This is safeguard against accidental inheritance of setup in QueryPerformanceTests
67+
// and to ensure the database resetting cannot impact any other network hosts.
68+
let host = Environment.get("DATABASE_HOST")
69+
precondition(["localhost", "postgres", "host.docker.internal"].contains(host),
70+
"DATABASE_HOST must be a local db, was: \(host)")
71+
72+
let testDbName = Environment.get("DATABASE_NAME")!
73+
let snapshotName = testDbName + "_snapshot"
74+
75+
// Create initial db snapshot on first run
76+
try await snapshotCreated.withValue { snapshotCreated in
77+
if !snapshotCreated {
78+
try await createSchema(environment, databaseName: testDbName)
79+
try await createSnapshot(original: testDbName, snapshot: snapshotName, environment: environment)
80+
snapshotCreated = true
81+
}
82+
}
83+
84+
try await restoreSnapshot(original: testDbName, snapshot: snapshotName, environment: environment)
85+
}
86+
87+
88+
static func createSchema(_ environment: Environment, databaseName: String) async throws {
89+
do {
90+
try await withDatabase("postgres", .testing) { // Connect to `postgres` db in order to reset the test db
91+
try await $0.query(PostgresQuery(unsafeSQL: "DROP DATABASE IF EXISTS \(databaseName) WITH (FORCE)"))
92+
try await $0.query(PostgresQuery(unsafeSQL: "CREATE DATABASE \(databaseName)"))
93+
}
94+
95+
do { // Use autoMigrate to spin up the schema
96+
let app = try await Application.make(environment)
97+
app.logger = .init(label: "noop") { _ in SwiftLogNoOpLogHandler() }
98+
try await configure(app)
99+
try await app.autoMigrate()
100+
try await app.asyncShutdown()
101+
}
102+
} catch {
103+
print("Create schema failed with error: ", String(reflecting: error))
104+
throw error
105+
}
106+
}
107+
108+
109+
static func createSnapshot(original: String, snapshot: String, environment: Environment) async throws {
110+
do {
111+
try await withDatabase("postgres", environment) { client in
112+
try await client.query(PostgresQuery(unsafeSQL: "DROP DATABASE IF EXISTS \(snapshot) WITH (FORCE)"))
113+
try await client.query(PostgresQuery(unsafeSQL: "CREATE DATABASE \(snapshot) TEMPLATE \(original)"))
114+
}
115+
} catch {
116+
print("Create snapshot failed with error: ", String(reflecting: error))
117+
throw error
118+
}
119+
}
120+
121+
122+
static func restoreSnapshot(original: String, snapshot: String, environment: Environment) async throws {
123+
// delete db and re-create from snapshot
124+
do {
125+
try await withDatabase("postgres", environment) { client in
126+
try await client.query(PostgresQuery(unsafeSQL: "DROP DATABASE IF EXISTS \(original) WITH (FORCE)"))
127+
try await client.query(PostgresQuery(unsafeSQL: "CREATE DATABASE \(original) TEMPLATE \(snapshot)"))
128+
}
129+
} catch {
130+
print("Restore snapshot failed with error: ", String(reflecting: error))
131+
throw error
132+
}
133+
}
134+
135+
136+
static let snapshotCreated = ActorIsolated(false)
137+
138+
}
139+
140+
38141
extension AppTestCase {
39142
func renderSQL(_ builder: SQLSelectBuilder) -> String {
40143
renderSQL(builder.query)
@@ -69,3 +172,29 @@ extension AppTestCase {
69172
}
70173
}
71174
}
175+
176+
177+
private func connect(to databaseName: String, _ environment: Environment) async throws -> PostgresClient {
178+
await DotEnvFile.load(for: environment, fileio: .init(threadPool: .singleton))
179+
let host = Environment.get("DATABASE_HOST")!
180+
let port = Environment.get("DATABASE_PORT").flatMap(Int.init)!
181+
let username = Environment.get("DATABASE_USERNAME")!
182+
let password = Environment.get("DATABASE_PASSWORD")!
183+
184+
let config = PostgresClient.Configuration(host: host, port: port, username: username, password: password, database: databaseName, tls: .disable)
185+
186+
return .init(configuration: config)
187+
}
188+
189+
190+
private func withDatabase(_ databaseName: String, _ environment: Environment, _ query: @escaping (PostgresClient) async throws -> Void) async throws {
191+
let client = try await connect(to: databaseName, environment)
192+
try await withThrowingTaskGroup(of: Void.self) { taskGroup in
193+
taskGroup.addTask { await client.run() }
194+
195+
try await query(client)
196+
197+
taskGroup.cancelAll()
198+
}
199+
}
200+

Tests/AppTests/Util.swift

Lines changed: 0 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -23,67 +23,6 @@ import NIOConcurrencyHelpers
2323

2424
// MARK: - Test helpers
2525

26-
private let _schemaCreated = NIOLockedValueBox<Bool>(false)
27-
28-
func setup(_ environment: Environment, resetDb: Bool = true) async throws -> Application {
29-
let app = try await Application.make(environment)
30-
let host = try await configure(app)
31-
32-
// Ensure `.testing` refers to "postgres" or "localhost"
33-
precondition(["localhost", "postgres", "host.docker.internal"].contains(host),
34-
".testing must be a local db, was: \(host)")
35-
36-
app.logger.logLevel = Environment.get("LOG_LEVEL").flatMap(Logger.Level.init(rawValue:)) ?? .warning
37-
38-
if !(_schemaCreated.withLockedValue { $0 }) {
39-
// ensure we create the schema when running the first test
40-
try await app.autoMigrate()
41-
_schemaCreated.withLockedValue { $0 = true }
42-
}
43-
if resetDb { try await _resetDb(app) }
44-
45-
// Always start with a baseline mock environment to avoid hitting live resources
46-
Current = .mock(eventLoop: app.eventLoopGroup.next())
47-
48-
return app
49-
}
50-
51-
52-
private let tableNamesCache: NIOLockedValueBox<[String]?> = .init(nil)
53-
54-
func _resetDb(_ app: Application) async throws {
55-
guard let db = app.db as? SQLDatabase else {
56-
fatalError("Database must be an SQLDatabase ('as? SQLDatabase' must succeed)")
57-
}
58-
59-
guard let tables = tableNamesCache.withLockedValue({ $0 }) else {
60-
struct Row: Decodable { var table_name: String }
61-
let tableNames = try await db.raw("""
62-
SELECT table_name FROM
63-
information_schema.tables
64-
WHERE
65-
table_schema NOT IN ('pg_catalog', 'information_schema', 'public._fluent_migrations')
66-
AND table_schema NOT LIKE 'pg_toast%'
67-
AND table_name NOT LIKE '_fluent_%'
68-
""")
69-
.all(decoding: Row.self)
70-
.map(\.table_name)
71-
tableNamesCache.withLockedValue { $0 = tableNames }
72-
try await _resetDb(app)
73-
return
74-
}
75-
76-
for table in tables {
77-
try await db.raw("TRUNCATE TABLE \(ident: table) CASCADE").run()
78-
}
79-
80-
try await RecentPackage.refresh(on: app.db)
81-
try await RecentRelease.refresh(on: app.db)
82-
try await Search.refresh(on: app.db)
83-
try await Stats.refresh(on: app.db)
84-
try await WeightedKeyword.refresh(on: app.db)
85-
}
86-
8726

8827
func fixtureString(for fixture: String) throws -> String {
8928
String(decoding: try fixtureData(for: fixture), as: UTF8.self)

scripts/convert_to_db_dump.sh

Lines changed: 0 additions & 64 deletions
This file was deleted.

scripts/db_backup.sh

Lines changed: 0 additions & 33 deletions
This file was deleted.

0 commit comments

Comments
 (0)