Skip to content

Commit 368fa56

Browse files
committed
e2e: add tests updating the remote & bundle server
Add scenarios to the end-to-end tests involving updates to the remote and/or bundle server. As part of this update, add a custom Cucumber expression for matching "are/are not" to a boolean. Helped-by: Derrick Stolee <[email protected]> Signed-off-by: Victoria Dye <[email protected]>
1 parent e8a38b0 commit 368fa56

File tree

10 files changed

+194
-4
lines changed

10 files changed

+194
-4
lines changed

test/e2e/cucumber.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ module.exports = {
88
snippetInterface: 'async-await'
99
},
1010
worldParameters: {
11+
bundleServerCommand: '../../bin/git-bundle-server',
1112
bundleWebServerCommand: '../../bin/git-bundle-web-server',
13+
trashDirectoryBase: '../../_test/e2e'
1214
}
1315
}
1416
}

test/e2e/features/basic.feature

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,35 @@ Feature: Basic bundle server usage
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
11+
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'
21+
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'
32+
33+
Scenario: A user can fetch force-pushed refs from the bundle server
34+
Given a new remote repository with main branch 'main'
35+
Given another user pushed 10 commits to 'main'
36+
Given the bundle server has been initialized with the remote repo
37+
Given I cloned from the remote repo with a bundle URI
38+
Given another user removed 2 commits and added 4 commits to 'main'
39+
Given the bundle server was updated for the remote repo
40+
When I fetch from the remote
41+
Then I am up-to-date with 'main'
42+
Then my repo's bundles are up-to-date with 'main'

test/e2e/features/classes/bundleServer.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,13 @@ 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 {
3542
if (!this.webServerProcess) {
3643
throw new Error("Tried to get bundle URI before starting the web server")

test/e2e/features/classes/remote.ts

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,43 @@
1+
import * as path from 'path'
2+
import * as utils from '../support/utils'
3+
import * as child_process from 'child_process'
4+
15
export class RemoteRepo {
6+
isLocal: boolean
27
remoteUri: string
38
root: string
49

5-
constructor(url: string) {
6-
this.remoteUri = url
7-
this.root = ""
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()
842
}
43+
}

test/e2e/features/classes/repository.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,13 @@ export class ClonedRepository {
2727
}
2828
}
2929

30+
runShell(command: string, ...args: string[]): child_process.SpawnSyncReturns<Buffer> {
31+
if (!this.initialized) {
32+
throw new Error("Repository is not initialized")
33+
}
34+
return child_process.spawnSync(command, args, { shell: true, cwd: this.root })
35+
}
36+
3037
runGit(...args: string[]): child_process.SpawnSyncReturns<Buffer> {
3138
if (!this.initialized) {
3239
throw new Error("Repository is not initialized")

test/e2e/features/step_definitions/bundleServer.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,7 @@ Given('the bundle server has been initialized with the remote repo', async funct
1212
}
1313
utils.assertStatus(0, this.bundleServer.init(this.remote))
1414
})
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: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
import { Given, } from '@cucumber/cucumber'
22
import { RemoteRepo } from '../classes/remote'
33
import { BundleServerWorld } from '../support/world'
4+
import * as path from 'path'
45

56
/**
67
* Steps relating to the setup of the remote repository users will clone from.
78
*/
89

910
Given('a remote repository {string}', async function (this: BundleServerWorld, url: string) {
10-
this.remote = new RemoteRepo(url)
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)
1116
})

test/e2e/features/step_definitions/repository.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as assert from 'assert'
22
import * as utils from '../support/utils'
3+
import { randomBytes } from 'crypto'
34
import { BundleServerWorld, User } from '../support/world'
45
import { Given, When, Then } from '@cucumber/cucumber'
56

@@ -9,10 +10,51 @@ import { Given, When, Then } from '@cucumber/cucumber'
910
* test steps will live here.
1011
*/
1112

13+
Given('another user pushed {int} commits to {string}', async function (this: BundleServerWorld, commitNum: number, branch: string) {
14+
const clonedRepo = this.getRepoAtBranch(User.Another, branch)
15+
16+
for (let i = 0; i < commitNum; i++) {
17+
utils.assertStatus(0, clonedRepo.runShell(`echo ${randomBytes(16).toString('hex')} >README.md`))
18+
utils.assertStatus(0, clonedRepo.runGit("add", "README.md"))
19+
utils.assertStatus(0, clonedRepo.runGit("commit", "-m", `test ${i + 1}`))
20+
}
21+
utils.assertStatus(0, clonedRepo.runGit("push", "origin", branch))
22+
})
23+
24+
Given('another user removed {int} commits and added {int} commits to {string}',
25+
async function (this: BundleServerWorld, removeCommits: number, addCommits: number, branch: string) {
26+
const clonedRepo = this.getRepoAtBranch(User.Another, branch)
27+
28+
// First, reset
29+
utils.assertStatus(0, clonedRepo.runGit("reset", "--hard", `HEAD~${removeCommits}`))
30+
31+
// Then, add new commits
32+
for (let i = 0; i < addCommits; i++) {
33+
utils.assertStatus(0, clonedRepo.runShell(`echo ${randomBytes(16).toString('hex')} >README.md`))
34+
utils.assertStatus(0, clonedRepo.runGit("add", "README.md"))
35+
utils.assertStatus(0, clonedRepo.runGit("commit", "-m", `test ${i + 1}`))
36+
}
37+
38+
// Finally, force push
39+
utils.assertStatus(0, clonedRepo.runGit("push", "-f", "origin", branch))
40+
}
41+
)
42+
43+
Given('I cloned from the remote repo with a bundle URI', async function (this: BundleServerWorld) {
44+
const user = User.Me
45+
this.cloneRepositoryFor(user, this.bundleServer.bundleUri())
46+
utils.assertStatus(0, this.getRepo(user).cloneResult)
47+
})
48+
1249
When('I clone from the remote repo with a bundle URI', async function (this: BundleServerWorld) {
1350
this.cloneRepositoryFor(User.Me, this.bundleServer.bundleUri())
1451
})
1552

53+
When('I fetch from the remote', async function (this: BundleServerWorld) {
54+
const clonedRepo = this.getRepo(User.Me)
55+
utils.assertStatus(0, clonedRepo.runGit("fetch", "origin"))
56+
})
57+
1658
Then('bundles are downloaded and used', async function (this: BundleServerWorld) {
1759
const clonedRepo = this.getRepo(User.Me)
1860

@@ -40,3 +82,28 @@ Then('bundles are downloaded and used', async function (this: BundleServerWorld)
4082
})
4183
assert.strict(bundleRefs.length > 0, "No bundle refs found in the repo")
4284
})
85+
86+
Then('I am up-to-date with {string}', async function (this: BundleServerWorld, branch: string) {
87+
const clonedRepo = this.getRepo(User.Me)
88+
const result = clonedRepo.runGit("rev-parse", `refs/remotes/origin/${branch}`)
89+
utils.assertStatus(0, result)
90+
const actualOid = result.stdout.toString().trim()
91+
const expectedOid = this.remote?.getBranchTipOid(branch)
92+
assert.strictEqual(actualOid, expectedOid, `branch '${branch}' is not up-to-date`)
93+
})
94+
95+
Then('my repo\'s bundles {boolean} up-to-date with {string}',
96+
async function (this: BundleServerWorld, expectedUpToDate: boolean, branch: string) {
97+
const clonedRepo = this.getRepo(User.Me)
98+
const result = clonedRepo.runGit("rev-parse", `refs/bundles/${branch}`)
99+
utils.assertStatus(0, result)
100+
const actualOid = result.stdout.toString().trim()
101+
const expectedOid = this.remote?.getBranchTipOid(branch)
102+
103+
if (expectedUpToDate) {
104+
assert.strictEqual(actualOid, expectedOid, `bundle ref for '${branch}' is not up-to-date`)
105+
} else {
106+
assert.notStrictEqual(actualOid, expectedOid, `bundle ref for '${branch}' is up-to-date, but should not be`)
107+
}
108+
}
109+
)
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/world.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { setWorldConstructor, World, IWorldOptions } from '@cucumber/cucumber'
2+
import { randomUUID } from 'crypto'
23
import { RemoteRepo } from '../classes/remote'
34
import * as utils from './utils'
45
import * as fs from 'fs'
@@ -8,6 +9,7 @@ import { BundleServer } from '../classes/bundleServer'
89

910
export enum User {
1011
Me = 1,
12+
Another,
1113
}
1214

1315
interface BundleServerParameters {
@@ -59,6 +61,28 @@ export class BundleServerWorld extends World<BundleServerParameters> {
5961
return repo
6062
}
6163

64+
getRepoAtBranch(user: User, branch: string): ClonedRepository {
65+
if (this.remote && !this.remote.isLocal) {
66+
throw new Error("Remote is not initialized or does not allow pushes")
67+
}
68+
69+
if (!this.repoMap.has(user)) {
70+
this.cloneRepositoryFor(user)
71+
utils.assertStatus(0, this.getRepo(user).cloneResult)
72+
}
73+
74+
const clonedRepo = this.getRepo(user)
75+
76+
const result = clonedRepo.runGit("rev-list", "--all", "-n", "1")
77+
if (result.stdout.toString().trim() == "") {
78+
// Repo is empty, so make sure we're on the right branch
79+
utils.assertStatus(0, clonedRepo.runGit("branch", "-m", branch))
80+
} else {
81+
utils.assertStatus(0, clonedRepo.runGit("switch", branch))
82+
utils.assertStatus(0, clonedRepo.runGit("pull", "origin", branch))
83+
}
84+
85+
return clonedRepo
6286
}
6387

6488
cleanup(): void {

0 commit comments

Comments
 (0)