build_test.v

8.9 KB · raw

1//! Unit tests for the build module: the version a project declares, what2//! `koh.build` says, the environment a build gets, and one real build.34module build56import os7import time89fn scratch(name string) !string {10	dir := os.join_path(os.temp_dir(), 'koh3-build-${name}-${time.now().unix_nano()}')11	os.mkdir_all(dir, os.MkdirParams{})!12	return dir13}1415fn write(root string, rel string, text string) ! {16	full := os.join_path(root, rel)17	os.mkdir_all(os.dir(full), os.MkdirParams{})!18	os.write_file(full, text)!19}2021struct SourceCase {22	file    string23	content string24	want    string25}2627fn test_every_version_source_is_read() {28	zon := '.{\n    .name = "demo",\n    .minimum_zig_version = "0.16.0",\n    .version = "2.0.1",\n}\n'29	plist := '<?xml version="1.0"?>\n<dict>\n<key>CFBundleShortVersionString</key>\n<string>6.0.5</string>\n</dict>\n'30	cases := [31		SourceCase{'VERSION', '1.0.0\n', '1.0.0'},32		SourceCase{'build.zig.zon', zon, '2.0.1'},33		SourceCase{'v.mod', "Module {\n\tname: 'demo'\n\tversion: '3.0.2'\n}", '3.0.2'},34		SourceCase{'package.json', '{\n  "name": "demo",\n  "version": "4.0.3"\n}', '4.0.3'},35		SourceCase{'Cargo.toml', '[package]\nname = "demo"\nversion = "5.0.4"\n', '5.0.4'},36		SourceCase{'Info.plist', plist, '6.0.5'},37		SourceCase{'build.gradle.kts', 'versionName = "7.0.6"\n', '7.0.6'},38		SourceCase{'build.gradle', 'versionName "8.0.7"\n', '8.0.7'},39		SourceCase{'build.gradle', "versionName '9.0.8'\n", '9.0.8'},40		SourceCase{'app/build.gradle.kts', 'versionName = "10.0.9"\n', '10.0.9'},41		SourceCase{'app/build.gradle', "versionName '10.0.9'\n", '10.0.9'},42	]43	for case in cases {44		root := scratch('source')!45		defer {46			os.rmdir_all(root) or {}47		}48		write(root, case.file, case.content)!49		assert project_version(root)! == case.want, case.file50	}51}5253// A VERSION file is the operator's override, so it wins over everything else.54fn test_a_version_file_wins_over_every_source() {55	root := scratch('override')!56	defer {57		os.rmdir_all(root) or {}58	}59	write(root, 'build.zig.zon', '.{\n    .version = "2.0.1",\n}\n')!60	write(root, 'v.mod', "Module {\n\tversion: '3.0.2'\n}")!61	write(root, 'VERSION', '9.9.9\n')!62	assert project_version(root)! == '9.9.9'63}6465// Nothing to read is refused, never published as 0.0.0.66fn test_a_project_with_no_version_is_refused() {67	root := scratch('no_version')!68	defer {69		os.rmdir_all(root) or {}70	}71	if _ := project_version(root) {72		assert false, 'a project with no version must be refused'73	} else {74		assert err.msg().contains('VERSION')75	}76}7778// A value that is not a version would become a file name, so it is refused79// where it was found rather than used.80fn test_a_value_that_is_not_a_version_is_refused() {81	root := scratch('bad_version')!82	defer {83		os.rmdir_all(root) or {}84	}85	write(root, 'VERSION', '1.0.0 build 7\n')!86	if _ := project_version(root) {87		assert false, 'a value that is not a version must be refused'88	} else {89		assert err.msg().contains('VERSION')90	}91	write(root, 'VERSION', 'abcdefghijklmnopqrstuvwxyz0123456789\n')!92	if _ := project_version(root) {93		assert false, 'a version longer than 32 characters must be refused'94	} else {95		assert err.msg().contains('VERSION')96	}97}9899// Koh never runs Gradle: a versionName built from a variable is not a version,100// so the next Gradle file is tried, and the next source after that.101fn test_a_variable_version_name_falls_through() {102	root := scratch('gradle_variable')!103	defer {104		os.rmdir_all(root) or {}105	}106	write(root, 'app/build.gradle.kts', 'versionName = project.ext.version\n')!107	write(root, 'build.gradle', 'versionName "2.17.43"\n')!108	assert project_version(root)! == '2.17.43'109110	// A commented-out literal is not read, and versionNameSuffix is not a111	// versionName: with nothing left, the refusal names a VERSION file.112	write(root, 'build.gradle', '// versionName "9.9.9"\nversionNameSuffix = "-beta"\n')!113	if _ := project_version(root) {114		assert false, 'nothing was readable, so the version must be refused'115	} else {116		assert err.msg().contains('VERSION')117	}118}119120fn test_read_config_parses_the_file_and_ignores_what_it_does_not_use() {121	root := scratch('config')!122	defer {123		os.rmdir_all(root) or {}124	}125	write(root, 'koh.build', '# how demo is built126enabled         = true127trigger         = on-offer128timeout_seconds = 900129command         = ./make.sh130output          = out/demo-{version}-{platform}131platforms       = macos-aarch64, linux-aarch64132lang            = Zig133')134	config := read_config(root)!135	assert config.command == './make.sh'136	assert config.output == 'out/demo-{version}-{platform}'137	assert config.platforms == ['macos-aarch64', 'linux-aarch64']138	assert config.lang == .zig139	assert output_path(config, 'linux-aarch64', '1.2.3') == 'out/demo-1.2.3-linux-aarch64'140}141142// The language is detected when the file does not name one, so an existing143// Zig project's build file needs no edit to keep cross-compiling.144fn test_the_language_is_detected_from_the_project() {145	root := scratch('detect')!146	defer {147		os.rmdir_all(root) or {}148	}149	write(root, 'koh.build', 'command = true\noutput = a\nplatforms = macos-aarch64\n')!150	write(root, 'build.zig', '// a zig project\n')!151	assert read_config(root)!.lang == .zig152	write(root, 'build.sh', '#!/bin/sh\n')!153	assert read_config(root)!.lang == .shell154}155156fn test_read_config_refuses_what_it_cannot_build() {157	root := scratch('config_bad')!158	defer {159		os.rmdir_all(root) or {}160	}161	if _ := read_config(root) {162		assert false, 'a missing koh.build must be refused'163	} else {164		assert err.msg().contains('koh.build')165	}166}167168fn test_read_config_refuses_each_missing_key() {169	root := scratch('config_keys')!170	defer {171		os.rmdir_all(root) or {}172	}173	cases := [174		['command = true\nplatforms = macos-aarch64\n', 'has no output'],175		['output = a\nplatforms = macos-aarch64\n', 'has no command'],176		['command = true\noutput = a\n', 'names no platforms'],177		['command = true\noutput = a\nplatforms = macos-aarch64, amiga\n', 'amiga'],178		['command = true\noutput = a\nplatforms = macos-aarch64\nlang = cobol\n', 'cobol'],179	]180	for case in cases {181		write(root, 'koh.build', case[0])!182		if _ := read_config(root) {183			assert false, case[1]184		} else {185			assert err.msg().contains(case[1]), err.msg()186		}187	}188}189190fn test_the_build_environment_is_koh_2s() {191	zig := env_for(.zig, 'linux-aarch64')192	assert zig['KOH_TARGET'] == 'aarch64-linux-musl'193	assert zig['KEPR_TARGET'] == 'aarch64-linux-musl'194	// `go` is a V keyword, so the map needs another name.195	go_env := env_for(.go, 'linux-x86_64')196	assert go_env['KOH_TARGET'] == 'linux-x86_64'197	assert go_env['KEPR_TARGET'] == 'linux-x86_64'198	assert go_env['GOOS'] == 'linux'199	assert go_env['GOARCH'] == 'amd64'200	assert env_for(.rust, 'windows-x86_64')['CARGO_BUILD_TARGET'] == 'x86_64-pc-windows-gnu'201	assert env_for(.rust, 'macos-aarch64')['CARGO_BUILD_TARGET'] == 'aarch64-apple-darwin'202	assert env_for(.c, 'linux-x86_64')['CC'] == 'x86_64-linux-musl-gcc'203	// macOS has no musl cc, and Android carries the targets and nothing else.204	assert 'CC' !in env_for(.c, 'macos-x86_64')205	assert env_for(.gradle, 'android-arm64').len == 2206	assert env_for(.node, 'android-x86')['KEPR_TARGET'] == 'android-x86'207}208209fn test_this_machine_is_one_of_the_platforms() {210	assert this_machine()! in platforms211}212213// A real build, small enough for a unit test: the command writes the file214// `output` names, and the environment it saw is in that file. Only `output`215// takes the tokens — a build script learns the version from the project itself,216// the way Peach Android's reads VERSION.217fn test_build_one_runs_the_command_and_finds_the_output() {218	root := scratch('build_one')!219	defer {220		os.rmdir_all(root) or {}221	}222	write(root, 'koh.build', 'command = mkdir -p out && env > out/demo-1.2.3223output = out/demo-{version}224platforms = macos-aarch64225lang = make226')!227	config := read_config(root)!228	built := build_one(root, config, 'macos-aarch64', '1.2.3')!229	assert built.platform == 'macos-aarch64'230	assert built.path == 'out/demo-1.2.3'231	assert built.size > 0232	seen := os.read_file(os.join_path(root, 'out/demo-1.2.3'))!233	assert seen.contains('KEPR_TARGET=macos-aarch64')234	assert seen.contains('KOH_TARGET=macos-aarch64')235}236237fn test_build_one_refuses_a_failing_build_and_shows_what_it_said() {238	root := scratch('build_fail')!239	defer {240		os.rmdir_all(root) or {}241	}242	write(root, 'koh.build', 'command = sh -c \'echo "no compiler here" >&2; exit 1\'243output = out/demo244platforms = macos-aarch64245')246	config := read_config(root)!247	if _ := build_one(root, config, 'macos-aarch64', '1.2.3') {248		assert false, 'a failing build must be refused'249	} else {250		assert err.msg().contains('no compiler here')251	}252}253254// A build that leaves nothing behind is a build failure, not a release.255fn test_build_one_refuses_a_build_that_creates_nothing() {256	root := scratch('build_empty')!257	defer {258		os.rmdir_all(root) or {}259	}260	write(root, 'koh.build', 'command = true261output = out/demo262platforms = macos-aarch64263')264	config := read_config(root)!265	if _ := build_one(root, config, 'macos-aarch64', '1.2.3') {266		assert false, 'a build with no output must be refused'267	} else {268		assert err.msg().contains('did not create')269	}270}