Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add end-to-end tests #33

Merged
merged 9 commits into from
Mar 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@ jobs:
runs-on: ubuntu-latest

steps:
- name: Setup Go
uses: actions/setup-go@v3
with:
go-version: '1.19.0'

- uses: actions/checkout@v3
with:
path: src/git-bundle-server

- name: Setup Go
uses: actions/setup-go@v3
with:
go-version-file: 'src/git-bundle-server/go.mod'

# TODO: when the '-C' option is available, remove the 'cd ...'
# See https://github.com/golang/go/issues/50332 for more details
- name: Build
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,16 +46,16 @@ jobs:
environment: ${{matrix.jobs.environment}}
runs-on: ${{matrix.jobs.pool}}
steps:
- name: Clone repository
uses: actions/checkout@v3
- name: Setup Go
uses: actions/setup-go@v3
with:
go-version: '1.19.0'
go-version-file: 'go.mod'
- uses: ruby/setup-ruby@v1
with:
ruby-version: '3.2.1'
- run: gem install asciidoctor
- name: Clone repository
uses: actions/checkout@v3
- name: Configure MacOS signing
if: ${{ matrix.jobs.pool == 'macos-latest' }}
env:
Expand Down
41 changes: 41 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: End-to-end test

on:
pull_request:
workflow_dispatch:
Comment on lines +3 to +5
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As you mentioned, "only on PRs" is quite nice because that way it's not on arbitrary pushes and not re-testing after a PR merged (and was subject to required builds).

The manual trigger of the workflow (with optional perf tests!) is excellent.

inputs:
run-all:
type: boolean
description: 'Include tests that are excluded by default due to slowness'
default: false

jobs:
e2e-test:
runs-on: ubuntu-latest

steps:
- name: Clone repository
uses: actions/checkout@v3

- name: Setup Go
uses: actions/setup-go@v3
with:
go-version-file: 'go.mod'

- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: 18

- name: Install system dependencies for end-to-end tests
run: build/ci/install-dependencies.sh
shell: bash

- name: Enable perf tests
if: ${{ github.event.inputs.run-all }}
run: echo "E2E_FLAGS='--all'" >> $GITHUB_ENV

- name: Run end-to-end tests
env:
E2E_FLAGS: ${{env.E2E_FLAGS}}
run: make e2e-test E2E_FLAGS="$E2E_FLAGS"
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@
/bin/
/_dist/
/_docs/
/_test/
node_modules/
11 changes: 10 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
// File formatting
"[markdown]": {
"editor.detectIndentation": false,
"editor.insertSpaces": false,
Expand Down Expand Up @@ -35,5 +36,13 @@
"prerm": "shellscript",
"postinstall": "shellscript",
"Makefile": "makefile"
}
},

// Cucumber settings
"cucumber.features": [
"test/e2e/features/**/*.feature",
],
"cucumber.glue": [
"test/e2e/features/**/*.ts"
],
}
11 changes: 11 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ INSTALL_ROOT := /
BINDIR := $(CURDIR)/bin
DISTDIR := $(CURDIR)/_dist
DOCDIR := $(CURDIR)/_docs
TESTDIR := $(CURDIR)/_test

# Platform information
GOOS := $(shell go env GOOS)
Expand All @@ -26,6 +27,7 @@ PACKAGE_ARCH := $(GOARCH)
APPLE_APP_IDENTITY =
APPLE_INST_IDENTITY =
APPLE_KEYCHAIN_PROFILE =
E2E_FLAGS=

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

# Testing targets
.PHONY: e2e-test
e2e-test: build
@echo
@echo "======== Running end-to-end tests ========"
$(RM) -r $(TESTDIR)
@scripts/run-e2e-tests.sh $(E2E_FLAGS)

# Installation targets
.PHONY: install
install: build doc
Expand Down Expand Up @@ -170,3 +180,4 @@ clean:
$(RM) -r $(BINDIR)
$(RM) -r $(DISTDIR)
$(RM) -r $(DOCDIR)
$(RM) -r $(TESTDIR)
42 changes: 38 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,11 +170,45 @@ $ go build -o bin/ ./...

### Testing and Linting

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

To run the project's linter, navigate to the repository root directory and run
`go vet ./...`.
#### Unit tests

```
go test -v ./...
```

#### Linter

```
go vet ./...
```

#### End-to-end tests

In order to run these tests, you need to have a recent version of
[Node.js](https://nodejs.org) (current LTS version is a pretty safe bet) and NPM
installed.

For the standard set of tests (i.e., excluding exceptionally slow tests), run:

```
make e2e-test
```

To configure the test execution and filtering, set the `E2E_FLAGS` build
variable. The available options are:

* `--offline`: run all tests except those that require internet access.
* `--all`: run all tests, including slow performance tests.

The above modes are mutually exclusive; if multiple are specified, only the last
will be used. For example, `E2E_FLAGS="--offline --all"` is equivalent to
`E2E_FLAGS="--all"`.

:warning: The performance tests that are excluded by default clone very large
repos from the internet and can take anywhere from ~30 minutes to multiple hours
to run, depending on internet connectivity and other system resources.

## License

Expand Down
23 changes: 23 additions & 0 deletions build/ci/install-dependencies.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#!/bin/bash
die () {
echo "$*" >&2
exit 1
}

# Exit as soon as any line fails
set -e

# Install latest version of Git (minimum v2.40)
if command -v apt >/dev/null 2>&1; then
sudo add-apt-repository ppa:git-core/ppa
sudo apt update
sudo apt -q -y install git
elif command -v brew >/dev/null 2>&1; then
brew install git
else
die 'Cannot install git'
fi

# Set up test Git config
git config --global user.name "GitHub Action"
git config --global user.email "[email protected]"
33 changes: 33 additions & 0 deletions scripts/run-e2e-tests.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#!/bin/bash

THISDIR="$( cd "$(dirname "$0")" ; pwd -P )"
TESTDIR="$THISDIR/../test/e2e"

# Defaults
ARGS=()

# Parse script arguments
for i in "$@"
do
case "$i" in
--offline)
ARGS+=("-p" "offline")
shift # past argument
;;
--all)
ARGS+=("-p" "all")
shift # past argument
;;
*)
die "unknown option '$i'"
;;
esac
done

# Exit as soon as any line fails
set -e

cd "$TESTDIR"

npm install
npm run test -- ${ARGS:+${ARGS[*]}}
28 changes: 28 additions & 0 deletions test/e2e/cucumber.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
const common = {
requireModule: ['ts-node/register'],
require: ['features/**/*.ts'],
publishQuiet: true,
format: ['progress'],
formatOptions: {
snippetInterface: 'async-await'
},
worldParameters: {
bundleServerCommand: '../../bin/git-bundle-server',
bundleWebServerCommand: '../../bin/git-bundle-web-server',
trashDirectoryBase: '../../_test/e2e'
}
}

module.exports = {
default: {
...common,
tags: 'not @slow',
},
offline: {
...common,
tags: 'not @online',
},
all: {
...common
}
}
43 changes: 43 additions & 0 deletions test/e2e/features/basic.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
Feature: Basic bundle server usage

Background: The bundle web server is running
Given the bundle web server was started at port 8080

@online
Scenario: A user can clone with a bundle URI pointing to the bundle server
Given a remote repository 'https://github.com/vdye/asset-hash.git'
Given the bundle server has been initialized with the remote repo
When I clone from the remote repo with a bundle URI
Then bundles are downloaded and used
Comment on lines +7 to +11
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an interesting formal mechanism for describing tests. Each of these lines are repeatable steps that can be mixed and matched. For instance, I could imagine (not looking ahead) a test like:

Scenario: User fails to clone from a non-existing bundle URI
    Given a remote repository 'https://github.com/vdye/asset-hash.git'
    When I clone from the remote repo with a bundle URI
    Then bundles are not downloaded and clone succeeds with a warning

The only new step to build is the Then statement. Neat!


Scenario: A user can fetch with a bundle server that's behind and get all updates
Given a new remote repository with main branch 'main'
Given another user pushed 10 commits to 'main'
Given the bundle server has been initialized with the remote repo
Given I cloned from the remote repo with a bundle URI
Given another user pushed 2 commits to 'main'
When I fetch from the remote
Then I am up-to-date with 'main'
Then my repo's bundles are not up-to-date with 'main'

Scenario: A user will fetch incremental bundles to stay up-to-date
Given a new remote repository with main branch 'main'
Given another user pushed 10 commits to 'main'
Given the bundle server has been initialized with the remote repo
Given I cloned from the remote repo with a bundle URI
Given another user pushed 2 commits to 'main'
Given the bundle server was updated for the remote repo
When I fetch from the remote
Then I am up-to-date with 'main'
Then my repo's bundles are up-to-date with 'main'

Scenario: A user can fetch force-pushed refs from the bundle server
Given a new remote repository with main branch 'main'
Given another user pushed 10 commits to 'main'
Given the bundle server has been initialized with the remote repo
Given I cloned from the remote repo with a bundle URI
Given another user removed 2 commits and added 4 commits to 'main'
Given the bundle server was updated for the remote repo
When I fetch from the remote
Then I am up-to-date with 'main'
Then my repo's bundles are up-to-date with 'main'
Comment on lines +13 to +43
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😍 this is amazing to read and clearly understand the scenarios.

65 changes: 65 additions & 0 deletions test/e2e/features/classes/bundleServer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { randomBytes } from 'crypto'
import * as child_process from 'child_process'
import { RemoteRepo } from './remote'

export class BundleServer {
private bundleServerCmd: string
private bundleWebServerCmd: string

// Web server
private webServerProcess: child_process.ChildProcess | undefined
private bundleUriBase: string | undefined

// Remote repo info (for now, only support one per test)
private route: string | undefined

constructor(bundleServerCmd: string, bundleWebServerCmd: string) {
this.bundleServerCmd = bundleServerCmd
this.bundleWebServerCmd = bundleWebServerCmd
}

startWebServer(port: number): void {
if (this.webServerProcess) {
throw new Error("Tried to start web server, but web server is already running")
}
this.webServerProcess = child_process.spawn(this.bundleWebServerCmd, ["--port", String(port)])
this.bundleUriBase = `http://localhost:${port}/`
}

init(remote: RemoteRepo): child_process.SpawnSyncReturns<Buffer> {
this.route = `e2e/${randomBytes(8).toString('hex')}`
return child_process.spawnSync(this.bundleServerCmd, ["init", remote.remoteUri, this.route])
}

update(): child_process.SpawnSyncReturns<Buffer> {
if (!this.route) {
throw new Error("Tried to update server before running 'init'")
}
return child_process.spawnSync(this.bundleServerCmd, ["update", this.route])
}

bundleUri(): string {
if (!this.webServerProcess) {
throw new Error("Tried to get bundle URI before starting the web server")
}
if (!this.route) {
throw new Error("Tried to get bundle URI before running 'init'")
}

return this.bundleUriBase + this.route
}

cleanup(): void {
if (this.webServerProcess) {
const killed = this.webServerProcess.kill('SIGINT')
if (!killed) {
console.warn("Web server process was not successfully stopped")
}
}

// Delete the added route
if (this.route) {
child_process.spawnSync(this.bundleServerCmd, ["delete", this.route])
}
}
}
Loading