Update testrunner to use new dev-env [WIP] (#1575)

* Update testrunner to use new dev-env

* Fix label testcase

* Vendor the dev-infra scripts from the atproto repo for the dev-env server runner

* Bump detox to fix the ios sim control issue

* Use iphone 15 pro for tests

* Ensure the reminders never trigger during tests

* Skip the shell tests due to a crash bug with detox and the drawer
zio/stable
Paul Frazee 2023-10-10 15:46:27 -07:00 committed by GitHub
parent aad8d12ede
commit 0b44af38ea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 288 additions and 67 deletions

View File

@ -41,7 +41,7 @@ module.exports = {
simulator: { simulator: {
type: 'ios.simulator', type: 'ios.simulator',
device: { device: {
type: 'iPhone 15', type: 'iPhone 15 Pro',
}, },
}, },
attached: { attached: {

View File

@ -502,6 +502,9 @@ async function main() {
createdAt: new Date().toISOString(), createdAt: new Date().toISOString(),
}, },
) )
// flush caches
await server.mocker.testNet.processAll()
} }
} }
console.log('Ready') console.log('Ready')

View File

@ -0,0 +1,92 @@
#!/usr/bin/env sh
get_container_id() {
local compose_file=$1
local service=$2
if [ -z "${compose_file}" ] || [ -z "${service}" ]; then
echo "usage: get_container_id <compose_file> <service>"
exit 1
fi
docker compose -f $compose_file ps --format json --status running \
| jq -r '.[]? | select(.Service == "'${service}'") | .ID'
}
# Exports all environment variables
export_env() {
export_pg_env
export_redis_env
}
# Exports postgres environment variables
export_pg_env() {
# Based on creds in compose.yaml
export PGPORT=5433
export PGHOST=localhost
export PGUSER=pg
export PGPASSWORD=password
export PGDATABASE=postgres
export DB_POSTGRES_URL="postgresql://pg:password@127.0.0.1:5433/postgres"
}
# Exports redis environment variables
export_redis_env() {
export REDIS_HOST="127.0.0.1:6380"
}
# Main entry point
main() {
# Expect a SERVICES env var to be set with the docker service names
local services=${SERVICES}
dir=$(dirname $0)
compose_file="${dir}/docker-compose.yaml"
# whether this particular script started the container(s)
started_container=false
# trap SIGINT and performs cleanup as necessary, i.e.
# taking down containers if this script started them
trap "on_sigint ${services}" INT
on_sigint() {
local services=$@
echo # newline
if $started_container; then
docker compose -f $compose_file rm -f --stop --volumes ${services}
fi
exit $?
}
# check if all services are running already
not_running=false
for service in $services; do
container_id=$(get_container_id $compose_file $service)
if [ -z $container_id ]; then
not_running=true
break
fi
done
# if any are missing, recreate all services
if $not_running; then
docker compose -f $compose_file up --wait --force-recreate ${services}
started_container=true
else
echo "all services ${services} are already running"
fi
# setup environment variables and run args
export_env
"$@"
# save return code for later
code=$?
# performs cleanup as necessary, i.e. taking down containers
# if this script started them
echo # newline
if $started_container; then
docker compose -f $compose_file rm -f --stop --volumes ${services}
fi
exit ${code}
}

View File

@ -0,0 +1,49 @@
version: '3.8'
services:
# An ephermerally-stored postgres database for single-use test runs
db_test: &db_test
image: postgres:14.4-alpine
environment:
- POSTGRES_USER=pg
- POSTGRES_PASSWORD=password
ports:
- '5433:5432'
# Healthcheck ensures db is queryable when `docker-compose up --wait` completes
healthcheck:
test: 'pg_isready -U pg'
interval: 500ms
timeout: 10s
retries: 20
# A persistently-stored postgres database
db:
<<: *db_test
ports:
- '5432:5432'
healthcheck:
disable: true
volumes:
- atp_db:/var/lib/postgresql/data
# An ephermerally-stored redis cache for single-use test runs
redis_test: &redis_test
image: redis:7.0-alpine
ports:
- '6380:6379'
# Healthcheck ensures redis is queryable when `docker-compose up --wait` completes
healthcheck:
test: ['CMD-SHELL', '[ "$$(redis-cli ping)" = "PONG" ]']
interval: 500ms
timeout: 10s
retries: 20
# A persistently-stored redis cache
redis:
<<: *redis_test
command: redis-server --save 60 1 --loglevel warning
ports:
- '6379:6379'
healthcheck:
disable: true
volumes:
- atp_redis:/data
volumes:
atp_db:
atp_redis:

View File

@ -0,0 +1,9 @@
#!/usr/bin/env sh
# Example usage:
# ./with-test-db.sh psql postgresql://pg:password@localhost:5433/postgres -c 'select 1;'
dir=$(dirname $0)
. ${dir}/_common.sh
SERVICES="db_test" main "$@"

View File

@ -0,0 +1,10 @@
#!/usr/bin/env sh
# Example usage:
# ./with-test-redis-and-db.sh psql postgresql://pg:password@localhost:5433/postgres -c 'select 1;'
# ./with-test-redis-and-db.sh redis-cli -h localhost -p 6380 ping
dir=$(dirname $0)
. ${dir}/_common.sh
SERVICES="db_test redis_test" main "$@"

View File

@ -1,7 +1,7 @@
import net from 'net' import net from 'net'
import path from 'path' import path from 'path'
import fs from 'fs' import fs from 'fs'
import {TestNetworkNoAppView} from '@atproto/dev-env' import {TestNetwork} from '@atproto/dev-env'
import {AtUri, BskyAgent} from '@atproto/api' import {AtUri, BskyAgent} from '@atproto/api'
export interface TestUser { export interface TestUser {
@ -18,14 +18,59 @@ export interface TestPDS {
close: () => Promise<void> close: () => Promise<void>
} }
class StringIdGenerator {
_nextId = [0]
constructor(
public _chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ',
) {}
next() {
const r = []
for (const char of this._nextId) {
r.unshift(this._chars[char])
}
this._increment()
return r.join('')
}
_increment() {
for (let i = 0; i < this._nextId.length; i++) {
const val = ++this._nextId[i]
if (val >= this._chars.length) {
this._nextId[i] = 0
} else {
return
}
}
this._nextId.push(0)
}
*[Symbol.iterator]() {
while (true) {
yield this.next()
}
}
}
const ids = new StringIdGenerator()
export async function createServer( export async function createServer(
{inviteRequired}: {inviteRequired: boolean} = {inviteRequired: false}, {inviteRequired}: {inviteRequired: boolean} = {inviteRequired: false},
): Promise<TestPDS> { ): Promise<TestPDS> {
const port = await getPort() const port = await getPort()
const port2 = await getPort(port + 1) const port2 = await getPort(port + 1)
const pdsUrl = `http://localhost:${port}` const pdsUrl = `http://localhost:${port}`
const testNet = await TestNetworkNoAppView.create({ const id = ids.next()
pds: {port, publicUrl: pdsUrl, inviteRequired}, const testNet = await TestNetwork.create({
pds: {
port,
publicUrl: pdsUrl,
inviteRequired,
dbPostgresSchema: `pds_${id}`,
},
bsky: {
dbPostgresSchema: `bsky_${id}`,
},
plc: {port: port2}, plc: {port: port2},
}) })
@ -48,7 +93,7 @@ class Mocker {
users: Record<string, TestUser> = {} users: Record<string, TestUser> = {}
constructor( constructor(
public testNet: TestNetworkNoAppView, public testNet: TestNetwork,
public service: string, public service: string,
public pic: Uint8Array, public pic: Uint8Array,
) { ) {
@ -59,6 +104,10 @@ class Mocker {
return this.testNet.pds return this.testNet.pds
} }
get bsky() {
return this.testNet.bsky
}
get plc() { get plc() {
return this.testNet.plc return this.testNet.plc
} }
@ -81,11 +130,7 @@ class Mocker {
const inviteRes = await agent.api.com.atproto.server.createInviteCode( const inviteRes = await agent.api.com.atproto.server.createInviteCode(
{useCount: 1}, {useCount: 1},
{ {
headers: { headers: this.pds.adminAuthHeaders('admin'),
authorization: `Basic ${btoa(
`admin:${this.pds.ctx.cfg.adminPassword}`,
)}`,
},
encoding: 'application/json', encoding: 'application/json',
}, },
) )
@ -260,11 +305,7 @@ class Mocker {
await agent.api.com.atproto.server.createInviteCode( await agent.api.com.atproto.server.createInviteCode(
{useCount: 1, forAccount}, {useCount: 1, forAccount},
{ {
headers: { headers: this.pds.adminAuthHeaders('admin'),
authorization: `Basic ${btoa(
`admin:${this.pds.ctx.cfg.adminPassword}`,
)}`,
},
encoding: 'application/json', encoding: 'application/json',
}, },
) )
@ -275,24 +316,21 @@ class Mocker {
if (!did) { if (!did) {
throw new Error(`Invalid user: ${user}`) throw new Error(`Invalid user: ${user}`)
} }
const ctx = this.pds.ctx const ctx = this.bsky.ctx
if (!ctx) { if (!ctx) {
throw new Error('Invalid PDS') throw new Error('Invalid appview')
} }
const labelSrvc = ctx.services.label(ctx.db.getPrimary())
await ctx.db.db await labelSrvc.createLabels([
.insertInto('label') {
.values([ src: ctx.cfg.labelerDid,
{ uri: did,
src: ctx.cfg.labelerDid, cid: '',
uri: did, val: label,
cid: '', neg: false,
val: label, cts: new Date().toISOString(),
neg: 0, },
cts: new Date().toISOString(), ])
},
])
.execute()
} }
async labelProfile(label: string, user: string) { async labelProfile(label: string, user: string) {
@ -307,43 +345,39 @@ class Mocker {
rkey: 'self', rkey: 'self',
}) })
const ctx = this.pds.ctx const ctx = this.bsky.ctx
if (!ctx) { if (!ctx) {
throw new Error('Invalid PDS') throw new Error('Invalid appview')
} }
await ctx.db.db const labelSrvc = ctx.services.label(ctx.db.getPrimary())
.insertInto('label') await labelSrvc.createLabels([
.values([ {
{ src: ctx.cfg.labelerDid,
src: ctx.cfg.labelerDid, uri: profile.uri,
uri: profile.uri, cid: profile.cid,
cid: profile.cid, val: label,
val: label, neg: false,
neg: 0, cts: new Date().toISOString(),
cts: new Date().toISOString(), },
}, ])
])
.execute()
} }
async labelPost(label: string, {uri, cid}: {uri: string; cid: string}) { async labelPost(label: string, {uri, cid}: {uri: string; cid: string}) {
const ctx = this.pds.ctx const ctx = this.bsky.ctx
if (!ctx) { if (!ctx) {
throw new Error('Invalid PDS') throw new Error('Invalid appview')
} }
await ctx.db.db const labelSrvc = ctx.services.label(ctx.db.getPrimary())
.insertInto('label') await labelSrvc.createLabels([
.values([ {
{ src: ctx.cfg.labelerDid,
src: ctx.cfg.labelerDid, uri,
uri, cid,
cid, val: label,
val: label, neg: false,
neg: 0, cts: new Date().toISOString(),
cts: new Date().toISOString(), },
}, ])
])
.execute()
} }
async createMuteList(user: string, name: string): Promise<string> { async createMuteList(user: string, name: string): Promise<string> {

View File

@ -18,7 +18,7 @@
"test-coverage": "jest --coverage", "test-coverage": "jest --coverage",
"lint": "eslint ./src --ext .js,.jsx,.ts,.tsx", "lint": "eslint ./src --ext .js,.jsx,.ts,.tsx",
"typecheck": "tsc --project ./tsconfig.check.json", "typecheck": "tsc --project ./tsconfig.check.json",
"e2e:mock-server": "ts-node __e2e__/mock-server.ts", "e2e:mock-server": "./jest/dev-infra/with-test-redis-and-db.sh ts-node __e2e__/mock-server.ts",
"e2e:metro": "RN_SRC_EXT=e2e.ts,e2e.tsx expo run:ios", "e2e:metro": "RN_SRC_EXT=e2e.ts,e2e.tsx expo run:ios",
"e2e:build": "detox build -c ios.sim.debug", "e2e:build": "detox build -c ios.sim.debug",
"e2e:run": "detox test --configuration ios.sim.debug --take-screenshots all", "e2e:run": "detox test --configuration ios.sim.debug --take-screenshots all",
@ -186,7 +186,7 @@
"babel-loader": "^9.1.2", "babel-loader": "^9.1.2",
"babel-plugin-module-resolver": "^5.0.0", "babel-plugin-module-resolver": "^5.0.0",
"babel-plugin-react-native-web": "^0.18.12", "babel-plugin-react-native-web": "^0.18.12",
"detox": "^20.11.3", "detox": "^20.13.0",
"eslint": "^8.19.0", "eslint": "^8.19.0",
"eslint-plugin-detox": "^1.0.0", "eslint-plugin-detox": "^1.0.0",
"eslint-plugin-ft-flow": "^2.0.3", "eslint-plugin-ft-flow": "^2.0.3",

View File

@ -0,0 +1,24 @@
import {makeAutoObservable} from 'mobx'
import {RootStoreModel} from '../root-store'
export class Reminders {
constructor(public rootStore: RootStoreModel) {
makeAutoObservable(
this,
{serialize: false, hydrate: false},
{autoBind: true},
)
}
serialize() {
return {}
}
hydrate(_v: unknown) {}
get shouldRequestEmailConfirmation() {
return false
}
setEmailConfirmationRequested() {}
}

View File

@ -8135,10 +8135,10 @@ detect-port-alt@^1.1.6:
address "^1.0.1" address "^1.0.1"
debug "^2.6.0" debug "^2.6.0"
detox@^20.11.3: detox@^20.13.0:
version "20.11.3" version "20.13.0"
resolved "https://registry.yarnpkg.com/detox/-/detox-20.11.3.tgz#56d5ea869977f5a747e1be0901b279ab953f8b7b" resolved "https://registry.yarnpkg.com/detox/-/detox-20.13.0.tgz#923111638dfdb16089eea4f07bf4f0b56468d097"
integrity sha512-kdoRAtDLFxXpjt1QlniI+WryMtf7Y8mrZ33Ql8cTR9qoCS/CThi4pweYAQm8yUPqAv1ZtT3eIm3EzRwjEosgLA== integrity sha512-p9MUcoHWFTqSDaoaN+/hnJYdzNYqdelUr/sxzy3zLoS/qehnVJv2yG9pYqz/+gKpJaMIpw2+TVw9imdAx5JpaA==
dependencies: dependencies:
ajv "^8.6.3" ajv "^8.6.3"
bunyan "^1.8.12" bunyan "^1.8.12"