Skip to content

Commit e8a38b0

Browse files
committed
e2e: add initial bundle server clone test
Add a test and its corresponding step definitions verifying that a remote repository initialized in a bundle server can be cloned with '--bundle-uri' to download bundles. Helped-by: Derrick Stolee <[email protected]> Signed-off-by: Victoria Dye <[email protected]>
1 parent b6dec91 commit e8a38b0

File tree

10 files changed

+205
-2
lines changed

10 files changed

+205
-2
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@
33
/bin/
44
/_dist/
55
/_docs/
6+
/_test/
67
node_modules/

test/e2e/features/basic.feature

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,9 @@ Feature: Basic bundle server usage
22

33
Background: The bundle web server is running
44
Given the bundle web server was started at port 8080
5+
6+
Scenario: A user can clone with a bundle URI pointing to the bundle server
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

test/e2e/features/classes/bundleServer.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,20 @@
1+
import { randomBytes } from 'crypto'
12
import * as child_process from 'child_process'
3+
import { RemoteRepo } from './remote'
24

35
export class BundleServer {
6+
private bundleServerCmd: string
47
private bundleWebServerCmd: string
58

69
// Web server
710
private webServerProcess: child_process.ChildProcess | undefined
11+
private bundleUriBase: string | undefined
812

9-
constructor(bundleWebServerCmd: string) {
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
1018
this.bundleWebServerCmd = bundleWebServerCmd
1119
}
1220

@@ -15,6 +23,23 @@ export class BundleServer {
1523
throw new Error("Tried to start web server, but web server is already running")
1624
}
1725
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) {
36+
throw new Error("Tried to get bundle URI before starting the web server")
37+
}
38+
if (!this.route) {
39+
throw new Error("Tried to get bundle URI before running 'init'")
40+
}
41+
42+
return this.bundleUriBase + this.route
1843
}
1944

2045
cleanup(): void {
@@ -24,5 +49,10 @@ export class BundleServer {
2449
console.warn("Web server process was not successfully stopped")
2550
}
2651
}
52+
53+
// Delete the added route
54+
if (this.route) {
55+
child_process.spawnSync(this.bundleServerCmd, ["delete", this.route])
56+
}
2757
}
2858
}

test/e2e/features/classes/remote.ts

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

45
Given('the bundle web server was started at port {int}', async function (this: BundleServerWorld, port: number) {
56
this.bundleServer.startWebServer(port)
67
})
8+
9+
Given('the bundle server has been initialized with the remote repo', async function (this: BundleServerWorld) {
10+
if (this.remote === undefined) {
11+
throw new Error("Remote repository is not initialized")
12+
}
13+
utils.assertStatus(0, this.bundleServer.init(this.remote))
14+
})
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { Given, } from '@cucumber/cucumber'
2+
import { RemoteRepo } from '../classes/remote'
3+
import { BundleServerWorld } from '../support/world'
4+
5+
/**
6+
* Steps relating to the setup of the remote repository users will clone from.
7+
*/
8+
9+
Given('a remote repository {string}', async function (this: BundleServerWorld, url: string) {
10+
this.remote = new RemoteRepo(url)
11+
})
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import * as assert from 'assert'
2+
import * as utils from '../support/utils'
3+
import { BundleServerWorld, User } from '../support/world'
4+
import { Given, When, Then } from '@cucumber/cucumber'
5+
6+
/**
7+
* Steps relating to the repository clones that users work with directly. Since
8+
* these steps represent the actions a user takes, the majority of end-to-end
9+
* test steps will live here.
10+
*/
11+
12+
When('I clone from the remote repo with a bundle URI', async function (this: BundleServerWorld) {
13+
this.cloneRepositoryFor(User.Me, this.bundleServer.bundleUri())
14+
})
15+
16+
Then('bundles are downloaded and used', async function (this: BundleServerWorld) {
17+
const clonedRepo = this.getRepo(User.Me)
18+
19+
// Verify the clone executed as-expected
20+
utils.assertStatus(0, clonedRepo.cloneResult, "git clone failed")
21+
22+
// Ensure warning wasn't thrown
23+
clonedRepo.cloneResult.stderr.toString().split("\n").forEach(function (line) {
24+
if (line.startsWith("warning: failed to download bundle from URI")) {
25+
assert.fail(line)
26+
}
27+
})
28+
29+
// Make sure the config is set up properly
30+
let result = clonedRepo.runGit("config", "--get", "fetch.bundleURI")
31+
utils.assertStatus(0, result, "'fetch.bundleURI' is not set after clone")
32+
const actualURI = result.stdout.toString().trim()
33+
assert.strictEqual(actualURI, this.bundleServer.bundleUri())
34+
35+
result = clonedRepo.runGit("for-each-ref", "--format=%(refname)", "refs/bundles/*")
36+
utils.assertStatus(0, result, "git for-each-ref failed")
37+
38+
const bundleRefs = result.stdout.toString().split("\n").filter(function(line) {
39+
return line.trim() != ""
40+
})
41+
assert.strict(bundleRefs.length > 0, "No bundle refs found in the repo")
42+
})

test/e2e/features/support/utils.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
11
import * as path from 'path'
2+
import * as assert from 'assert'
3+
import * as child_process from 'child_process'
4+
5+
export function runGit(...args: string[]): child_process.SpawnSyncReturns<Buffer> {
6+
return child_process.spawnSync("git", args)
7+
}
28

39
export function absPath(pathParam: string): string {
410
// Convert a given path (either relative to 'test/e2e/' or absolute) to an
@@ -9,3 +15,11 @@ export function absPath(pathParam: string): string {
915
return pathParam
1016
}
1117
}
18+
19+
export function assertStatus(expectedStatusCode: number, result: child_process.SpawnSyncReturns<Buffer>, message?: string): void {
20+
if (result.error) {
21+
throw result.error
22+
}
23+
assert.strictEqual(result.status, expectedStatusCode,
24+
`${message ?? "Invalid status code"}:\n\tstdout: ${result.stdout.toString()}\n\tstderr: ${result.stderr.toString()}`)
25+
}

test/e2e/features/support/world.ts

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,71 @@
11
import { setWorldConstructor, World, IWorldOptions } from '@cucumber/cucumber'
2+
import { RemoteRepo } from '../classes/remote'
23
import * as utils from './utils'
4+
import * as fs from 'fs'
5+
import * as path from 'path'
6+
import { ClonedRepository } from '../classes/repository'
37
import { BundleServer } from '../classes/bundleServer'
48

9+
export enum User {
10+
Me = 1,
11+
}
12+
513
interface BundleServerParameters {
14+
bundleServerCommand: string
615
bundleWebServerCommand: string
16+
trashDirectoryBase: string
717
}
818

919
export class BundleServerWorld extends World<BundleServerParameters> {
20+
// Internal variables
21+
trashDirectory: string
22+
1023
// Bundle server
1124
bundleServer: BundleServer
25+
remote: RemoteRepo | undefined
26+
27+
// Users
28+
repoMap: Map<User, ClonedRepository>
1229

1330
constructor(options: IWorldOptions<BundleServerParameters>) {
1431
super(options)
1532

33+
this.repoMap = new Map<User, ClonedRepository>()
34+
35+
// Set up the trash directory
36+
this.trashDirectory = path.join(utils.absPath(this.parameters.trashDirectoryBase), randomUUID())
37+
fs.mkdirSync(this.trashDirectory, { recursive: true });
38+
1639
// Set up the bundle server
17-
this.bundleServer = new BundleServer(utils.absPath(this.parameters.bundleWebServerCommand))
40+
this.bundleServer = new BundleServer(utils.absPath(this.parameters.bundleServerCommand),
41+
utils.absPath(this.parameters.bundleWebServerCommand))
42+
}
43+
44+
cloneRepositoryFor(user: User, bundleUri?: string): void {
45+
if (!this.remote) {
46+
throw new Error("Remote repository is not initialized")
47+
}
48+
49+
const repoRoot = `${this.trashDirectory}/${User[user]}`
50+
this.repoMap.set(user, new ClonedRepository(this.remote, repoRoot, bundleUri))
51+
}
52+
53+
getRepo(user: User): ClonedRepository {
54+
const repo = this.repoMap.get(user)
55+
if (!repo) {
56+
throw new Error("Cloned repository has not been initialized")
57+
}
58+
59+
return repo
60+
}
61+
1862
}
1963

2064
cleanup(): void {
2165
this.bundleServer.cleanup()
66+
67+
// Delete the trash directory
68+
fs.rmSync(this.trashDirectory, { recursive: true })
2269
}
2370
}
2471

0 commit comments

Comments
 (0)