Skip to content

Commit 238be21

Browse files
Add GUI tests
1 parent 5665b99 commit 238be21

File tree

7 files changed

+377
-0
lines changed

7 files changed

+377
-0
lines changed

.github/workflows/ci.yml

+8
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,14 @@ jobs:
7777
- name: Clean up the database
7878
run: docker-compose down --volumes
7979

80+
gui_tests:
81+
runs-on: ubuntu-latest
82+
needs: build
83+
84+
steps:
85+
- name: Run GUI tests
86+
run: ./dockerfiles/run-gui-tests.sh
87+
8088
build_tests:
8189
runs-on: ubuntu-latest
8290
needs: build

docker-compose.yml

+9
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,15 @@ services:
8989
timeout: 5s
9090
retries: 10
9191

92+
gui_tests:
93+
build:
94+
context: .
95+
dockerfile: ./dockerfiles/Dockerfile-gui-tests
96+
depends_on:
97+
- web
98+
volumes:
99+
- "${PWD}:/build/out"
100+
92101
volumes:
93102
postgres-data: {}
94103
minio-data: {}

dockerfiles/Dockerfile-gui-tests

+80
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
FROM ubuntu:16.04 as build
2+
SHELL ["/bin/bash", "-c"]
3+
4+
RUN apt-get update && apt-get install -y \
5+
ca-certificates \
6+
curl \
7+
docker.io \
8+
gcc \
9+
git \
10+
libssl-dev \
11+
pkg-config \
12+
xz-utils
13+
14+
# Install dependencies for chromium browser
15+
RUN apt-get install -y \
16+
gconf-service \
17+
libasound2 \
18+
libatk1.0-0 \
19+
libatk-bridge2.0-0 \
20+
libc6 \
21+
libcairo2 \
22+
libcups2 \
23+
libdbus-1-3 \
24+
libexpat1 \
25+
libfontconfig1 \
26+
libgbm-dev \
27+
libgcc1 \
28+
libgconf-2-4 \
29+
libgdk-pixbuf2.0-0 \
30+
libglib2.0-0 \
31+
libgtk-3-0 \
32+
libnspr4 \
33+
libpango-1.0-0 \
34+
libpangocairo-1.0-0 \
35+
libstdc++6 \
36+
libx11-6 \
37+
libx11-xcb1 \
38+
libxcb1 \
39+
libxcomposite1 \
40+
libxcursor1 \
41+
libxdamage1 \
42+
libxext6 \
43+
libxfixes3 \
44+
libxi6 \
45+
libxrandr2 \
46+
libxrender1 \
47+
libxss1 \
48+
libxtst6 \
49+
fonts-liberation \
50+
libappindicator1 \
51+
libnss3 \
52+
lsb-release \
53+
xdg-utils \
54+
wget
55+
56+
# Install rust
57+
RUN curl https://sh.rustup.rs -sSf | bash -s -- -y --default-toolchain nightly --no-modify-path --profile minimal
58+
ENV PATH="/root/.cargo/bin:${PATH}"
59+
60+
RUN curl -sL https://nodejs.org/dist/v14.4.0/node-v14.4.0-linux-x64.tar.xz | tar -xJ
61+
ENV PATH="/node-v14.4.0-linux-x64/bin:${PATH}"
62+
ENV NODE_PATH="/node-v14.4.0-linux-x64/lib/node_modules/"
63+
64+
WORKDIR /build
65+
66+
COPY .env.sample .env
67+
68+
RUN source .env
69+
RUN mkdir out
70+
71+
# For now, we need to use `--unsafe-perm=true` to go around an issue when npm tries
72+
# to create a new folder. For reference:
73+
# https://github.com/puppeteer/puppeteer/issues/375
74+
#
75+
# We also specify the version in case we need to update it to go around cache limitations.
76+
RUN npm install -g [email protected] --unsafe-perm=true
77+
78+
EXPOSE 3000
79+
80+
CMD ["node", "/build/out/gui-tests/tester.js"]

dockerfiles/run-gui-tests.sh

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
#!/usr/bin/env bash
2+
3+
# Just in case, we shut stop the web server.
4+
docker-compose stop web
5+
6+
# From this point, any error is fatal
7+
set -e
8+
9+
docker-compose up -d db s3
10+
11+
# We add the information we need.
12+
cargo run -- database migrate
13+
cargo run -- build crate sysinfo 0.23.4
14+
cargo run -- build crate sysinfo 0.23.5
15+
cargo run -- build add-essential-files
16+
17+
docker-compose run gui_tests

gui-tests/404.goml

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
// Checks the content of the 404 page.
2+
goto: |DOC_PATH|/non-existing-crate
3+
assert-text: ("#crate-title", "The requested crate does not exist")

gui-tests/basic.goml

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Checks that the "latest" URL leads us to the last version of the `sysinfo` crate.
2+
goto: |DOC_PATH|/sysinfo
3+
// We first check if the redirection worked as expected:
4+
assert-document-property: ({"URL": "/sysinfo/latest/sysinfo/"}, ENDS_WITH)
5+
assert: "//*[@class='title' and text()='sysinfo-0.23.5']"
6+
// And we also confirm we're on a rustdoc page.
7+
assert: "#rustdoc_body_wrapper"
8+
9+
// Let's go to the docs.rs page of the crate.
10+
goto: |DOC_PATH|/crate/sysinfo/latest
11+
assert-false: "#rustdoc_body_wrapper"
12+
assert-text: ("#crate-title", "sysinfo 0.23.5", CONTAINS)

gui-tests/tester.js

+248
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
// This package needs to be install:
2+
//
3+
// ```
4+
// npm install browser-ui-test
5+
// ```
6+
7+
const fs = require("fs");
8+
const path = require("path");
9+
const os = require('os');
10+
const {Options, runTest} = require('browser-ui-test');
11+
12+
function showHelp() {
13+
console.log("docs-rs-gui-js options:");
14+
console.log(" --file [PATH] : file to run (can be repeated)");
15+
console.log(" --debug : show extra information about script run");
16+
console.log(" --show-text : render font in pages");
17+
console.log(" --no-headless : disable headless mode");
18+
console.log(" --help : show this message then quit");
19+
console.log(" --jobs [NUMBER] : number of threads to run tests on");
20+
}
21+
22+
function isNumeric(s) {
23+
return /^\d+$/.test(s);
24+
}
25+
26+
function parseOptions(args) {
27+
var opts = {
28+
"files": [],
29+
"debug": false,
30+
"show_text": false,
31+
"no_headless": false,
32+
"jobs": -1,
33+
};
34+
var correspondances = {
35+
"--debug": "debug",
36+
"--show-text": "show_text",
37+
"--no-headless": "no_headless",
38+
};
39+
40+
for (var i = 0; i < args.length; ++i) {
41+
if (args[i] === "--file"
42+
|| args[i] === "--jobs") {
43+
i += 1;
44+
if (i >= args.length) {
45+
console.log("Missing argument after `" + args[i - 1] + "` option.");
46+
return null;
47+
}
48+
if (args[i - 1] === "--jobs") {
49+
if (!isNumeric(args[i])) {
50+
console.log(
51+
"`--jobs` option expects a positive number, found `" + args[i] + "`");
52+
return null;
53+
}
54+
opts["jobs"] = parseInt(args[i]);
55+
} else if (args[i - 1] !== "--file") {
56+
opts[correspondances[args[i - 1]]] = args[i];
57+
} else {
58+
opts["files"].push(args[i]);
59+
}
60+
} else if (args[i] === "--help") {
61+
showHelp();
62+
process.exit(0);
63+
} else if (correspondances[args[i]]) {
64+
opts[correspondances[args[i]]] = true;
65+
} else {
66+
console.log("Unknown option `" + args[i] + "`.");
67+
console.log("Use `--help` to see the list of options");
68+
return null;
69+
}
70+
}
71+
return opts;
72+
}
73+
74+
/// Print single char status information without \n
75+
function char_printer(n_tests) {
76+
const max_per_line = 10;
77+
let current = 0;
78+
return {
79+
successful: function() {
80+
current += 1;
81+
if (current % max_per_line === 0) {
82+
process.stdout.write(`. (${current}/${n_tests})${os.EOL}`);
83+
} else {
84+
process.stdout.write(".");
85+
}
86+
},
87+
erroneous: function() {
88+
current += 1;
89+
if (current % max_per_line === 0) {
90+
process.stderr.write(`F (${current}/${n_tests})${os.EOL}`);
91+
} else {
92+
process.stderr.write("F");
93+
}
94+
},
95+
finish: function() {
96+
if (current % max_per_line === 0) {
97+
// Don't output if we are already at a matching line end
98+
console.log("");
99+
} else {
100+
const spaces = " ".repeat(max_per_line - (current % max_per_line));
101+
process.stdout.write(`${spaces} (${current}/${n_tests})${os.EOL}${os.EOL}`);
102+
}
103+
},
104+
};
105+
}
106+
107+
/// Sort array by .file_name property
108+
function by_filename(a, b) {
109+
return a.file_name - b.file_name;
110+
}
111+
112+
async function main(argv) {
113+
let opts = parseOptions(argv.slice(2));
114+
if (opts === null) {
115+
process.exit(1);
116+
}
117+
118+
// Print successful tests too
119+
let debug = false;
120+
// Run tests in sequentially
121+
let headless = true;
122+
const options = new Options();
123+
try {
124+
// This is more convenient that setting fields one by one.
125+
let args = [
126+
"--no-screenshot-comparison",
127+
"--no-sandbox",
128+
"--variable", "DOC_PATH", "http://127.0.0.1:3000",
129+
];
130+
if (opts["debug"]) {
131+
debug = true;
132+
args.push("--debug");
133+
}
134+
if (opts["show_text"]) {
135+
args.push("--show-text");
136+
}
137+
if (opts["no_headless"]) {
138+
args.push("--no-headless");
139+
headless = false;
140+
}
141+
options.parseArguments(args);
142+
} catch (error) {
143+
console.error(`invalid argument: ${error}`);
144+
process.exit(1);
145+
}
146+
147+
let failed = false;
148+
let files;
149+
if (opts["files"].length === 0) {
150+
files = fs.readdirSync(__dirname);
151+
} else {
152+
files = opts["files"];
153+
}
154+
files = files.filter(file => path.extname(file) == ".goml");
155+
if (files.length === 0) {
156+
console.error("No test selected");
157+
process.exit(2);
158+
}
159+
files.sort();
160+
161+
if (!headless) {
162+
opts["jobs"] = 1;
163+
console.log("`--no-headless` option is active, disabling concurrency for running tests.");
164+
}
165+
166+
console.log(`Running ${files.length} docs.rs GUI (${opts["jobs"]} concurrently) ...`);
167+
168+
if (opts["jobs"] < 1) {
169+
process.setMaxListeners(files.length + 1);
170+
} else if (headless) {
171+
process.setMaxListeners(opts["jobs"] + 1);
172+
}
173+
174+
const tests_queue = [];
175+
let results = {
176+
successful: [],
177+
failed: [],
178+
errored: [],
179+
};
180+
const status_bar = char_printer(files.length);
181+
for (let i = 0; i < files.length; ++i) {
182+
const file_name = files[i];
183+
const testPath = path.join(__dirname, file_name);
184+
const callback = runTest(testPath, options)
185+
.then(out => {
186+
const [output, nb_failures] = out;
187+
results[nb_failures === 0 ? "successful" : "failed"].push({
188+
file_name: testPath,
189+
output: output,
190+
});
191+
if (nb_failures > 0) {
192+
status_bar.erroneous();
193+
failed = true;
194+
} else {
195+
status_bar.successful();
196+
}
197+
})
198+
.catch(err => {
199+
results.errored.push({
200+
file_name: testPath + file_name,
201+
output: err,
202+
});
203+
status_bar.erroneous();
204+
failed = true;
205+
})
206+
.finally(() => {
207+
// We now remove the promise from the tests_queue.
208+
tests_queue.splice(tests_queue.indexOf(callback), 1);
209+
});
210+
tests_queue.push(callback);
211+
if (opts["jobs"] > 0 && tests_queue.length >= opts["jobs"]) {
212+
await Promise.race(tests_queue);
213+
}
214+
}
215+
if (tests_queue.length > 0) {
216+
await Promise.all(tests_queue);
217+
}
218+
status_bar.finish();
219+
220+
if (debug) {
221+
results.successful.sort(by_filename);
222+
results.successful.forEach(r => {
223+
console.log(r.output);
224+
});
225+
}
226+
227+
if (results.failed.length > 0) {
228+
console.log("");
229+
results.failed.sort(by_filename);
230+
results.failed.forEach(r => {
231+
console.log(r.file_name, r.output);
232+
});
233+
}
234+
if (results.errored.length > 0) {
235+
console.log(os.EOL);
236+
// print run errors on the bottom so developers see them better
237+
results.errored.sort(by_filename);
238+
results.errored.forEach(r => {
239+
console.error(r.file_name, r.output);
240+
});
241+
}
242+
243+
if (failed) {
244+
process.exit(1);
245+
}
246+
}
247+
248+
main(process.argv);

0 commit comments

Comments
 (0)