Added automated testing... and there are a lot of issues

This commit is contained in:
2025-11-18 07:21:46 +01:00
parent d72dc34556
commit 2e2bf46529
5 changed files with 354 additions and 0 deletions

20
run_test.sh Executable file
View File

@@ -0,0 +1,20 @@
#!/bin/bash
OUT=/tmp/tultemplategen_test_bundle.js
cat \
tests/init.js \
lib/jszip.min.js \
src/data/autogen.js \
src/data/typst_header.js \
src/data/l10n.js \
src/l10n.js \
src/data/steps.js \
src/output/header.js \
src/output/zip.js \
tests/headless.js \
tests/main.js \
> $OUT
node $OUT

131
tests/headless.js Normal file
View File

@@ -0,0 +1,131 @@
// Minimal implementations of layout classes for analyzing possible inputs.
class LayoutButtonList {
constructor(inputskey, options, optional_cb) {
this.options = options;
this.optional_cb = optional_cb;
this.inputskey = inputskey;
}
narrow() { return this; }
horizontal() { return this; }
*iter(inputs) {
inputs[this.inputskey] = "";
yield `${this.inputskey}: Empty string`;
for(const val in this.options) {
if(val == "dis" || val == "hab" || val == "teze" || val == "autoref" || val == "sp") continue; // complete bodge until upstream supports all thesis types
if(this.inputskey == "toolchain" && val == "online") continue; // bodge for skipping tests for archives intended for the online editor
inputs[this.inputskey] = val;
yield `${this.inputskey}: Value from options: ${val}`;
}
}
}
class LayoutTextInput {
constructor(inputskey) {
this.inputskey = inputskey;
this.togglekey = null;
}
hint() { return this; }
toggle(togglekey) {
this.togglekey = togglekey;
return this;
}
*iter(inputs) {
if(this.togglekey) {
inputs[this.togglekey] = false;
inputs[this.inputskey] = "";
yield `${this.inputskey}, ${this.togglekey}: Empty string (disabled)`;
inputs[this.inputskey] = "Skibidi";
yield `${this.inputskey}, ${this.togglekey}: Populated string (disabled)`;
inputs[this.togglekey] = true;
}
inputs[this.inputskey] = "";
yield `${this.inputskey}, ${this.togglekey}: Empty string`;
inputs[this.inputskey] = "Skibidi";
yield `${this.inputskey}, ${this.togglekey}: Populated string`;
}
}
class LayoutBinaryInput {
constructor(inputskey) {
this.inputskey = inputskey;
}
label() { return this; }
*iter(inputs) {
inputs[this.inputskey] = false;
yield `${this.inputskey}: Unchecked`;
inputs[this.inputskey] = true;
yield `${this.inputskey}: Checked`;
}
}
class LayoutFileUpload {
constructor(namekey, contentskey) {
this.namekey = namekey;
this.contentskey = contentskey;
}
magic() { return this; }
*iter(inputs) {
inputs[this.namekey] = null;
inputs[this.contentskey] = null;
yield `${this.namekey}, ${this.contentskey}: No file selected`;
inputs[this.namekey] = "test-pages.pdf"
inputs[this.contentskey] = new Uint8Array(test_pages_pdf);
yield `${this.namekey}, ${this.contentskey}: Selected example PDF file`;
}
}
class LayoutDropdownList {
constructor(inputskey, options) {
this.inputskey = inputskey;
this.options = options;
}
*iter(inputs) {
for(const val in this.options) {
inputs[this.inputskey] = val;
yield `${this.inputskey}: Value from options: ${val}`
}
}
}
class LayoutSpacer {
constructor() {}
}
class LayoutHint {
constructor() {}
}
class LayoutLabel {
constructor() {}
}
// Minimal implementations of functions just so that the generator can work.
function show_experimental_faculty_warning() {}
function fetch_cached_blob(name) {
if(name != "template.zip") throw "files other than the main assets bundle not supported";
return template_zip;
}

10
tests/init.js Normal file
View File

@@ -0,0 +1,10 @@
import fs from "fs/promises";
import { exec } from "child_process";
let config = {
lang: "cs"
};
let template_zip = await fs.readFile("template.zip");
let test_pages_pdf = await fs.readFile("tests/test-pages.pdf");

193
tests/main.js Normal file
View File

@@ -0,0 +1,193 @@
import { unzip } from "zlib";
let step_keys = Object.keys(steps);
async function finalize_step(step) {
for(const key in step.inputs) {
step.outputs[key] = step.inputs[key];
}
if(step.next) {
await step.next(step.inputs, step.outputs);
}
}
function allow_next_step(step) {
if(!step.only_continue_if) {
return true;
}
return step.only_continue_if(step.inputs);
}
async function exec_process(cmd) {
return new Promise(r => {
exec(cmd, (error, stdout, stderr) => {
if(error) {
// console.log(`Error running ${error}: ${stderr}`);
r(stderr);
}
r(null);
});
});
}
async function iterate_layout(inputs, layout, layout_idx, full_cb) {
if(layout_idx >= layout.length) {
await full_cb();
return;
}
let input = layout[layout_idx];
if(input.iter) {
for(const val of input.iter(inputs)) {
// console.log(`${layout_idx}/${layout.length}: ${val}`);
await iterate_layout(inputs, layout, layout_idx + 1, full_cb);
}
} else {
await iterate_layout(inputs, layout, layout_idx + 1, full_cb);
}
}
async function iterate_steps(steps_idx, dead_end_cb, finish_cb) {
if(steps_idx >= step_keys.length) {
await finish_cb();
return;
}
let step = steps[step_keys[steps_idx]];
step.outputs = {};
let old_inputs = {};
for(const input in step.inputs) {
old_inputs[input] = step.inputs[input];
}
if(step.condition && !step.condition()) {
await iterate_steps(steps_idx + 1, dead_end_cb, finish_cb);
return;
}
await iterate_layout(step.inputs, step.layout(), 0, async () => {
if(!allow_next_step(step)) {
await dead_end_cb(steps_idx);
return;
};
await finalize_step(step);
await iterate_steps(steps_idx + 1, dead_end_cb, finish_cb);
});
step.inputs = old_inputs;
step.outputs = {};
}
// for(const step in steps) {
// console.log(step);
// if(steps[step].condition) steps[step].condition();
// for(const input of steps[step].layout()) {
// console.log(input);
// if(input.iter) for(const val of input.iter(steps[step].inputs)) {
// console.log(" - " + val);
// }
// }
//
// console.log(steps[step].inputs);
// }
let dead_ends = 0;
let viable_paths = 0;
await iterate_steps(0, async (step_idx) => {
// console.log("dead end at step idx " + step_idx + " (" + step_keys[step_idx] + ")");
//
// let out = {};
//
// for(const key in steps) {
// out[key] = steps[key].outputs;
// }
//
// console.log(out);
dead_ends += 1;
}, async () => {
viable_paths += 1;
});
console.log(`Found ${viable_paths} viable paths and ${dead_ends} dead ends.`);
let test_ctr = 0;
let failed_test_ctr = 0;
const base_path = `/tmp/tultemplategen_test_${new Date().toISOString()}`;
await fs.mkdir(base_path)
async function store_test_results(idx, stderr) {
let inputs = {};
let outputs = {};
for(const key in steps) {
inputs[key] = steps[key].inputs;
outputs[key] = steps[key].outputs;
}
let outdir = `${base_path}/tultemplategen_failed_test_${idx}`;
await fs.mkdir(outdir);
await fs.rename(`${base_path}/tultemplategen_test_bundle.zip`, `${outdir}/thesis.zip`);
await fs.writeFile(`${outdir}/inputs.json`, JSON.stringify(inputs));
await fs.writeFile(`${outdir}/outputs.json`, JSON.stringify(outputs));
await fs.writeFile(`${outdir}/stderr`, stderr);
failed_test_ctr++;
if(failed_test_ctr >= 10) {
console.log("10 test failed. Aborting.");
process.exit(1);
}
}
await iterate_steps(0, () => {}, async () => {
test_ctr++;
console.log(`Running test ${test_ctr}/${viable_paths}...`);
// console.log(" - generating archive...");
let zip = await generate_zip();
// console.log(" - saving archive...");
await fs.writeFile(`${base_path}/tultemplategen_test_bundle.zip`, zip);
// console.log(" - clearing old directory...");
await fs.rm(`${base_path}/tul-thesis`, { recursive: true, force: true }, () => r());
// console.log(" - extracting archive...");
let unzip_output = await exec_process(`cd ${base_path} && unzip tultemplategen_test_bundle.zip`);
if(unzip_output !== null) {
console.log(" - failed during extraction");
store_test_results(test_ctr, unzip_output);
return;
}
// console.log(" - compiling...")
let make_output = await exec_process(`cd ${base_path}/tul-thesis && make thesis.pdf`);
if(make_output !== null) {
console.log(" - failed during building");
store_test_results(test_ctr, make_output);
return;
}
});
console.log("Finished.");

BIN
tests/test-pages.pdf Normal file

Binary file not shown.