// Whitebox tests for `build_android.mbt`.
//
// Tests focus on the pure helpers (`stage_android_window_hosted_project`,
// `copy_tree`, `copy_file_atomic`, `resolve_gradle_command`). End-to-end
// Gradle invocation is exercised by integration tests with a real Android
// SDK and is intentionally not covered here.
///|
fn build_android_test_temp_path(label : String) -> String {
let root = match @env.get_env_var("TMPDIR") {
Some(value) => value
None =>
match @env.get_env_var("TEMP") {
Some(value) => value
None => @env.get_env_var("TMP").unwrap_or(".")
}
}
join_path(
root,
"moui-cli-build-android-" + label + "-" + @env.now().to_string(),
)
}
///|
test "test_stage_android_window_hosted_project_refuses_unowned_target" {
let root = build_android_test_temp_path("refuse-unowned")
remove_tree(root)
match mkdir_p(root) {
Ok(_) => ()
Err(message) => fail(message)
}
// Pre-existing dir without `.moui-window-hosted-stage` marker must be refused.
let target = join_path(root, "android-project")
match mkdir_p(target) {
Ok(_) => ()
Err(message) => {
remove_tree(root)
fail(message)
}
}
match stage_android_window_hosted_project("/nonexistent/template", target) {
Ok(_) => {
remove_tree(root)
fail(
"expected stage_android_window_hosted_project to refuse unowned target",
)
}
Err(message) => {
remove_tree(root)
assert_true(message.contains("unowned"))
}
}
}
///|
test "test_stage_android_window_hosted_project_copies_template_and_writes_marker" {
let root = build_android_test_temp_path("stage-managed")
remove_tree(root)
match mkdir_p(root) {
Ok(_) => ()
Err(message) => fail(message)
}
// Build a tiny fake template: `settings.gradle` + `app/build.gradle`.
let template = join_path(root, "template")
match mkdir_p(join_path(template, "app")) {
Ok(_) => ()
Err(message) => {
remove_tree(root)
fail(message)
}
}
match
write_file_bytes_result(
join_path(template, "settings.gradle"),
b"rootProject.name = 'moui'\n",
) {
Ok(_) => ()
Err(message) => {
remove_tree(root)
fail(message)
}
}
match
write_file_bytes_result(
join_path(template, "app/build.gradle"),
b"// app\n",
) {
Ok(_) => ()
Err(message) => {
remove_tree(root)
fail(message)
}
}
let target = join_path(root, "android-project")
match stage_android_window_hosted_project(template, target) {
Ok(_) => ()
Err(message) => {
remove_tree(root)
fail(message)
}
}
// Verify files copied and marker written.
assert_true(path_is_file(join_path(target, "settings.gradle")))
assert_true(path_is_file(join_path(target, "app/build.gradle")))
assert_true(path_is_file(join_path(target, ".moui-window-hosted-stage")))
remove_tree(root)
}
///|
test "test_copy_tree_recursive_copy" {
let root = build_android_test_temp_path("copy-tree")
remove_tree(root)
match mkdir_p(root) {
Ok(_) => ()
Err(message) => fail(message)
}
let source = join_path(root, "source")
let nested = join_path(source, "sub/deep")
match mkdir_p(nested) {
Ok(_) => ()
Err(message) => {
remove_tree(root)
fail(message)
}
}
match write_file_bytes_result(join_path(source, "a.txt"), b"alpha\n") {
Ok(_) => ()
Err(message) => {
remove_tree(root)
fail(message)
}
}
match write_file_bytes_result(join_path(nested, "b.txt"), b"beta\n") {
Ok(_) => ()
Err(message) => {
remove_tree(root)
fail(message)
}
}
let target = join_path(root, "target")
match mkdir_p(target) {
Ok(_) => ()
Err(message) => {
remove_tree(root)
fail(message)
}
}
match copy_tree(source, target) {
Ok(_) => ()
Err(message) => {
remove_tree(root)
fail(message)
}
}
assert_true(path_is_file(join_path(target, "a.txt")))
assert_true(path_is_file(join_path(target, "sub/deep/b.txt")))
remove_tree(root)
}
///|
test "test_copy_file_atomic_copies_bytes" {
let root = build_android_test_temp_path("copy-atomic")
remove_tree(root)
match mkdir_p(root) {
Ok(_) => ()
Err(message) => fail(message)
}
let source = join_path(root, "source.bin")
let target = join_path(root, "target.bin")
let payload = b"\x00\x01\x02\x03moui\xFF"
match write_file_bytes_result(source, payload) {
Ok(_) => ()
Err(message) => {
remove_tree(root)
fail(message)
}
}
match copy_file_atomic(source, target) {
Ok(_) => ()
Err(message) => {
remove_tree(root)
fail(message)
}
}
match read_file_bytes_optional(target) {
Some(bytes) => {
inspect(bytes, content="b\"\\x00\\x01\\x02\\x03moui\\xff\"")
remove_tree(root)
}
None => {
remove_tree(root)
fail("could not read target file back")
}
}
}
///|
test "test_copy_file_atomic_returns_err_for_missing_source" {
let root = build_android_test_temp_path("copy-atomic-missing")
remove_tree(root)
match mkdir_p(root) {
Ok(_) => ()
Err(message) => fail(message)
}
match
copy_file_atomic(join_path(root, "missing.bin"), join_path(root, "out.bin")) {
Ok(_) => {
remove_tree(root)
fail("expected Err for missing source")
}
Err(message) => {
remove_tree(root)
assert_true(message.contains("could not read"))
}
}
}
///|
test "test_resolve_gradle_command_prefers_workspace_gradlew" {
let root = build_android_test_temp_path("gradle-workspace")
remove_tree(root)
match mkdir_p(root) {
Ok(_) => ()
Err(message) => fail(message)
}
// Touch a fake `gradlew` file with executable bit. On macOS, `path_is_executable`
// checks the file mode; we cannot easily fake that without chmod. Skip the
// executable check by leaving the file non-executable: resolve_gradle_command
// should then fall through to `find_in_path("gradle")`. In a sandbox without
// `gradle` on PATH, this returns Err. Either way, the workspace wrapper is
// not chosen unless executable.
let gradlew = join_path(root, "gradlew")
match write_file_bytes_result(gradlew, b"#!/bin/sh\n") {
Ok(_) => ()
Err(message) => {
remove_tree(root)
fail(message)
}
}
match resolve_gradle_command(root) {
Ok(_) => {
// PATH had a `gradle` binary; acceptable.
remove_tree(root)
assert_true(true)
}
Err(message) => {
remove_tree(root)
assert_true(message.contains("Gradle was not found"))
}
}
}
///|
test "test_resolve_gradle_command_returns_err_without_gradlew_or_path" {
let root = build_android_test_temp_path("gradle-missing")
remove_tree(root)
match mkdir_p(root) {
Ok(_) => ()
Err(message) => fail(message)
}
// In a typical sandbox this returns Err; tolerate the rare case where
// `gradle` is on PATH by accepting either outcome, but assert the message
// format when Err is returned.
match resolve_gradle_command(root) {
Ok(_) => assert_true(true)
Err(message) => assert_true(message.contains("Gradle was not found"))
}
remove_tree(root)
}