Files
STCourses/packages/feature-import/test/fixtures.ts
T

126 lines
6.5 KiB
TypeScript

// Tiny synthetic inputs for the importer tests. Nothing here is real course material.
import { existsSync } from "node:fs";
import { mkdir, mkdtemp, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import { run } from "../src/util/exec";
// PyMuPDF is needed for PDF tests: set STC_PYTHON, or this machine's venv is used when present.
const VENV_PY = "/home/user/.venvs/pdf/bin/python";
export const PYTHON = process.env.STC_PYTHON ?? (existsSync(VENV_PY) ? VENV_PY : "python3");
export async function tmp(prefix = "stc-imp-"): Promise<string> {
return mkdtemp(join(tmpdir(), prefix));
}
export async function makeVideo(path: string, seconds = 2): Promise<void> {
await mkdir(dirname(path), { recursive: true });
await run(["ffmpeg", "-v", "error", "-y", "-f", "lavfi", "-i", `testsrc=duration=${seconds}:size=160x90:rate=10`, "-f", "lavfi", "-i", `sine=frequency=440:duration=${seconds}`,
"-c:v", "libx264", "-pix_fmt", "yuv420p", "-c:a", "aac", "-shortest", path]);
}
/** Minimal valid PDF with one line of text per page. */
export function pdfBytes(pages: string[]): Uint8Array {
const objs: string[] = [];
const kids = pages.map((_, i) => `${4 + i * 2} 0 R`).join(" ");
objs.push("<< /Type /Catalog /Pages 2 0 R >>");
objs.push(`<< /Type /Pages /Kids [${kids}] /Count ${pages.length} >>`);
objs.push("<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>");
pages.forEach((text, i) => {
const content = `BT /F1 24 Tf 40 150 Td (${text.replace(/[()\\]/g, "")}) Tj ET`;
objs.push(`<< /Type /Page /Parent 2 0 R /MediaBox [0 0 320 200] /Resources << /Font << /F1 3 0 R >> >> /Contents ${5 + i * 2} 0 R >>`);
objs.push(`<< /Length ${content.length} >>\nstream\n${content}\nendstream`);
});
let out = "%PDF-1.4\n";
const offsets: number[] = [];
objs.forEach((o, i) => { offsets.push(out.length); out += `${i + 1} 0 obj\n${o}\nendobj\n`; });
const xref = out.length;
out += `xref\n0 ${objs.length + 1}\n0000000000 65535 f \n` + offsets.map((o) => `${String(o).padStart(10, "0")} 00000 n \n`).join("");
out += `trailer\n<< /Size ${objs.length + 1} /Root 1 0 R >>\nstartxref\n${xref}\n%%EOF\n`;
return new TextEncoder().encode(out);
}
export async function makePdf(path: string, pages = ["First slide alpha", "Second slide beta"]): Promise<void> {
await mkdir(dirname(path), { recursive: true });
await writeFile(path, pdfBytes(pages));
}
export async function zipDir(srcDir: string, zipPath: string): Promise<void> {
await run([PYTHON, "-I", "-c",
"import sys,os,zipfile\nsrc,out=sys.argv[1:3]\nz=zipfile.ZipFile(out,'w',zipfile.ZIP_STORED)\nfor r,d,f in os.walk(src):\n for n in f:\n p=os.path.join(r,n); z.write(p, os.path.relpath(p,src))\nz.close()",
srcDir, zipPath]);
}
const li = (t: string) => `<li>${t}</li>`;
const clipHtml = (s: number, m: number, title: string) => `
<li>
<img src="images/image.jpg" class="image-holder"/>
<h1>${s}.${m} ${title}</h1>
<span>In this video, ${title}, you&#039;ll learn it.</span>
<div id="video${s}_${m}" class="btn btn-small btn-primary watch" data-video="videos/video${s}_${m}.mp4" data-poster="images/videoImage.jpg">Watch Video</div>
<a href="videos/video${s}_${m}.mp4" class="btn download">Download</a>
</li>`;
export const PACKT_SECTIONS = [
{ title: "Intro to Docker", clips: ["Course Introduction", "Docker &amp; Friends"] },
{ title: "Images (Sample)", clips: ["Topic Introduction - Images"] },
];
/** Mimics the real package layout: summary lists without "Video N.M:" prefixes, then chapter blocks. */
export function packtIndexHtml(opts: { dropChapter?: number } = {}): string {
const summary = PACKT_SECTIONS.map((s, i) => `
<li>
<img src="images/image.jpg" class="image-holder"/>
<h1>Section ${i + 1}: ${s.title}</h1>
<p>Video ${i + 1}.1: ${s.clips[0]}</p>
<ul>${s.clips.map(li).join("\n")}</ul>
<div class="btn btn-primary browse" id="#content-${i + 1}">Browse Videos</div>
</li>`).join("");
const chapters = PACKT_SECTIONS.map((s, i) => i + 1 === opts.dropChapter ? "" : `
<div class="chapter" id="content-${i + 1}" style="display: none;">
<ul class="move">${s.clips.map((c, j) => clipHtml(i + 1, j + 1, c)).join("")}
</ul>
</div>`).join("");
return `<!doctype html><html><head><title>Fake</title></head><body>
<div class="intro"><div class="inner"><h1>Fake Docker Course</h1></div></div>
<div id="content-main"><ul class="move">${summary}</ul>${chapters}</div></body></html>`;
}
export async function makePacktTree(root: string): Promise<void> {
await mkdir(join(root, "package", "videos"), { recursive: true });
await writeFile(join(root, "package", "index.html"), packtIndexHtml());
await writeFile(join(root, "Video Package_User Guide.pdf"), "not used");
await makeVideo(join(root, "package/videos/video1_1.mp4"), 2);
for (const n of ["video1_2", "video2_1"]) await Bun.write(join(root, "package/videos", `${n}.mp4`), Bun.file(join(root, "package/videos/video1_1.mp4")));
}
export async function makePacktZip(dir: string): Promise<string> {
const tree = join(dir, "packt-tree");
await makePacktTree(tree);
const zip = join(dir, "video_zip.zip");
await zipDir(tree, zip);
return zip;
}
/** Companion repo with folders that have spaces; S01 has a deck and a cheat sheet, S02 has code. */
export async function makeRepoTree(root: string): Promise<string> {
const top = join(root, "Course-Repo-main");
await makePdf(join(top, "S01 - Intro to Docker", "Slides_Intro.pdf"), ["Repo deck one", "Repo deck two", "Repo deck three"]);
await makePdf(join(top, "S01 - Intro to Docker", "Docker_CheatSheet.pdf"), ["cheats"]);
await mkdir(join(top, "S02 - Images (Sample)", "app"), { recursive: true });
await writeFile(join(top, "S02 - Images (Sample)", "app", "main.py"), "print('hi')\n");
await writeFile(join(top, "S02 - Images (Sample)", "Ünïcode notes.txt"), "notes\n");
await mkdir(join(top, "Bonus Extras"), { recursive: true });
await writeFile(join(top, "Bonus Extras", "x.txt"), "x");
await writeFile(join(top, "README.md"), "# repo\n");
return top;
}
export async function makeRepoZip(dir: string): Promise<string> {
const tree = join(dir, "repo-tree");
await makeRepoTree(tree);
const zip = join(dir, "repo.zip");
await zipDir(tree, zip);
return zip;
}