diff options
| author | _ov4 <ov4@ov4.org> | 2026-10-22 11:46:32 +0000 |
|---|---|---|
| committer | _ov4 <ov4@ov4.org> | 2026-10-22 11:46:32 +0000 |
| commit | 4494b54d0e4b0b1564a7c66391216b98c8523b0e (patch) | |
| tree | 6ed15f551e826b8623581292c4cdab2f05fbf9b9 /bundle/scripts | |
| parent | 8d82bcba8f24a1ee30d4dee8a2ba19d2b58e7f4f (diff) | |
build(bundle): port the package smoke test to Perl
Replace bundle/scripts/smoke-package.py with smoke-package.pl, which uses
only core modules like the other build and test scripts. Windows runs it
with native Strawberry Perl.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'bundle/scripts')
| -rw-r--r-- | bundle/scripts/smoke-package.pl | 106 | ||||
| -rw-r--r-- | bundle/scripts/smoke-package.py | 50 |
2 files changed, 106 insertions, 50 deletions
diff --git a/bundle/scripts/smoke-package.pl b/bundle/scripts/smoke-package.pl new file mode 100644 index 0000000..919e925 --- /dev/null +++ b/bundle/scripts/smoke-package.pl @@ -0,0 +1,106 @@ +#!/usr/bin/env perl +# Check a portable desktop package without relying on an installed toolchain. +use strict; +use warnings; +use Cwd qw(abs_path); +use File::Basename qw(dirname); +use File::Find (); +use File::Spec; +use File::Temp qw(tempdir); +use IPC::Open3 qw(open3); +use POSIX qw(WNOHANG); +use Time::HiRes qw(sleep time); + +my $IS_WINDOWS = $^O eq 'MSWin32'; + +sub write_file { + my ($path, $layer, $data) = @_; + open my $fh, ">$layer", $path or die "cannot write $path: $!\n"; + print {$fh} $data; + close $fh or die "cannot write $path: $!\n"; +} + +sub read_file { + my ($path) = @_; + open my $fh, '<:raw', $path or die "cannot read $path: $!\n"; + my $data = do { local $/; <$fh> }; + close $fh; + return defined($data) ? $data : ''; +} + +# Reap a child within $seconds, killing it on timeout. Returns its exit code. +sub finish { + my ($pid, $seconds, $what) = @_; + my $deadline = time() + $seconds; + while (waitpid($pid, WNOHANG) == 0) { + if (time() >= $deadline) { + kill 'KILL', $pid; + waitpid($pid, 0); + die "$what timed out\n"; + } + sleep 0.1; + } + return $? >> 8; +} + +my $root = abs_path($ARGV[0]); +die "Missing package path\n" unless defined $root; +# Children inherit %ENV; Win32 %ENV lookups ignore case. +$ENV{PATH} = $IS_WINDOWS + ? join(';', File::Spec->catdir($ENV{SystemRoot}, 'System32'), $ENV{SystemRoot}) + : '/nonexistent'; +# Children also inherit the cwd. Exit cleanup leaves it before removing it. +my $work = tempdir('cpsc bundle smoke XXXXXXXX', TMPDIR => 1, CLEANUP => 1); +chdir $work or die "cannot chdir to $work: $!\n"; +my $compiler; +if ($IS_WINDOWS) { + die "Expected a single portable .exe\n" unless -f $root && $root =~ /\.exe\z/; + # Extraction happens before WebView startup and publishes a complete file by rename. + my $desktop = system 1, $root; + die "cannot start $root: $!\n" unless $desktop > 0; + $compiler = File::Spec->catfile($work, 'cpsc.exe'); + my $extracted = eval { + my $deadline = time() + 30; + until (-f $compiler) { + die "Desktop exited before extracting its compiler\n" if waitpid($desktop, WNOHANG) != 0; + die "Compiler extraction timed out\n" if time() >= $deadline; + sleep 0.1; + } + 1; + }; + my $error = $@; + kill 'KILL', $desktop if waitpid($desktop, WNOHANG) == 0; + finish($desktop, 10, 'Desktop shutdown'); + die $error unless $extracted; +} else { + my @compilers; + File::Find::find(sub { push @compilers, $File::Find::name if $_ eq 'cpsc' }, $root); + die "Expected one packaged compiler, found [@compilers]\n" unless @compilers == 1; + $compiler = $compilers[0]; + my $directory = dirname($compiler); + die "Missing desktop executable\n" unless -f File::Spec->catfile($directory, 'cpsc-bundle'); + my $resources = $^O eq 'darwin' ? File::Spec->catdir(dirname($directory), 'Resources') : $directory; + for my $filename ('LICENSE', 'ThirdPartyNotices.md', 'licenses/vscode.txt', 'licenses/plugin.md') { + die "Missing $filename\n" unless -f File::Spec->catfile($resources, $filename); + } +} +# Text mode: Windows sources and data files use CRLF. +write_file('data.txt', '', "relative file\n"); +my $source = File::Spec->catfile($work, 'source file.cps'); +write_file($source, '', qq{DECLARE N : INTEGER\nINPUT N\nOUTPUT N\nDECLARE S : STRING\nOPENFILE "data.txt" FOR READ\nREADFILE "data.txt", S\nCLOSEFILE "data.txt"\nOUTPUT S\n}); +# Files instead of pipes: Windows cannot poll pipes, and files cannot fill up. +write_file('stdin', ':raw', "17\n"); +open my $in, '<:raw', 'stdin' or die "cannot read stdin: $!\n"; +open my $out, '>:raw', 'stdout' or die "cannot write stdout: $!\n"; +open my $err, '>:raw', 'stderr' or die "cannot write stderr: $!\n"; +my $pid = open3('<&' . fileno($in), '>&' . fileno($out), '>&' . fileno($err), $compiler, $source, '-r'); +my $status = finish($pid, 30, 'Compiler run'); +close $out; +close $err; +my $stdout = read_file('stdout'); +my $stderr = read_file('stderr'); +die "Compiler exited with $status: '$stderr'\n" unless $status == 0; +(my $normalized = $stdout) =~ s/\r\n/\n/g; +die "Unexpected output: '$stdout'\n" unless $normalized eq "17\nrelative file\n"; +die "Unexpected diagnostics: '$stderr'\n" unless $stderr eq ''; +print "Desktop package smoke passed: portable application, compiler, stdin and relative files.\n"; diff --git a/bundle/scripts/smoke-package.py b/bundle/scripts/smoke-package.py deleted file mode 100644 index 44f885f..0000000 --- a/bundle/scripts/smoke-package.py +++ /dev/null @@ -1,50 +0,0 @@ -"""Check a portable desktop package without relying on an installed toolchain.""" - -from pathlib import Path -import os -import subprocess -import sys -import tempfile -import time - -root = Path(sys.argv[1]).resolve() -environment = os.environ.copy() -if sys.platform == "win32": - # Windows environ lookups ignore case; the copied dict has uppercase keys. - system_root = os.environ["SystemRoot"] - environment["PATH"] = os.pathsep.join((os.path.join(system_root, "System32"), system_root)) -else: - environment["PATH"] = "/nonexistent" -with tempfile.TemporaryDirectory(prefix="cpsc bundle smoke ") as directory: - work = Path(directory) - if sys.platform == "win32": - assert root.is_file() and root.suffix == ".exe", "Expected a single portable .exe" - # Extraction happens before WebView startup and publishes a complete file by rename. - desktop = subprocess.Popen([str(root)], cwd=work, env=environment) - compiler = work / "cpsc.exe" - try: - deadline = time.monotonic() + 30 - while not compiler.is_file(): - assert desktop.poll() is None, "Desktop exited before extracting its compiler" - assert time.monotonic() < deadline, "Compiler extraction timed out" - time.sleep(0.1) - finally: - if desktop.poll() is None: - desktop.terminate() - desktop.wait(timeout=10) - else: - compilers = list(root.rglob("cpsc")) - assert len(compilers) == 1, f"Expected one packaged compiler, found {compilers}" - compiler = compilers[0] - assert compiler.with_name("cpsc-bundle").is_file(), "Missing desktop executable" - resources = compiler.parent.parent / "Resources" if sys.platform == "darwin" else compiler.parent - for filename in ("LICENSE", "ThirdPartyNotices.md", "licenses/vscode.txt", "licenses/plugin.md"): - assert (resources / filename).is_file(), f"Missing {filename}" - (work / "data.txt").write_text("relative file\n", encoding="utf-8") - source = work / "source file.cps" - source.write_text('DECLARE N : INTEGER\nINPUT N\nOUTPUT N\nDECLARE S : STRING\nOPENFILE "data.txt" FOR READ\nREADFILE "data.txt", S\nCLOSEFILE "data.txt"\nOUTPUT S\n', encoding="utf-8") - result = subprocess.run([str(compiler), str(source), "-r"], input=b"17\n", capture_output=True, cwd=work, env=environment, timeout=30) - assert result.returncode == 0, result.stderr - assert result.stdout.replace(b"\r\n", b"\n") == b"17\nrelative file\n", result.stdout - assert result.stderr == b"", result.stderr -print("Desktop package smoke passed: portable application, compiler, stdin and relative files.") |
