Skip to content

Commit 10fee03

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

File tree

13 files changed

+258
-88
lines changed

13 files changed

+258
-88
lines changed

test/e2e/features/basic.feature

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,28 +3,32 @@ Feature: Basic bundle server usage
33
Background: The bundle web server is running
44
Given the bundle web server was started at port 8080
55

6-
Scenario: First-time bundle server setup
6+
Scenario: A user can clone with a bundle URI pointing to the bundle server
77
Given a remote repository 'https://github.com/vdye/asset-hash.git'
88
Given the bundle server has been initialized with the remote repo
99
When I clone from the remote repo with a bundle URI
1010
Then bundles are downloaded and used
1111

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'
12+
Scenario: A user can fetch with a bundle server that's behind and get all updates
13+
Given a new remote repository with main branch 'main'
14+
Given another user pushed 10 commits to 'main'
15+
Given the bundle server has been initialized with the remote repo
16+
Given I cloned from the remote repo with a bundle URI
17+
Given another user pushed 2 commits to 'main'
18+
When I fetch from the remote
19+
Then I am up-to-date with 'main'
20+
Then my repo's bundles are not up-to-date with 'main'
1921

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'
22+
Scenario: A user will fetch incremental bundles to stay up-to-date
23+
Given a new remote repository with main branch 'main'
24+
Given another user pushed 10 commits to 'main'
25+
Given the bundle server has been initialized with the remote repo
26+
Given I cloned from the remote repo with a bundle URI
27+
Given another user pushed 2 commits to 'main'
28+
Given the bundle server was updated for the remote repo
29+
When I fetch from the remote
30+
Then I am up-to-date with 'main'
31+
Then my repo's bundles are up-to-date with 'main'
2832

2933
# Scenario: Bundle with a force-push
3034
# Given a remote repository with 10 commits to 'main'

test/e2e/features/support/bundleServer.ts renamed to test/e2e/features/classes/bundleServer.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export class BundleServer {
1919
}
2020

2121
startWebServer(port: number): void {
22-
if (this.webServerProcess !== undefined) {
22+
if (this.webServerProcess) {
2323
throw new Error("Tried to start web server, but web server is already running")
2424
}
2525
this.webServerProcess = child_process.spawn(this.bundleWebServerCmd, ["--port", String(port)])
@@ -31,11 +31,18 @@ export class BundleServer {
3131
return child_process.spawnSync(this.bundleServerCmd, ["init", remote.remoteUri, this.route])
3232
}
3333

34+
update(): child_process.SpawnSyncReturns<Buffer> {
35+
if (!this.route) {
36+
throw new Error("Tried to update server before running 'init'")
37+
}
38+
return child_process.spawnSync(this.bundleServerCmd, ["update", this.route])
39+
}
40+
3441
bundleUri(): string {
35-
if (this.webServerProcess === undefined) {
42+
if (!this.webServerProcess) {
3643
throw new Error("Tried to get bundle URI before starting the web server")
3744
}
38-
if (this.route === undefined) {
45+
if (!this.route) {
3946
throw new Error("Tried to get bundle URI before running 'init'")
4047
}
4148

@@ -51,7 +58,7 @@ export class BundleServer {
5158
}
5259

5360
// Delete the added route
54-
if (this.route !== undefined) {
61+
if (this.route) {
5562
child_process.spawnSync(this.bundleServerCmd, ["delete", this.route])
5663
}
5764
}

test/e2e/features/classes/remote.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import * as path from 'path'
2+
import * as utils from '../support/utils'
3+
import * as child_process from 'child_process'
4+
5+
export class RemoteRepo {
6+
isLocal: boolean
7+
remoteUri: string
8+
root: string
9+
10+
constructor(isLocal: boolean, urlOrPath: string, mainBranch?: string) {
11+
this.isLocal = isLocal
12+
if (!this.isLocal) {
13+
// Not a bare repo on the filesystem
14+
this.remoteUri = urlOrPath
15+
this.root = ""
16+
} else {
17+
// Bare repo on the filesystem - need to initialize
18+
if (urlOrPath.startsWith("file://")) {
19+
this.remoteUri = urlOrPath
20+
this.root = urlOrPath.substring(7)
21+
} else if (path.isAbsolute(urlOrPath)) {
22+
this.remoteUri = `file://${urlOrPath}`
23+
this.root = urlOrPath
24+
} else {
25+
throw new Error("'urlOrPath' must be a 'file://' URL or absolute path")
26+
}
27+
28+
utils.assertStatus(0, utils.runGit("init", "--bare", this.root))
29+
if (mainBranch) {
30+
utils.assertStatus(0, utils.runGit("-C", this.root, "symbolic-ref", "HEAD", `refs/heads/${mainBranch}`))
31+
}
32+
}
33+
}
34+
35+
getBranchTipOid(branch: string): string {
36+
if (!this.isLocal) {
37+
throw new Error("Logged branch tips are only available for local custom remotes")
38+
}
39+
const result = child_process.spawnSync("cat", [`refs/heads/${branch}`], { shell: true, cwd: this.root })
40+
utils.assertStatus(0, result)
41+
return result.stdout.toString().trim()
42+
}
43+
}

test/e2e/features/support/repository.ts renamed to test/e2e/features/classes/repository.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as child_process from 'child_process'
22
import { RemoteRepo } from './remote'
3+
import * as utils from '../support/utils'
34

45
export class ClonedRepository {
56
private initialized: boolean
@@ -26,11 +27,17 @@ export class ClonedRepository {
2627
}
2728
}
2829

29-
runGit(...args: string[]): child_process.SpawnSyncReturns<Buffer> {
30+
runShell(command: string, ...args: string[]): child_process.SpawnSyncReturns<Buffer> {
3031
if (!this.initialized) {
3132
throw new Error("Repository is not initialized")
3233
}
34+
return child_process.spawnSync(command, args, { shell: true, cwd: this.root })
35+
}
3336

34-
return child_process.spawnSync("git", ["-C", this.root].concat(args))
37+
runGit(...args: string[]): child_process.SpawnSyncReturns<Buffer> {
38+
if (!this.initialized) {
39+
throw new Error("Repository is not initialized")
40+
}
41+
return utils.runGit("-C", this.root, ...args)
3542
}
3643
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import * as utils from '../support/utils'
2+
import { BundleServerWorld, } from '../support/world'
3+
import { Given } from '@cucumber/cucumber'
4+
5+
Given('the bundle web server was started at port {int}', async function(this: BundleServerWorld, port: number) {
6+
this.bundleServer.startWebServer(port)
7+
})
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+
})
15+
16+
Given('the bundle server was updated for the remote repo', async function(this: BundleServerWorld) {
17+
utils.assertStatus(0, this.bundleServer.update())
18+
})
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { Given, } from '@cucumber/cucumber'
2+
import { RemoteRepo } from '../classes/remote'
3+
import { BundleServerWorld, } from '../support/world'
4+
import * as path from 'path'
5+
6+
/**
7+
* Steps relating to the setup of the remote repository users will clone from.
8+
*/
9+
10+
Given('a remote repository {string}', async function(this: BundleServerWorld, url: string) {
11+
this.remote = new RemoteRepo(false, url)
12+
})
13+
14+
Given('a new remote repository with main branch {string}', async function(this: BundleServerWorld, mainBranch: string) {
15+
this.remote = new RemoteRepo(true, path.join(this.trashDirectory, "server"), mainBranch)
16+
})
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import * as assert from 'assert'
2+
import * as utils from '../support/utils'
3+
import { randomBytes } from 'crypto'
4+
import { BundleServerWorld, User } from '../support/world'
5+
import { Given, When, Then } from '@cucumber/cucumber'
6+
7+
/**
8+
* Steps relating to the repository clones that users work with directly. Since
9+
* these steps represent the actions a user takes, the majority of end-to-end
10+
* test steps will live here.
11+
*/
12+
13+
Given('another user pushed {int} commits to {string}', async function(this: BundleServerWorld, commitNum: number, branch: string) {
14+
if (this.remote && !this.remote.isLocal) {
15+
throw new Error("Remote is not initialized or does not allow pushes")
16+
}
17+
18+
const user = User.Another
19+
if (!this.repoMap.has(user)) {
20+
this.cloneRepositoryFor(user)
21+
utils.assertStatus(0, this.getRepo(user).cloneResult)
22+
}
23+
const clonedRepo = this.getRepo(user)
24+
25+
// TODO: figure out a better way to check whether the repo is empty
26+
if (clonedRepo.runGit("show").status != 0) {
27+
// Repo is empty, so make sure we're on the right branch
28+
clonedRepo.runGit("branch", "-m", branch)
29+
} else {
30+
clonedRepo.runGit("switch", branch)
31+
clonedRepo.runGit("pull", "origin", branch)
32+
}
33+
34+
for (let i = 0; i < commitNum; i++) {
35+
utils.assertStatus(0, clonedRepo.runShell(`echo ${randomBytes(16).toString('hex')} >README.md`))
36+
utils.assertStatus(0, clonedRepo.runGit("add", "README.md"))
37+
utils.assertStatus(0, clonedRepo.runGit("commit", "-m", `test ${i+1}`))
38+
}
39+
utils.assertStatus(0, clonedRepo.runGit("push", "origin", branch))
40+
})
41+
42+
Given('I cloned from the remote repo with a bundle URI', async function(this: BundleServerWorld) {
43+
const user = User.Me
44+
this.cloneRepositoryFor(user, this.bundleServer.bundleUri())
45+
utils.assertStatus(0, this.getRepo(user).cloneResult)
46+
})
47+
48+
When('I clone from the remote repo with a bundle URI', async function(this: BundleServerWorld) {
49+
this.cloneRepositoryFor(User.Me, this.bundleServer.bundleUri())
50+
})
51+
52+
When('I fetch from the remote', async function(this: BundleServerWorld) {
53+
const clonedRepo = this.getRepo(User.Me)
54+
utils.assertStatus(0, clonedRepo.runGit("fetch", "origin"))
55+
})
56+
57+
Then('bundles are downloaded and used', async function(this: BundleServerWorld) {
58+
const clonedRepo = this.getRepo(User.Me)
59+
60+
// Verify the clone executed as-expected
61+
utils.assertStatus(0, clonedRepo.cloneResult, "git clone failed")
62+
63+
// Ensure warning wasn't thrown
64+
clonedRepo.cloneResult.stderr.toString().split("\n").forEach(function(line) {
65+
if (line.startsWith("warning: failed to download bundle from URI")) {
66+
assert.fail(line)
67+
}
68+
})
69+
70+
const result = clonedRepo.runGit("for-each-ref", "--format=%(refname)")
71+
assert.strictEqual(result.status, 0, "git for-each-ref failed")
72+
73+
const bundleRefRegex = new RegExp('^refs/bundles/.*$')
74+
const bundleRefs = result.stdout.toString().split("\n").filter(function(line) {
75+
return bundleRefRegex.test(line)
76+
})
77+
assert.strict(bundleRefs.length > 0, "No bundle refs found in the repo")
78+
})
79+
80+
Then('I am up-to-date with {string}', async function(this: BundleServerWorld, branch: string) {
81+
const clonedRepo = this.getRepo(User.Me)
82+
const result = clonedRepo.runGit("rev-parse", `refs/remotes/origin/${branch}`)
83+
utils.assertStatus(0, result)
84+
const actualOid = result.stdout.toString().trim()
85+
const expectedOid = this.remote?.getBranchTipOid(branch)
86+
assert.strictEqual(actualOid, expectedOid, `branch '${branch}' is not up-to-date`)
87+
})
88+
89+
Then('my repo\'s bundles {boolean} up-to-date with {string}',
90+
async function(this: BundleServerWorld, expectedUpToDate: boolean, branch: string) {
91+
const clonedRepo = this.getRepo(User.Me)
92+
const result = clonedRepo.runGit("rev-parse", `refs/bundles/${branch}`)
93+
utils.assertStatus(0, result)
94+
const actualOid = result.stdout.toString().trim()
95+
const expectedOid = this.remote?.getBranchTipOid(branch)
96+
97+
if (expectedUpToDate) {
98+
assert.strictEqual(actualOid, expectedOid, `bundle ref for '${branch}' is not up-to-date`)
99+
} else {
100+
assert.notStrictEqual(actualOid, expectedOid, `bundle ref for '${branch}' is up-to-date, but should not be`)
101+
}
102+
}
103+
)
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { BundleServerWorld } from '../support/world'
2+
import { After } from '@cucumber/cucumber'
3+
4+
/**
5+
* Steps handling operations that are common across tests.
6+
*/
7+
8+
After(function (this: BundleServerWorld) {
9+
this.cleanup()
10+
});

test/e2e/features/step_definitions/steps.ts

Lines changed: 0 additions & 50 deletions
This file was deleted.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { defineParameterType } from '@cucumber/cucumber'
2+
3+
defineParameterType({
4+
name: 'boolean',
5+
regexp: /are|are not/,
6+
transformer: s => s == "are"
7+
})

test/e2e/features/support/remote.ts

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

0 commit comments

Comments
 (0)