build_test.v

10.1 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, versionNameSuffix is not a111	// versionName, and a call is not a literal however many strings it passes:112	// with nothing left, the refusal names a VERSION file.113	write(root, 'build.gradle', '// versionName "9.9.9"\nversionNameSuffix = "-beta"\n')!114	if _ := project_version(root) {115		assert false, 'nothing was readable, so the version must be refused'116	} else {117		assert err.msg().contains('VERSION')118	}119120	// A quoted argument inside a call is not the version.121	write(root, 'build.gradle', "versionName = providers.gradleProperty('VERSION').get()\n")122	if _ := project_version(root) {123		assert false, 'a versionName built from a function call must not be read'124	} else {125		assert err.msg().contains('VERSION')126	}127	// `version.workspace = true` is Cargo delegating to the workspace, not a128	// version either.129	write(root, 'Cargo.toml', '[package]\nversion.workspace = true\n')130	if _ := project_version(root) {131		assert false, 'a version delegated to the workspace must not be read'132	} else {133		assert err.msg().contains('VERSION')134	}135}136137fn test_read_config_parses_the_file_and_ignores_what_it_does_not_use() {138	root := scratch('config')!139	defer {140		os.rmdir_all(root) or {}141	}142	write(root, 'koh.build', '# how demo is built143enabled         = true144trigger         = on-offer145timeout_seconds = 900146command         = ./make.sh147output          = out/demo-{version}-{platform}148platforms       = macos-aarch64, linux-aarch64149lang            = Zig150')151	config := read_config(root)!152	assert config.command == './make.sh'153	assert config.output == 'out/demo-{version}-{platform}'154	assert config.platforms == ['macos-aarch64', 'linux-aarch64']155	assert config.lang == .zig156	assert output_path(config, 'linux-aarch64', '1.2.3') == 'out/demo-1.2.3-linux-aarch64'157}158159// The language is detected when the file does not name one, so an existing160// Zig project's build file needs no edit to keep cross-compiling.161fn test_the_language_is_detected_from_the_project() {162	root := scratch('detect')!163	defer {164		os.rmdir_all(root) or {}165	}166	write(root, 'koh.build', 'command = true\noutput = a\nplatforms = macos-aarch64\n')!167	write(root, 'build.zig', '// a zig project\n')!168	assert read_config(root)!.lang == .zig169	write(root, 'build.sh', '#!/bin/sh\n')!170	assert read_config(root)!.lang == .shell171}172173fn test_read_config_refuses_what_it_cannot_build() {174	root := scratch('config_bad')!175	defer {176		os.rmdir_all(root) or {}177	}178	if _ := read_config(root) {179		assert false, 'a missing koh.build must be refused'180	} else {181		assert err.msg().contains('koh.build')182	}183}184185fn test_read_config_refuses_each_missing_key() {186	root := scratch('config_keys')!187	defer {188		os.rmdir_all(root) or {}189	}190	cases := [191		['command = true\nplatforms = macos-aarch64\n', 'has no output'],192		['output = a\nplatforms = macos-aarch64\n', 'has no command'],193		['command = true\noutput = a\n', 'names no platforms'],194		['command = true\noutput = a\nplatforms = macos-aarch64, amiga\n', 'amiga'],195		['command = true\noutput = a\nplatforms = macos-aarch64\nlang = cobol\n', 'cobol'],196	]197	for case in cases {198		write(root, 'koh.build', case[0])!199		if _ := read_config(root) {200			assert false, case[1]201		} else {202			assert err.msg().contains(case[1]), err.msg()203		}204	}205}206207fn test_the_build_environment_is_koh_2s() {208	zig := env_for(.zig, 'linux-aarch64')209	assert zig['KOH_TARGET'] == 'aarch64-linux-musl'210	assert zig['KEPR_TARGET'] == 'aarch64-linux-musl'211	// `go` is a V keyword, so the map needs another name.212	go_env := env_for(.go, 'linux-x86_64')213	assert go_env['KOH_TARGET'] == 'linux-x86_64'214	assert go_env['KEPR_TARGET'] == 'linux-x86_64'215	assert go_env['GOOS'] == 'linux'216	assert go_env['GOARCH'] == 'amd64'217	assert env_for(.rust, 'windows-x86_64')['CARGO_BUILD_TARGET'] == 'x86_64-pc-windows-gnu'218	assert env_for(.rust, 'macos-aarch64')['CARGO_BUILD_TARGET'] == 'aarch64-apple-darwin'219	assert env_for(.c, 'linux-x86_64')['CC'] == 'x86_64-linux-musl-gcc'220	// macOS has no musl cc, and Android carries the targets and nothing else.221	assert 'CC' !in env_for(.c, 'macos-x86_64')222	assert env_for(.gradle, 'android-arm64').len == 2223	assert env_for(.node, 'android-x86')['KEPR_TARGET'] == 'android-x86'224}225226fn test_this_machine_is_one_of_the_platforms() {227	assert this_machine()! in platforms228}229230// A real build, small enough for a unit test: the command writes the file231// `output` names, and the environment it saw is in that file. Only `output`232// takes the tokens — a build script learns the version from the project itself,233// the way Peach Android's reads VERSION.234fn test_build_one_runs_the_command_and_finds_the_output() {235	root := scratch('build_one')!236	defer {237		os.rmdir_all(root) or {}238	}239	write(root, 'koh.build', 'command = mkdir -p out && env > out/demo-1.2.3240output = out/demo-{version}241platforms = macos-aarch64242lang = make243')!244	config := read_config(root)!245	built := build_one(root, config, 'macos-aarch64', '1.2.3')!246	assert built.platform == 'macos-aarch64'247	assert built.path == 'out/demo-1.2.3'248	assert built.size > 0249	seen := os.read_file(os.join_path(root, 'out/demo-1.2.3'))!250	assert seen.contains('KEPR_TARGET=macos-aarch64')251	assert seen.contains('KOH_TARGET=macos-aarch64')252}253254fn test_build_one_refuses_a_failing_build_and_shows_what_it_said() {255	root := scratch('build_fail')!256	defer {257		os.rmdir_all(root) or {}258	}259	write(root, 'koh.build', 'command = sh -c \'echo "no compiler here" >&2; exit 1\'260output = out/demo261platforms = macos-aarch64262')263	config := read_config(root)!264	if _ := build_one(root, config, 'macos-aarch64', '1.2.3') {265		assert false, 'a failing build must be refused'266	} else {267		assert err.msg().contains('no compiler here')268	}269}270271// A build that leaves nothing behind is a build failure, not a release.272fn test_build_one_refuses_a_build_that_creates_nothing() {273	root := scratch('build_empty')!274	defer {275		os.rmdir_all(root) or {}276	}277	write(root, 'koh.build', 'command = true278output = out/demo279platforms = macos-aarch64280')281	config := read_config(root)!282	if _ := build_one(root, config, 'macos-aarch64', '1.2.3') {283		assert false, 'a build with no output must be refused'284	} else {285		assert err.msg().contains('did not create')286	}287}288289// A build that leaves an empty file did not build anything: 0 bytes would be290// published as a release for that platform.291fn test_build_one_refuses_an_empty_artifact() {292	root := scratch('build_zero')!293	defer {294		os.rmdir_all(root) or {}295	}296	write(root, 'koh.build', 'command = mkdir -p out && : > out/demo297output = out/demo298platforms = macos-aarch64299')!300	config := read_config(root)!301	if _ := build_one(root, config, 'macos-aarch64', '1.2.3') {302		assert false, 'an empty artifact must be refused'303	} else {304		assert err.msg().contains('produced an empty')305	}306}