Skip to content

Commit 2b2ed70

Browse files
committed
e2e: more WIP
Signed-off-by: Victoria Dye <[email protected]>
1 parent da1fe7c commit 2b2ed70

File tree

7 files changed

+197
-76
lines changed

7 files changed

+197
-76
lines changed

test/e2e/features/basic.feature

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,36 @@
11
Feature: Basic bundle server usage
22

33
Background: The bundle web server is running
4-
Given the bundle web server is running at port 8080
4+
Given the bundle web server was started at port 8080
55

66
Scenario: First-time bundle server setup
7-
Given the bundle server has been initialized with 'https://github.com/vdye/asset-hash.git'
8-
When I clone from 'https://github.com/vdye/asset-hash.git' with a bundle URI
9-
Then bundles are downloaded and used
7+
Given a remote repository 'https://github.com/vdye/asset-hash.git'
8+
Given the bundle server has been initialized with the remote repo
9+
When I clone from the remote repo with a bundle URI
10+
Then bundles are downloaded and used
11+
12+
# Scenario: Bundle server is behind remote
13+
# Given a new remote repository with main branch 'main'
14+
# Given the bundle server has been initialized with the remote repo
15+
# Given I clone from the remote repo with a bundle URI
16+
# Given another developer pushes 2 commits to 'main'
17+
# When I fetch from the remote
18+
# Then I am up-to-date with 'main'
19+
20+
# Scenario: Fetch an incremental bundle
21+
# Given a remote repository with 10 commits to 'main'
22+
# Given the bundle server has been initialized with the remote repo
23+
# Given I clone from the local repo with a bundle URI
24+
# Given another developer pushes 2 commits to 'main'
25+
# Given the bundle server is updated for the local repo
26+
# When I fetch from the remote
27+
# Then I am up-to-date with 'main'
28+
29+
# Scenario: Bundle with a force-push
30+
# Given a remote repository with 10 commits to 'main'
31+
# Given the bundle server has been initialized with the local repo
32+
# Given I clone from the local repo with a bundle URI
33+
# Given another developer removes 2 commits and adds 4 commits to 'main'
34+
# Given the bundle server is updated for the local repo
35+
# When I fetch from the remote
36+
# Then I am up-to-date with 'main'

test/e2e/features/step_definitions/steps.ts

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,45 @@
11
import * as assert from 'assert'
2-
import { BundleServerWorld } from '../support/world'
2+
import { BundleServerWorld, User } from '../support/world'
3+
import { RemoteRepo } from '../support/remote'
34
import { Given, When, Then, After } from '@cucumber/cucumber'
45

5-
Given('the bundle web server is running at port {int}', async function(this: BundleServerWorld, port: number) {
6-
this.startWebServer(port)
6+
Given('the bundle web server was started at port {int}', async function(this: BundleServerWorld, port: number) {
7+
this.bundleServer.startWebServer(port)
78
})
89

9-
Given('the bundle server has been initialized with {string}', async function(this: BundleServerWorld, url: string) {
10-
const result = this.bundleServerInit(url)
11-
assert.strictEqual(result.status, 0, "git-bundle-server init failed")
10+
Given('a remote repository {string}', async function(this: BundleServerWorld, url: string) {
11+
this.remote = new RemoteRepo(false, url)
1212
})
1313

14-
When('I clone from {string} with a bundle URI', async function(this: BundleServerWorld, url: string) {
15-
this.cloneWithBundleUri(url)
14+
Given('the bundle server has been initialized with the remote repo', async function(this: BundleServerWorld) {
15+
if (this.remote === undefined) {
16+
throw new Error("Remote repository is not initialized")
17+
}
18+
this.bundleServer.init(this.remote)
19+
})
20+
21+
When('I clone from the remote repo with a bundle URI', async function(this: BundleServerWorld) {
22+
this.cloneRepositoryFor(User.Me, this.bundleServer.bundleUri())
1623
})
1724

1825
Then('bundles are downloaded and used', async function(this: BundleServerWorld) {
19-
// Verify the command executed as-expected
20-
if (this.cloneResult === undefined) {
21-
throw new Error("Tried to access clone result before clone")
22-
}
26+
const clonedRepo = this.getRepo(User.Me)
2327

24-
// Exit code
25-
assert.strictEqual(this.cloneResult.status, 0, "git clone failed")
28+
// Verify the clone executed as-expected
29+
assert.strictEqual(clonedRepo.cloneResult.status, 0, "git clone failed")
2630

2731
// Ensure warning wasn't thrown
28-
this.cloneResult.stderr.toString().split("\n").forEach(function(line) {
32+
clonedRepo.cloneResult.stderr.toString().split("\n").forEach(function(line) {
2933
if (line.startsWith("warning: failed to download bundle from URI")) {
3034
assert.fail(line)
3135
}
3236
})
3337

34-
// TODO: verify results of 'for-each-ref'
35-
const result = this.runGitInLocalRepo("for-each-ref", "--format=%(refname)")
38+
const result = clonedRepo.runGit("for-each-ref", "--format=%(refname)")
3639
assert.strictEqual(result.status, 0, "git for-each-ref failed")
3740

3841
const bundleRefRegex = new RegExp('^refs/bundles/.*$')
39-
const bundleRefs = this.cloneResult.stdout.toString().split("\n").filter(function(line) {
42+
const bundleRefs = result.stdout.toString().split("\n").filter(function(line) {
4043
return bundleRefRegex.test(line)
4144
})
4245
assert.strict(bundleRefs.length > 0, "No bundle refs found in the repo")
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { randomBytes } from 'crypto'
2+
import * as child_process from 'child_process'
3+
import { RemoteRepo } from './remote'
4+
5+
export class BundleServer {
6+
private bundleServerCmd: string
7+
private bundleWebServerCmd: string
8+
9+
// Web server
10+
private webServerProcess: child_process.ChildProcess | undefined
11+
private bundleUriBase: string | undefined
12+
13+
// Remote repo info (for now, only support one per test)
14+
private route: string | undefined
15+
16+
constructor(bundleServerCmd: string, bundleWebServerCmd: string) {
17+
this.bundleServerCmd = bundleServerCmd
18+
this.bundleWebServerCmd = bundleWebServerCmd
19+
}
20+
21+
startWebServer(port: number): void {
22+
if (this.webServerProcess !== undefined) {
23+
throw new Error("Tried to start web server, but web server is already running")
24+
}
25+
this.webServerProcess = child_process.spawn(this.bundleWebServerCmd, ["--port", String(port)])
26+
this.bundleUriBase = `http://localhost:${port}/`
27+
}
28+
29+
init(remote: RemoteRepo): child_process.SpawnSyncReturns<Buffer> {
30+
this.route = `e2e/${randomBytes(8).toString('hex')}`
31+
return child_process.spawnSync(this.bundleServerCmd, ["init", remote.remoteUri, this.route])
32+
}
33+
34+
bundleUri(): string {
35+
if (this.webServerProcess === undefined) {
36+
throw new Error("Tried to get bundle URI before starting the web server")
37+
}
38+
if (this.route === undefined) {
39+
throw new Error("Tried to get bundle URI before running 'init'")
40+
}
41+
42+
return this.bundleUriBase + this.route
43+
}
44+
45+
cleanup(): void {
46+
if (this.webServerProcess) {
47+
const killed = this.webServerProcess.kill('SIGINT')
48+
if (!killed) {
49+
console.warn("Web server process was not successfully stopped")
50+
}
51+
}
52+
53+
// Delete the added route
54+
if (this.route !== undefined) {
55+
child_process.spawnSync(this.bundleServerCmd, ["delete", this.route])
56+
}
57+
}
58+
}

test/e2e/features/support/remote.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export class RemoteRepo {
2+
remoteUri: string
3+
private isLocal: boolean
4+
5+
constructor(isLocal: boolean, uri: string) {
6+
this.remoteUri = uri
7+
this.isLocal = isLocal
8+
}
9+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import * as child_process from 'child_process'
2+
import { RemoteRepo } from './remote'
3+
4+
export class ClonedRepository {
5+
private initialized: boolean
6+
private root: string
7+
private remote: RemoteRepo | undefined
8+
9+
cloneResult: child_process.SpawnSyncReturns<Buffer>
10+
11+
constructor(remote: RemoteRepo, root: string, bundleUri?: string) {
12+
this.initialized = false
13+
this.remote = remote
14+
this.root = root
15+
16+
// Clone the remote repository
17+
let args = ["clone"]
18+
if (bundleUri) {
19+
args.push(`--bundle-uri=${bundleUri}`)
20+
}
21+
args.push(this.remote.remoteUri, this.root)
22+
23+
this.cloneResult = child_process.spawnSync("git", args)
24+
if (!this.cloneResult.error) {
25+
this.initialized = true
26+
}
27+
}
28+
29+
runGit(...args: string[]): child_process.SpawnSyncReturns<Buffer> {
30+
if (!this.initialized) {
31+
throw new Error("Repository is not initialized")
32+
}
33+
34+
return child_process.spawnSync("git", ["-C", this.root].concat(args))
35+
}
36+
}

test/e2e/features/support/utils.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import * as path from 'path'
2+
3+
export function absPath(pathParam: string): string {
4+
// Convert a path (possibly relative to 'test/e2e/') to an absolute path
5+
if (!path.isAbsolute(pathParam)) {
6+
return path.resolve(__dirname, "../..", pathParam)
7+
} else {
8+
return pathParam
9+
}
10+
}

test/e2e/features/support/world.ts

Lines changed: 32 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
11
import { setWorldConstructor, World, IWorldOptions } from '@cucumber/cucumber'
2-
import { randomUUID, randomBytes } from 'crypto'
3-
import * as child_process from 'child_process'
2+
import { randomUUID } from 'crypto'
3+
import { RemoteRepo } from './remote'
4+
import { absPath } from './utils'
45
import * as fs from 'fs'
56
import * as path from 'path'
7+
import { ClonedRepository } from './repository'
8+
import { BundleServer } from './bundleServer'
9+
10+
export enum User {
11+
Me = 1,
12+
Another,
13+
}
614

715
interface BundleServerParameters {
816
bundleServerCommand: string
@@ -13,78 +21,48 @@ interface BundleServerParameters {
1321
export class BundleServerWorld extends World<BundleServerParameters> {
1422
// Internal variables
1523
private trashDirectory: string
16-
private webServer: child_process.ChildProcess | undefined
17-
private bundleUriBase: string | undefined
18-
private route: string | undefined
1924

20-
cloneResult: child_process.SpawnSyncReturns<Buffer> | undefined
25+
// Bundle server
26+
bundleServer: BundleServer
27+
remote: RemoteRepo | undefined
2128

29+
// Users
30+
private repoMap: Map<User, ClonedRepository>
2231

2332
constructor(options: IWorldOptions<BundleServerParameters>) {
2433
super(options)
2534

35+
this.repoMap = new Map<User, ClonedRepository>()
36+
2637
// Set up the trash directory
27-
this.trashDirectory = this.pathParamToAbsolute(
28-
path.join(this.parameters.trashDirectoryBase, randomUUID())
29-
)
38+
this.trashDirectory = path.join(absPath(this.parameters.trashDirectoryBase), randomUUID())
3039
fs.mkdirSync(this.trashDirectory, { recursive: true });
31-
}
3240

33-
pathParamToAbsolute(relPath: string): string {
34-
let absPath = this.parameters.bundleWebServerCommand
35-
if (!path.isAbsolute(absPath)) {
36-
// Assume relative path is relative to 'test/e2e'
37-
absPath = path.resolve(__dirname, "../..", relPath)
38-
}
39-
return absPath
41+
// Set up the bundle server
42+
this.bundleServer = new BundleServer(absPath(this.parameters.bundleServerCommand),
43+
absPath(this.parameters.bundleWebServerCommand))
4044
}
4145

42-
startWebServer(port: number): void {
43-
if (this.webServer !== undefined) {
44-
throw new Error("Tried to start web server, but web server is already running")
46+
cloneRepositoryFor(user: User, bundleUri?: string): void {
47+
if (this.remote === undefined) {
48+
throw new Error("Remote repository is not initialized")
4549
}
4650

47-
const webServerCmd = this.pathParamToAbsolute(this.parameters.bundleWebServerCommand)
48-
this.webServer = child_process.spawn(webServerCmd, ["--port", String(port)])
49-
this.bundleUriBase = `http://localhost:${port}/`
50-
}
51-
52-
bundleServerInit(url: string): child_process.SpawnSyncReturns<Buffer> {
53-
const serverCliCmd = this.pathParamToAbsolute(this.parameters.bundleServerCommand)
54-
this.route = `e2e/${randomBytes(8).toString('hex')}`
55-
return child_process.spawnSync(serverCliCmd, ["init", url, this.route])
51+
const repoRoot = `${this.trashDirectory}/${User[user]}`
52+
this.repoMap.set(user, new ClonedRepository(this.remote, repoRoot, bundleUri))
5653
}
5754

58-
cloneWithBundleUri(url: string): void {
59-
if (this.route === undefined || this.bundleUriBase === undefined) {
60-
throw new Error("Bundle URI is not initialized")
55+
getRepo(user: User): ClonedRepository {
56+
const repo = this.repoMap.get(user)
57+
if (repo === undefined) {
58+
throw new Error("Cloned repository has not been initialized")
6159
}
6260

63-
this.cloneResult = child_process.spawnSync("git",
64-
["clone", `--bundle-uri=${this.bundleUriBase}/${this.route}/`, url, this.trashDirectory]
65-
)
66-
}
67-
68-
runGitInLocalRepo(...args: string[]): child_process.SpawnSyncReturns<Buffer> {
69-
return child_process.spawnSync("git",
70-
["-C", this.trashDirectory].concat(args)
71-
)
61+
return repo
7262
}
7363

7464
cleanup(): void {
75-
// Stop the web server
76-
if (this.webServer !== undefined) {
77-
const killed = this.webServer.kill('SIGINT')
78-
if (!killed) {
79-
console.warn("Web server process was not successfully stopped")
80-
}
81-
}
82-
83-
// Delete the added route
84-
if (this.route !== undefined) {
85-
const serverCliCmd = this.pathParamToAbsolute(this.parameters.bundleServerCommand)
86-
child_process.spawnSync(serverCliCmd, ["delete", this.route])
87-
}
65+
this.bundleServer.cleanup()
8866

8967
// Delete the trash directory
9068
fs.rmSync(this.trashDirectory, { recursive: true })

0 commit comments

Comments
 (0)