| # Copyright 2022 The Fuchsia Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| import("//build/assembly/assembled_system.gni") |
| import("//build/assembly/hybrid_board_configuration.gni") |
| import("//build/assembly/product_bundle.gni") |
| import("//build/testing/environments.gni") |
| import("//build/testing/test_spec.gni") |
| import("//zircon/kernel/phys/qemu.gni") |
| |
| declare_args() { |
| # Default value for `disabled` parameter for generated `boot_test()`. |
| # TODO(https://fxbug.dev/320511796): Cleanup when no longer necessary. |
| disable_boot_tests = false |
| } |
| |
| # The test runners have no better way to determine that a boot test succeeded |
| # than to look for an exact string in the console log output. zbi_test() |
| # targets produce metadata to drive the test runners, which tells them to |
| # match this particular string. When booted in standalone mode, userboot |
| # prints this message after the initial process exits iff its return_code was |
| # zero, but shutting down. This string includes some random data that |
| # shouldn't appear elsewhere, to avoid false-positive matches. |
| boot_test_success_string = |
| "***Boot-test-successful!-MDd7/O65SuVZ23yGAaQG4CedYQGH9E1/58r73pSAVK0=***" |
| |
| # The default timeout for boot tests, which is also the bootup timeout for |
| # cuckoo tests. |
| default_boot_test_timeout_secs = 600 |
| |
| _emu_device_types = [ |
| device_types.aemu, |
| device_types.crosvm, |
| device_types.qemu, |
| ] |
| |
| # Helper template defining a boot test for a particular host architecture. This |
| # flexibility is needed as we wish to define one for emulators on $target_cpu |
| # hosts even when $target_cpu != $host_cpu in order to take advantage of KVM, |
| # HVF, etc. |
| # |
| # Parameters |
| # |
| # * output_name |
| # - Optional: The name of the associated test. |
| # - Default: target_name |
| # |
| # * cpu_for_host |
| # - Required: The host CPU to generate the test for. |
| # |
| # * data_deps, deps |
| # - Optional: The usual GN meaning. |
| # |
| # See boot_test() for all other parameters. |
| template("_boot_test") { |
| main_target = target_name |
| script_target = "_boot_test.${target_name}.create_script" |
| |
| toolchain = "//build/toolchain:host_${invoker.cpu_for_host}" |
| test_script = "$root_out_dir/$main_target.sh" |
| |
| timeout_secs = default_boot_test_timeout_secs |
| if (defined(invoker.timeout)) { |
| if (invoker.timeout != false) { |
| timeout_secs = invoker.timeout |
| } |
| } |
| |
| action(script_target) { |
| visibility = [ ":*" ] |
| testonly = true |
| |
| deps = [ "//tools/testing/seriallistener($toolchain)" ] |
| inputs = [ get_label_info(deps[0], "root_out_dir") + "/seriallistener" ] |
| outputs = [ test_script ] |
| |
| script = "//build/testing/create_test.sh" |
| args = [ |
| rebase_path(outputs[0], root_build_dir), |
| rebase_path(inputs[0], root_build_dir), |
| "-success-str", |
| boot_test_success_string, |
| "-failure-str", |
| "FAILED TEST", |
| "-timeout", |
| "${timeout_secs}s", |
| ] |
| |
| metadata = { |
| test_runtime_deps = [] |
| if (defined(invoker.metadata)) { |
| forward_variables_from(invoker.metadata, "*") |
| } |
| test_runtime_deps += inputs + outputs |
| } |
| } |
| |
| test_spec(main_target) { |
| target = get_label_info(invoker.label, "label_with_toolchain") |
| name = target_name |
| if (defined(invoker.output_name)) { |
| name = invoker.output_name |
| } |
| path = test_script |
| os = host_os |
| cpu = invoker.cpu_for_host |
| isolated = true |
| is_boot_test = true |
| product_bundle = name |
| expects_ssh = false |
| |
| forward_variables_from(invoker, |
| [ |
| "assert_no_deps", |
| "data_deps", |
| "deps", |
| "environments", |
| "visibility", |
| ]) |
| |
| if ((defined(invoker.disabled) && invoker.disabled) || disable_boot_tests) { |
| # A disabled test is marked by an empty environments list. |
| environments = [] |
| } |
| |
| timeout_secs = timeout_secs |
| |
| if (!defined(deps)) { |
| deps = [] |
| } |
| deps += [ ":$script_target" ] |
| |
| if (defined(invoker.efi_disk)) { |
| deps += [ invoker.efi_disk ] |
| } |
| if (defined(invoker.qemu_kernel)) { |
| deps += [ invoker.qemu_kernel ] |
| } |
| if (defined(invoker.vbmeta)) { |
| deps += [ invoker.vbmeta ] |
| } |
| if (defined(invoker.zbi)) { |
| deps += [ invoker.zbi ] |
| } |
| if (defined(invoker.ramdisk)) { |
| deps += [ invoker.ramdisk ] |
| } |
| } |
| } |
| |
| # Specifies a boot test. |
| # |
| # A boot test is a general category of test defined by booting select images on |
| # a device and declaring success if a certain magic string is written by the |
| # booted system. This set-up allows us to execute test logic in constrained |
| # environments (e.g., in physical memory or UEFI) that lack finer command-control |
| # options for driving testing from the outside or a robust means of exfiltrating |
| # test results for later analysis. |
| # |
| # While this template does define host-side test target(s) for listening on |
| # serial for the success string, the contained logic expects to already be run |
| # after the associated system has been booted as a 'host-target interaction' |
| # test, specifically with the environment variables of `$FUCHSIA_SERIAL_SOCKET` |
| # and `$FUCHSIA_DEVICE_TYPE` set, specifying a Unix socket path from which |
| # serial can be read and a device type (as spelled in |
| # //build/testing/environments.gni). This eventually define a test that is |
| # less geared towards automation, but for now boot tests can be discovered and |
| # run locally with `fx run-boot-test`. |
| # |
| # Subtargets |
| # |
| # * $target_name.product_bundle |
| # - The associated product_bundle() target, creating a testing product |
| # bundle based on the images comprising the boot test. |
| # |
| # Parameters: |
| # |
| # * environments |
| # - Required: A list of environments from those defined as `*_env` variables |
| # in //build/testing/environments.gni. |
| # - Type: list of strings |
| # |
| # * zbi, ramdisk, qemu_kernel, vbmeta, dtbo, efi_disk |
| # - Optional: A label specifying a ZBI, QEMU kernel, VBMeta, dtbo, UEFI |
| # executable, or a bootable UEFI filesystem or disk image, respectively. |
| # At least one of these parameters must be set, but each on their own is |
| # optional. |
| # |
| # `ramdisk` is distinct from `zbi` in that it indicates an arbitrary, |
| # possibly non-ZBI ramdisk to be interpreted in the emulated context by |
| # QEMU kernel alone; in particular, it should be passed through the |
| # assembly machinery as-is. |
| # |
| # An `efi_disk` must provide `efi_input` metadata entry consisting of a |
| # `name`, `path` and `type`. |
| # - Type: label |
| # |
| # * timeout |
| # - Optional: The test's timeout, in seconds. |
| # TODO(ihuh): Once we have more data, we can override this with a more |
| # sensible timeout for each test. |
| # - Type: int |
| # - Default: 600 (10 minutes) |
| # |
| # * disabled |
| # - Optional: When true, the generated image is considered disabled, that |
| # is any associated test should not be run automatically. Useful for |
| # generating build artifacts. |
| # - Type: boolean |
| # - Default: `disable_boot_tests` |
| # |
| # * assert_no_deps, data_deps, metadata, visibility |
| # - Optional: Usual GN meanings. |
| # |
| # Metadata Protocol: |
| # |
| # * efi_input |
| # - Optional: Protocol for identifying the presence of a EFI disk, that shall be used |
| # instead of a ZBI. Using `efi_input` data key and `efi_input_barrier`, a single entry must |
| # be provided. This entry will be used to generated the partition configuration for the product |
| # bundle of this `boot_test()`. |
| # - Type: list[scope] |
| # * name |
| # - Required: partition name assigned to the bootloader partition entries in the partitionconfig. |
| # - Type: string |
| # * path |
| # - Required: relative path to $root_build_dir of the image of the partition. |
| # - Type: string |
| # * type |
| # - Required: type of the partition as defined in the bootloader partition entry. |
| # - Type: string |
| # |
| template("boot_test") { |
| assert(defined(invoker.environments), |
| "boot_test(\"$target_name\") must define `environments`") |
| assert( |
| defined(invoker.zbi) || defined(invoker.ramdisk) || |
| defined(invoker.qemu_kernel) || defined(invoker.vbmeta) || |
| defined(invoker.efi_disk) || defined(invoker.dtbo), |
| "boot_test(\"$target_name\") must define at least one of `zbi`, `ramdisk`, `qemu_kernel`, `vbmeta`, `efi_disk`, `dtbo`") |
| |
| main_target = target_name |
| product_bundle_target = "$target_name.product_bundle" |
| |
| # If the boot test specifies an EFI disk image, then this should be regarded |
| # as the contents of a bootloader partition. Otherwise, default to the |
| # board-specific partition contents. |
| if (defined(invoker.efi_disk)) { |
| efi_disk_manifest_target = "_boot_test.${target_name}.efi_disk_manifest" |
| efi_disk_manifest = "${target_gen_dir}/${main_target}.efi_disks.json" |
| generated_file(efi_disk_manifest_target) { |
| testonly = true |
| |
| data_keys = [ "efi_input" ] |
| walk_keys = [ "efi_input_barrier" ] |
| output_conversion = "json" |
| |
| deps = [ invoker.efi_disk ] |
| outputs = [ efi_disk_manifest ] |
| } |
| |
| partition_config_intermediate_path = |
| "${target_out_dir}/${main_target}.partition_config_intermediate.json" |
| partition_config_intermediate_target = |
| "_boot_test.${target_name}.create_efi_partition_config_intermediate" |
| action(partition_config_intermediate_target) { |
| testonly = true |
| |
| sources = [ efi_disk_manifest ] |
| outputs = [ partition_config_intermediate_path ] |
| |
| script = "//build/testing/boot_tests/create_efi_partition_config.py" |
| args = [ |
| "--metadata", |
| rebase_path(sources[0], root_build_dir), |
| "--output", |
| rebase_path(outputs[0], root_build_dir), |
| "--hardware-revision", |
| current_cpu, |
| ] |
| |
| deps = [ ":$efi_disk_manifest_target" ] |
| } |
| |
| partition_config_path = "${target_out_dir}/${main_target}.partitions_config" |
| partition_config_target = "${main_target}.partitions_config" |
| compiled_action(partition_config_target) { |
| testonly = true |
| |
| # The contents of these folders are dynamic, and managed entirely by this |
| # action. Further, this action will need to delete items from these |
| # directories that are not added back (on an incremental build, if an item |
| # is removed from one of these sets) |
| hermetic_action_ignored_prefixes = [ partition_config_path ] |
| |
| tool = "//build/assembly/tools/assembly_config" |
| tool_output_name = "assembly_config" |
| |
| depfile_path = "$target_out_dir/$target_name.depfile" |
| depfile = depfile_path |
| |
| outputs = [ "$partition_config_path/partitions_config.json" ] |
| inputs = [ partition_config_intermediate_path ] |
| |
| args = [ |
| "generate", |
| "partitions", |
| "--config", |
| rebase_path(inputs[0], root_build_dir), |
| "--output", |
| rebase_path(partition_config_path, root_build_dir), |
| "--depfile", |
| rebase_path(depfile_path, root_build_dir), |
| ] |
| |
| deps = [ ":$partition_config_intermediate_target" ] |
| } |
| |
| # Take the existing board, and shove the new partitions config in. |
| board_config_target = "_boot_test.${target_name}.create_board_config" |
| recovery_board_config_target = "${board_config_target}.recovery" |
| hybrid_board_configuration(board_config_target) { |
| board_config = board_configuration_label |
| replace_partitions_config = ":$partition_config_target" |
| } |
| hybrid_board_configuration(recovery_board_config_target) { |
| board_config = recovery_board_configuration_label |
| replace_partitions_config = ":$partition_config_target" |
| } |
| _board_config_label = ":${board_config_target}" |
| _recovery_board_config_label = ":${recovery_board_config_target}" |
| } else { |
| # Configurations that don't declare a board would fail to generate |
| # a hybrid board below. Passing an invalid board to assembled_system |
| # is alright for GN as long as it isn't built. |
| _board_config_label = board_configuration_label |
| _recovery_board_config_label = recovery_board_configuration_label |
| } |
| |
| if (current_toolchain == default_toolchain) { |
| assembled_system_target = "_$main_target.assembled_system" |
| recovery_assembled_system_target = "${assembled_system_target}.recovery" |
| |
| assembly_mode = "test-zbi" |
| assembled_system_kernel_zbi = "//build/testing/boot_tests:empty-zbi" |
| signable_ramdisk = true |
| if (defined(invoker.zbi)) { |
| assembled_system_kernel_zbi = invoker.zbi |
| } else if (defined(invoker.ramdisk)) { |
| assembly_mode = "test-ramdisk" |
| assembled_system_kernel_zbi = invoker.ramdisk |
| signable_ramdisk = false # Per test-ramdisk semantics |
| } |
| |
| assembled_system(assembled_system_target) { |
| visibility = [ ":*" ] |
| testonly = true |
| image_name = "fuchsia" |
| namespace = assembled_system_target |
| board_config_label = _board_config_label |
| product_assembly_config_label = |
| "//build/testing/boot_tests:product_config" |
| kernel_zbi = assembled_system_kernel_zbi |
| generate_vbmeta = use_vbmeta |
| assembly_mode = assembly_mode |
| |
| generate_signed_zbi = false |
| if (signable_ramdisk) { |
| if (custom_signing_script != "") { |
| inputs = custom_signing_script_inputs |
| deps = [ "//build/images/custom_signing:deps" ] |
| generate_signed_zbi = true |
| } else if (use_vboot) { |
| inputs = vboot_action.inputs |
| generate_signed_zbi = true |
| } |
| } |
| forward_variables_from(invoker, [ "qemu_kernel" ]) |
| } |
| |
| # Mirrors ${assembled_system_target} but using the recovery board config. |
| # |
| # Recovery isn't important for the test itself so the contents are largely |
| # irrelevant, but some boards require a valid image in the recovery slot |
| # in order to boot (b/450614346). |
| assembled_system(recovery_assembled_system_target) { |
| visibility = [ ":*" ] |
| testonly = true |
| image_name = "fuchsia" |
| namespace = recovery_assembled_system_target |
| |
| # Must use the recovery board config to ensure the final artifact is a |
| # valid recovery image. |
| board_config_label = _recovery_board_config_label |
| |
| # Product and ZBI contents can be the same as the main target. |
| product_assembly_config_label = |
| "//build/testing/boot_tests:product_config" |
| kernel_zbi = assembled_system_kernel_zbi |
| generate_vbmeta = use_vbmeta |
| assembly_mode = assembly_mode |
| |
| generate_signed_zbi = false |
| if (signable_ramdisk) { |
| if (custom_signing_script != "") { |
| inputs = custom_signing_script_inputs |
| deps = [ "//build/images/custom_signing:deps" ] |
| generate_signed_zbi = true |
| } else if (use_vboot) { |
| inputs = vboot_action.inputs |
| generate_signed_zbi = true |
| } |
| } |
| forward_variables_from(invoker, [ "qemu_kernel" ]) |
| } |
| |
| product_bundle(product_bundle_target) { |
| testonly = true |
| |
| name = main_target |
| system_a = "$target_out_dir/$assembled_system_target" |
| system_r = "$target_out_dir/$recovery_assembled_system_target" |
| |
| deps = [ |
| ":$assembled_system_target", |
| ":$recovery_assembled_system_target", |
| ] |
| } |
| } else { |
| not_needed([ |
| "_board_config_label", |
| "_recovery_board_config_label", |
| ]) |
| group(product_bundle_target) { |
| testonly = true |
| deps = [ ":${product_bundle_target}($default_toolchain)" ] |
| } |
| } |
| |
| # If host_cpu != target_cpu, then we make sure to define a separate boot |
| # test for a target_cpu host in order to take advantage of KVM, HVF, etc. |
| # |
| # TODO(mcgrathr): No riscv64 hosts are available yet. |
| if (host_cpu != target_cpu && target_cpu != "riscv64") { |
| target_cpu_envs = [] |
| foreach(env, invoker.environments) { |
| # Clear from any previous iteration. |
| dims = { |
| } |
| dims = env.dimensions |
| |
| # Things get more complicated if we permit environment dimensions to |
| # specify their own architecture to run on. We don't have a use-case for |
| # that, so we bar it from happening for now. |
| assert(!defined(dims.cpu)) |
| |
| device_type = dims.device_type |
| if ([ device_type ] + _emu_device_types - _emu_device_types == []) { |
| accel = true |
| if (defined(env.emulator)) { |
| # Clear from any previous iteration. |
| emulator_spec = { |
| } |
| emulator_spec = env.emulator |
| accel = !defined(emulator_spec.accel) || emulator_spec.accel != "none" |
| } |
| |
| # If hardware acceleration is explicitly disabled for an emulator |
| # environment, no point in running it on a target_cpu host. |
| if (accel) { |
| target_cpu_envs += [ env ] |
| } |
| } |
| } |
| host_cpu_envs = invoker.environments - target_cpu_envs |
| |
| common_params = { |
| forward_variables_from(invoker, |
| "*", |
| [ |
| "assert_no_deps", |
| "environments", |
| "visibility", |
| ]) |
| visibility = [ ":*" ] |
| output_name = main_target |
| label = ":$main_target" |
| deps = [ ":$product_bundle_target" ] |
| } |
| |
| _boot_test("$main_target.host-$target_cpu") { |
| cpu_for_host = target_cpu |
| environments = target_cpu_envs |
| forward_variables_from(common_params, "*") |
| } |
| |
| _boot_test("$main_target.host-$host_cpu") { |
| cpu_for_host = host_cpu |
| environments = host_cpu_envs |
| forward_variables_from(common_params, "*") |
| } |
| |
| group(main_target) { |
| forward_variables_from(invoker, |
| [ |
| "assert_no_deps", |
| "visibility", |
| ]) |
| testonly = true |
| deps = [ |
| ":$main_target.host-$host_cpu", |
| ":$main_target.host-$target_cpu", |
| ] |
| } |
| } else { |
| _boot_test(main_target) { |
| label = ":$main_target" |
| forward_variables_from(invoker, "*") |
| cpu_for_host = host_cpu |
| deps = [ ":$product_bundle_target" ] |
| } |
| } |
| } |