Skip to content

Commit 7ea9882

Browse files
authored
Merge pull request #33 from github/vdye/e2e-test-ts
Add end-to-end tests
2 parents 3c0554e + f0b17de commit 7ea9882

25 files changed

+2024
-13
lines changed

.github/workflows/main.yml

+5-5
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,15 @@ jobs:
77
runs-on: ubuntu-latest
88

99
steps:
10-
- name: Setup Go
11-
uses: actions/setup-go@v3
12-
with:
13-
go-version: '1.19.0'
14-
1510
- uses: actions/checkout@v3
1611
with:
1712
path: src/git-bundle-server
1813

14+
- name: Setup Go
15+
uses: actions/setup-go@v3
16+
with:
17+
go-version-file: 'src/git-bundle-server/go.mod'
18+
1919
# TODO: when the '-C' option is available, remove the 'cd ...'
2020
# See https://github.com/golang/go/issues/50332 for more details
2121
- name: Build

.github/workflows/release.yml

+3-3
Original file line numberDiff line numberDiff line change
@@ -46,16 +46,16 @@ jobs:
4646
environment: ${{matrix.jobs.environment}}
4747
runs-on: ${{matrix.jobs.pool}}
4848
steps:
49+
- name: Clone repository
50+
uses: actions/checkout@v3
4951
- name: Setup Go
5052
uses: actions/setup-go@v3
5153
with:
52-
go-version: '1.19.0'
54+
go-version-file: 'go.mod'
5355
- uses: ruby/setup-ruby@v1
5456
with:
5557
ruby-version: '3.2.1'
5658
- run: gem install asciidoctor
57-
- name: Clone repository
58-
uses: actions/checkout@v3
5959
- name: Configure MacOS signing
6060
if: ${{ matrix.jobs.pool == 'macos-latest' }}
6161
env:

.github/workflows/test.yml

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
name: End-to-end test
2+
3+
on:
4+
pull_request:
5+
workflow_dispatch:
6+
inputs:
7+
run-all:
8+
type: boolean
9+
description: 'Include tests that are excluded by default due to slowness'
10+
default: false
11+
12+
jobs:
13+
e2e-test:
14+
runs-on: ubuntu-latest
15+
16+
steps:
17+
- name: Clone repository
18+
uses: actions/checkout@v3
19+
20+
- name: Setup Go
21+
uses: actions/setup-go@v3
22+
with:
23+
go-version-file: 'go.mod'
24+
25+
- name: Setup Node.js
26+
uses: actions/setup-node@v3
27+
with:
28+
node-version: 18
29+
30+
- name: Install system dependencies for end-to-end tests
31+
run: build/ci/install-dependencies.sh
32+
shell: bash
33+
34+
- name: Enable perf tests
35+
if: ${{ github.event.inputs.run-all }}
36+
run: echo "E2E_FLAGS='--all'" >> $GITHUB_ENV
37+
38+
- name: Run end-to-end tests
39+
env:
40+
E2E_FLAGS: ${{env.E2E_FLAGS}}
41+
run: make e2e-test E2E_FLAGS="$E2E_FLAGS"

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,5 @@
33
/bin/
44
/_dist/
55
/_docs/
6+
/_test/
7+
node_modules/

.vscode/settings.json

+10-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
{
2+
// File formatting
23
"[markdown]": {
34
"editor.detectIndentation": false,
45
"editor.insertSpaces": false,
@@ -35,5 +36,13 @@
3536
"prerm": "shellscript",
3637
"postinstall": "shellscript",
3738
"Makefile": "makefile"
38-
}
39+
},
40+
41+
// Cucumber settings
42+
"cucumber.features": [
43+
"test/e2e/features/**/*.feature",
44+
],
45+
"cucumber.glue": [
46+
"test/e2e/features/**/*.ts"
47+
],
3948
}

Makefile

+11
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ INSTALL_ROOT := /
1313
BINDIR := $(CURDIR)/bin
1414
DISTDIR := $(CURDIR)/_dist
1515
DOCDIR := $(CURDIR)/_docs
16+
TESTDIR := $(CURDIR)/_test
1617

1718
# Platform information
1819
GOOS := $(shell go env GOOS)
@@ -26,6 +27,7 @@ PACKAGE_ARCH := $(GOARCH)
2627
APPLE_APP_IDENTITY =
2728
APPLE_INST_IDENTITY =
2829
APPLE_KEYCHAIN_PROFILE =
30+
E2E_FLAGS=
2931

3032
# Build targets
3133
.PHONY: build
@@ -39,6 +41,14 @@ doc:
3941
@scripts/make-docs.sh --docs="$(CURDIR)/docs/man" \
4042
--output="$(DOCDIR)"
4143

44+
# Testing targets
45+
.PHONY: e2e-test
46+
e2e-test: build
47+
@echo
48+
@echo "======== Running end-to-end tests ========"
49+
$(RM) -r $(TESTDIR)
50+
@scripts/run-e2e-tests.sh $(E2E_FLAGS)
51+
4252
# Installation targets
4353
.PHONY: install
4454
install: build doc
@@ -170,3 +180,4 @@ clean:
170180
$(RM) -r $(BINDIR)
171181
$(RM) -r $(DISTDIR)
172182
$(RM) -r $(DOCDIR)
183+
$(RM) -r $(TESTDIR)

README.md

+38-4
Original file line numberDiff line numberDiff line change
@@ -170,11 +170,45 @@ $ go build -o bin/ ./...
170170

171171
### Testing and Linting
172172

173-
To run the project's unit tests, navigate to the repository root directory and
174-
run `go test -v ./...`.
173+
Unless otherwise specified, run commands from the repository root.
175174

176-
To run the project's linter, navigate to the repository root directory and run
177-
`go vet ./...`.
175+
#### Unit tests
176+
177+
```
178+
go test -v ./...
179+
```
180+
181+
#### Linter
182+
183+
```
184+
go vet ./...
185+
```
186+
187+
#### End-to-end tests
188+
189+
In order to run these tests, you need to have a recent version of
190+
[Node.js](https://nodejs.org) (current LTS version is a pretty safe bet) and NPM
191+
installed.
192+
193+
For the standard set of tests (i.e., excluding exceptionally slow tests), run:
194+
195+
```
196+
make e2e-test
197+
```
198+
199+
To configure the test execution and filtering, set the `E2E_FLAGS` build
200+
variable. The available options are:
201+
202+
* `--offline`: run all tests except those that require internet access.
203+
* `--all`: run all tests, including slow performance tests.
204+
205+
The above modes are mutually exclusive; if multiple are specified, only the last
206+
will be used. For example, `E2E_FLAGS="--offline --all"` is equivalent to
207+
`E2E_FLAGS="--all"`.
208+
209+
:warning: The performance tests that are excluded by default clone very large
210+
repos from the internet and can take anywhere from ~30 minutes to multiple hours
211+
to run, depending on internet connectivity and other system resources.
178212

179213
## License
180214

build/ci/install-dependencies.sh

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#!/bin/bash
2+
die () {
3+
echo "$*" >&2
4+
exit 1
5+
}
6+
7+
# Exit as soon as any line fails
8+
set -e
9+
10+
# Install latest version of Git (minimum v2.40)
11+
if command -v apt >/dev/null 2>&1; then
12+
sudo add-apt-repository ppa:git-core/ppa
13+
sudo apt update
14+
sudo apt -q -y install git
15+
elif command -v brew >/dev/null 2>&1; then
16+
brew install git
17+
else
18+
die 'Cannot install git'
19+
fi
20+
21+
# Set up test Git config
22+
git config --global user.name "GitHub Action"
23+
git config --global user.email "[email protected]"

scripts/run-e2e-tests.sh

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
#!/bin/bash
2+
3+
THISDIR="$( cd "$(dirname "$0")" ; pwd -P )"
4+
TESTDIR="$THISDIR/../test/e2e"
5+
6+
# Defaults
7+
ARGS=()
8+
9+
# Parse script arguments
10+
for i in "$@"
11+
do
12+
case "$i" in
13+
--offline)
14+
ARGS+=("-p" "offline")
15+
shift # past argument
16+
;;
17+
--all)
18+
ARGS+=("-p" "all")
19+
shift # past argument
20+
;;
21+
*)
22+
die "unknown option '$i'"
23+
;;
24+
esac
25+
done
26+
27+
# Exit as soon as any line fails
28+
set -e
29+
30+
cd "$TESTDIR"
31+
32+
npm install
33+
npm run test -- ${ARGS:+${ARGS[*]}}

test/e2e/cucumber.js

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
const common = {
2+
requireModule: ['ts-node/register'],
3+
require: ['features/**/*.ts'],
4+
publishQuiet: true,
5+
format: ['progress'],
6+
formatOptions: {
7+
snippetInterface: 'async-await'
8+
},
9+
worldParameters: {
10+
bundleServerCommand: '../../bin/git-bundle-server',
11+
bundleWebServerCommand: '../../bin/git-bundle-web-server',
12+
trashDirectoryBase: '../../_test/e2e'
13+
}
14+
}
15+
16+
module.exports = {
17+
default: {
18+
...common,
19+
tags: 'not @slow',
20+
},
21+
offline: {
22+
...common,
23+
tags: 'not @online',
24+
},
25+
all: {
26+
...common
27+
}
28+
}

test/e2e/features/basic.feature

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
Feature: Basic bundle server usage
2+
3+
Background: The bundle web server is running
4+
Given the bundle web server was started at port 8080
5+
6+
@online
7+
Scenario: A user can clone with a bundle URI pointing to the bundle server
8+
Given a remote repository 'https://github.com/vdye/asset-hash.git'
9+
Given the bundle server has been initialized with the remote repo
10+
When I clone from the remote repo with a bundle URI
11+
Then bundles are downloaded and used
12+
13+
Scenario: A user can fetch with a bundle server that's behind and get all updates
14+
Given a new remote repository with main branch 'main'
15+
Given another user pushed 10 commits to 'main'
16+
Given the bundle server has been initialized with the remote repo
17+
Given I cloned from the remote repo with a bundle URI
18+
Given another user pushed 2 commits to 'main'
19+
When I fetch from the remote
20+
Then I am up-to-date with 'main'
21+
Then my repo's bundles are not up-to-date with 'main'
22+
23+
Scenario: A user will fetch incremental bundles to stay up-to-date
24+
Given a new remote repository with main branch 'main'
25+
Given another user pushed 10 commits to 'main'
26+
Given the bundle server has been initialized with the remote repo
27+
Given I cloned from the remote repo with a bundle URI
28+
Given another user pushed 2 commits to 'main'
29+
Given the bundle server was updated for the remote repo
30+
When I fetch from the remote
31+
Then I am up-to-date with 'main'
32+
Then my repo's bundles are up-to-date with 'main'
33+
34+
Scenario: A user can fetch force-pushed refs from the bundle server
35+
Given a new remote repository with main branch 'main'
36+
Given another user pushed 10 commits to 'main'
37+
Given the bundle server has been initialized with the remote repo
38+
Given I cloned from the remote repo with a bundle URI
39+
Given another user removed 2 commits and added 4 commits to 'main'
40+
Given the bundle server was updated for the remote repo
41+
When I fetch from the remote
42+
Then I am up-to-date with 'main'
43+
Then my repo's bundles are up-to-date with 'main'
+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
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) {
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+
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+
41+
bundleUri(): string {
42+
if (!this.webServerProcess) {
43+
throw new Error("Tried to get bundle URI before starting the web server")
44+
}
45+
if (!this.route) {
46+
throw new Error("Tried to get bundle URI before running 'init'")
47+
}
48+
49+
return this.bundleUriBase + this.route
50+
}
51+
52+
cleanup(): void {
53+
if (this.webServerProcess) {
54+
const killed = this.webServerProcess.kill('SIGINT')
55+
if (!killed) {
56+
console.warn("Web server process was not successfully stopped")
57+
}
58+
}
59+
60+
// Delete the added route
61+
if (this.route) {
62+
child_process.spawnSync(this.bundleServerCmd, ["delete", this.route])
63+
}
64+
}
65+
}

0 commit comments

Comments
 (0)