ceedling 1.2.0 → 1.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/GIT_COMMIT_SHA +1 -1
- data/README.md +57 -58
- data/Rakefile +5 -3
- data/assets/features/ceedling +1 -1
- data/assets/fixtures/test_source_file_duplicate_basenames/test_calc_typo.c +14 -0
- data/assets/fixtures/tests_with_duplicate_test_names/test_foo.c +21 -0
- data/bin/actionator.rb +6 -12
- data/bin/app_cfg.rb +35 -1
- data/bin/ceedling +13 -3
- data/bin/cli.rb +25 -6
- data/bin/cli_handler.rb +119 -73
- data/bin/cli_helper.rb +56 -78
- data/bin/installation_selector.rb +81 -0
- data/bin/launchinator.rb +69 -0
- data/bin/mixin_resolvinator.rb +26 -36
- data/bin/objects.yml +13 -0
- data/bin/path_validator.rb +25 -29
- data/bin/versionator.rb +47 -48
- data/ceedling.gemspec +19 -15
- data/docs/BreakingChanges.md +70 -1
- data/docs/Changelog.md +84 -3
- data/docs/KnownIssues.md +90 -4
- data/docs/ReleaseNotes.md +13 -0
- data/docs/overrides/main.html +11 -8
- data/lib/ceedling/build_log_writer.rb +33 -0
- data/lib/ceedling/config/build_root_policy.rb +70 -0
- data/lib/ceedling/config/configurator.rb +19 -18
- data/lib/ceedling/config/configurator_builder.rb +58 -28
- data/lib/ceedling/config/configurator_paths_validator.rb +179 -0
- data/lib/ceedling/config/configurator_plugins.rb +1 -1
- data/lib/ceedling/config/configurator_setup.rb +39 -41
- data/lib/ceedling/config/configurator_validator.rb +1 -110
- data/lib/ceedling/defaults.rb +4 -4
- data/lib/ceedling/dependencies/README.md +18 -16
- data/lib/ceedling/dependencies/dependency_cache_store.rb +20 -3
- data/lib/ceedling/dependencies/dependency_hasher.rb +35 -1
- data/lib/ceedling/filename_extension.rb +7 -1
- data/lib/ceedling/filesystem/FILE_TREE.md +253 -0
- data/lib/ceedling/filesystem/PATH_HANDLING.md +304 -0
- data/lib/ceedling/filesystem/README.md +41 -0
- data/lib/ceedling/filesystem/build_subpath.rb +102 -0
- data/lib/ceedling/filesystem/destination_collision.rb +25 -0
- data/lib/ceedling/{file_finder.rb → filesystem/file_finder.rb} +159 -104
- data/lib/ceedling/{file_finder_helper.rb → filesystem/file_finder_helper.rb} +36 -31
- data/lib/ceedling/filesystem/file_path_collection_utils.rb +183 -0
- data/lib/ceedling/{file_path_utils.rb → filesystem/file_path_utils.rb} +108 -97
- data/lib/ceedling/{file_system_wrapper.rb → filesystem/file_system_wrapper.rb} +6 -5
- data/lib/ceedling/filesystem/file_tree.rb +241 -0
- data/lib/ceedling/{file_wrapper.rb → filesystem/file_wrapper.rb} +65 -28
- data/lib/ceedling/filesystem/path_exclusions.rb +30 -0
- data/lib/ceedling/{path_matcher.rb → filesystem/path_matcher.rb} +27 -23
- data/lib/ceedling/{path_mirror.rb → filesystem/path_mirror.rb} +19 -18
- data/lib/ceedling/filesystem/path_spellings.rb +55 -0
- data/lib/ceedling/filesystem/project_paths.rb +184 -0
- data/lib/ceedling/filesystem/working_dir.rb +40 -0
- data/lib/ceedling/generators/generator.rb +5 -6
- data/lib/ceedling/generators/generator_partials.rb +1 -1
- data/lib/ceedling/generators/generator_test_results_backtrace.rb +7 -7
- data/lib/ceedling/handoff.rb +121 -0
- data/lib/ceedling/includes/include_factory.rb +3 -3
- data/lib/ceedling/includes/include_pathinator.rb +67 -10
- data/lib/ceedling/includes/includes.rb +1 -1
- data/lib/ceedling/installation.rb +92 -0
- data/lib/ceedling/library_resolver.rb +99 -0
- data/lib/ceedling/objects.yml +23 -2
- data/lib/ceedling/partials/partializer.rb +4 -4
- data/lib/ceedling/partials/partializer_config.rb +3 -1
- data/lib/ceedling/plugins/report_log_writer_plugin.rb +3 -5
- data/lib/ceedling/preprocess/preprocessinator_file_assembler.rb +1 -1
- data/lib/ceedling/preprocess/preprocessinator_includes_handler.rb +1 -1
- data/lib/ceedling/preprocess/preprocessinator_line_marker_includes_extractor.rb +1 -1
- data/lib/ceedling/rake_app/rakefile_component_resolver.rb +10 -17
- data/lib/ceedling/rake_app/test_task_name.rb +29 -0
- data/lib/ceedling/rakefile.rb +16 -1
- data/lib/ceedling/rakefiles/base/tasks_base.rake +1 -1
- data/lib/ceedling/rakefiles/release/rules_release.rake +4 -7
- data/lib/ceedling/rakefiles/tests/rules_tests.rake +3 -20
- data/lib/ceedling/rakefiles/tests/rules_tests_generate.rake +4 -30
- data/lib/ceedling/release_invoker/release_build_executor.rb +33 -44
- data/lib/ceedling/release_invoker/release_build_planner.rb +14 -6
- data/lib/ceedling/reportinator.rb +3 -4
- data/lib/ceedling/setupinator.rb +95 -87
- data/lib/ceedling/test_context_extractor.rb +5 -1
- data/lib/ceedling/test_invoker/PIPELINE.md +9 -7
- data/lib/ceedling/test_invoker/mock_locator.rb +60 -2
- data/lib/ceedling/test_invoker/partials_manager.rb +12 -0
- data/lib/ceedling/test_invoker/standin_headerinator.rb +22 -13
- data/lib/ceedling/test_invoker/test_build_executor.rb +42 -41
- data/lib/ceedling/test_invoker/test_build_planner.rb +37 -18
- data/lib/ceedling/test_invoker/test_build_setup.rb +17 -12
- data/lib/ceedling/test_invoker/test_build_validations.rb +16 -31
- data/lib/ceedling/test_invoker/test_file_preprocessinator.rb +1 -1
- data/lib/ceedling/test_invoker/test_invoker.rb +6 -1
- data/lib/ceedling/test_invoker/test_pipeline_helpers.rb +1 -1
- data/lib/ceedling/test_invoker/test_source_file_directive_resolver.rb +40 -20
- data/lib/ceedling/tool_executor.rb +9 -3
- data/lib/ceedling/yaml_wrapper.rb +3 -2
- data/lib/version.rb +1 -1
- data/plugins/bullseye/bullseye.rake +6 -9
- data/plugins/bullseye/config/defaults_bullseye.rb +1 -1
- data/plugins/bullseye/lib/bullseye.rb +9 -7
- data/plugins/cppcheck/config/defaults_cppcheck.rb +1 -1
- data/plugins/dependencies/config/defaults.yml +6 -6
- data/plugins/dependencies/dependencies.rake +4 -1
- data/plugins/dependencies/lib/dependencies.rb +18 -8
- data/plugins/dependencies/lib/dependency_spec.rb +17 -5
- data/plugins/fff/lib/fff.rb +45 -39
- data/plugins/fff/spec/fff_cmock_wrapper_spec.rb +128 -0
- data/plugins/gcov/config/defaults_gcov.rb +1 -1
- data/plugins/gcov/gcov.rake +6 -9
- data/plugins/gcov/lib/console_reportinator.rb +4 -18
- data/plugins/gcov/lib/gcov.rb +9 -2
- data/plugins/gcov/lib/gcovr_reportinator.rb +8 -40
- data/plugins/gcov/lib/gcovr_thresholds.rb +74 -0
- data/plugins/gcov/lib/reportgenerator_reportinator.rb +1 -1
- data/plugins/module_generator/lib/module_generator.rb +3 -3
- data/plugins/report_tests_teamcity_stdout/lib/report_tests_teamcity_stdout.rb +53 -80
- data/plugins/sanitize/lib/sanitize_records.rb +3 -6
- data/plugins/sanitize/sanitize.rake +6 -6
- data/plugins/valgrind/lib/valgrind.rb +14 -7
- data/plugins/valgrind/valgrind.rake +6 -6
- data/site-local/404.html +6 -20
- data/site-local/assets/javascripts/{bundle.d7400e89.min.js → bundle.c21346df.min.js} +3 -3
- data/site-local/assets/javascripts/{bundle.d7400e89.min.js.map → bundle.c21346df.min.js.map} +2 -2
- data/site-local/assets/javascripts/workers/{search.2c215733.min.js → search.a7633da7.min.js} +2 -2
- data/site-local/assets/javascripts/workers/{search.2c215733.min.js.map → search.a7633da7.min.js.map} +1 -1
- data/site-local/assets/stylesheets/main.57a0d79d.min.css +6 -0
- data/site-local/assets/stylesheets/main.57a0d79d.min.css.map +1 -0
- data/site-local/assets/stylesheets/palette.fb2fffe2.min.css +4 -0
- data/site-local/assets/stylesheets/palette.fb2fffe2.min.css.map +1 -0
- data/site-local/configuration/environment-vars.html +6 -20
- data/site-local/configuration/global-collections.html +6 -20
- data/site-local/configuration/index.html +6 -20
- data/site-local/configuration/loading.html +6 -20
- data/site-local/configuration/mixins.html +10 -24
- data/site-local/configuration/parallel-builds.html +6 -20
- data/site-local/configuration/project-file.html +8 -22
- data/site-local/configuration/reference/cexception.html +6 -20
- data/site-local/configuration/reference/cmock.html +8 -21
- data/site-local/configuration/reference/defines.html +16 -30
- data/site-local/configuration/reference/environment.html +10 -24
- data/site-local/configuration/reference/extension.html +6 -20
- data/site-local/configuration/reference/files.html +22 -30
- data/site-local/configuration/reference/flags.html +13 -27
- data/site-local/configuration/reference/index.html +6 -20
- data/site-local/configuration/reference/libraries.html +27 -23
- data/site-local/configuration/reference/mixins.html +6 -20
- data/site-local/configuration/reference/partials.html +6 -20
- data/site-local/configuration/reference/paths.html +38 -29
- data/site-local/configuration/reference/plugins.html +6 -20
- data/site-local/configuration/reference/project.html +32 -36
- data/site-local/configuration/reference/release-build.html +8 -20
- data/site-local/configuration/reference/test-build.html +6 -20
- data/site-local/configuration/reference/test-runner.html +6 -20
- data/site-local/configuration/reference/tools.html +27 -32
- data/site-local/configuration/reference/unity.html +6 -20
- data/site-local/configuration/which-ceedling.html +383 -37
- data/site-local/development/index.html +6 -20
- data/site-local/development/plugins/configuration.html +6 -20
- data/site-local/development/plugins/index.html +6 -20
- data/site-local/development/plugins/plugin-subclass.html +54 -68
- data/site-local/development/plugins/rake-tasks.html +8 -22
- data/site-local/development/releases.html +84 -29
- data/site-local/development/workflow.html +62 -71
- data/site-local/getting-started/builds.html +8 -20
- data/site-local/getting-started/command-line.html +62 -24
- data/site-local/getting-started/example-projects/cipher-quest.html +6 -20
- data/site-local/getting-started/example-projects/index.html +6 -20
- data/site-local/getting-started/example-projects/temp-sensor.html +6 -20
- data/site-local/getting-started/example-projects/wondrous-forest.html +6 -20
- data/site-local/getting-started/index.html +6 -20
- data/site-local/getting-started/installation.html +6 -20
- data/site-local/getting-started/quick-start.html +6 -20
- data/site-local/help.html +6 -20
- data/site-local/index.html +11 -24
- data/site-local/overview/build-system.html +6 -20
- data/site-local/overview/index.html +6 -20
- data/site-local/overview/test-environments.html +6 -20
- data/site-local/overview/tools-and-frameworks.html +6 -20
- data/site-local/plugins/beep.html +6 -20
- data/site-local/plugins/bullseye/index.html +13 -24
- data/site-local/plugins/bullseye/licensing.html +14 -28
- data/site-local/plugins/command-hooks.html +8 -22
- data/site-local/plugins/compile-commands-json-db.html +6 -20
- data/site-local/plugins/cppcheck.html +8 -22
- data/site-local/plugins/dependencies.html +16 -27
- data/site-local/plugins/fff.html +49 -32
- data/site-local/plugins/gcov/examples.html +7 -21
- data/site-local/plugins/gcov/gcovr.html +22 -33
- data/site-local/plugins/gcov/index.html +6 -20
- data/site-local/plugins/gcov/overview.html +9 -20
- data/site-local/plugins/gcov/reportgenerator.html +9 -23
- data/site-local/plugins/gcov/reporting.html +6 -20
- data/site-local/plugins/gcov/setup.html +8 -21
- data/site-local/plugins/gcov/tool-versions.html +6 -20
- data/site-local/plugins/gcov/troubleshooting.html +10 -24
- data/site-local/plugins/index.html +6 -20
- data/site-local/plugins/module-generator.html +21 -35
- data/site-local/plugins/report-build-warnings-log.html +53 -67
- data/site-local/plugins/report-tests-gtestlike-stdout.html +6 -20
- data/site-local/plugins/report-tests-ide-stdout.html +43 -57
- data/site-local/plugins/report-tests-log-factory.html +50 -64
- data/site-local/plugins/report-tests-pretty-stdout.html +7 -21
- data/site-local/plugins/report-tests-raw-output-log.html +9 -21
- data/site-local/plugins/report-tests-teamcity-stdout.html +20 -31
- data/site-local/plugins/sanitize.html +56 -65
- data/site-local/plugins/valgrind.html +49 -58
- data/site-local/project/index.html +6 -20
- data/site-local/project/release-history.html +6 -20
- data/site-local/project/sbom.html +6 -20
- data/site-local/project/upgrade.html +9 -20
- data/site-local/reference/build-directives.html +6 -20
- data/site-local/reference/command-line.html +15 -22
- data/site-local/reference/environment-vars.html +6 -20
- data/site-local/reference/gcov-plugin.html +6 -20
- data/site-local/reference/global-collections.html +6 -20
- data/site-local/reference/index.html +6 -20
- data/site-local/reference/partials-macros.html +6 -20
- data/site-local/reference/project-configuration.html +6 -20
- data/site-local/sitemap.xml.gz +0 -0
- data/site-local/snapshot/examples/cipher_quest/index.html +16 -30
- data/site-local/snapshot/examples/temp_sensor/index.html +8 -22
- data/site-local/snapshot/examples/wondrous_forest/index.html +6 -20
- data/site-local/snapshot/plugins/bullseye/config/defaults_bullseye.rb +1 -1
- data/site-local/snapshot/plugins/gcov/config/defaults_gcov.rb +1 -1
- data/site-local/testing-guide/build-directives.html +14 -28
- data/site-local/testing-guide/conventions.html +66 -27
- data/site-local/testing-guide/frameworks.html +11 -25
- data/site-local/testing-guide/index.html +6 -20
- data/site-local/testing-guide/partials/configuration.html +8 -22
- data/site-local/testing-guide/partials/conventions.html +8 -22
- data/site-local/testing-guide/partials/directives.html +16 -30
- data/site-local/testing-guide/partials/example.html +14 -28
- data/site-local/testing-guide/partials/index.html +6 -20
- data/site-local/testing-guide/partials/overview.html +6 -20
- data/site-local/testing-guide/partials/variables.html +11 -25
- data/site-local/testing-guide/test-cases.html +13 -27
- data/site-local/testing-guide/test-sample.html +12 -26
- data/site-local/testing-guide/test-suite-anatomy.html +6 -20
- data/spec/integration/actionator_file_operations_spec.rb +91 -1
- data/spec/integration/compile_commands_json_db_spec.rb +1 -1
- data/spec/integration/configurator_pipeline_spec.rb +10 -0
- data/spec/integration/configurator_validation_spec.rb +1 -1
- data/spec/integration/dependencies/dependency_tracker_cache_hardening_spec.rb +26 -0
- data/spec/integration/dependencies/dependency_tracker_core_spec.rb +62 -0
- data/spec/integration/launcher_handoff_spec.rb +297 -0
- data/spec/integration/mixin_loading_spec.rb +1 -1
- data/spec/integration/module_generator_spec.rb +3 -3
- data/spec/integration/nested_test_artifacts_spec.rb +99 -0
- data/spec/integration/partials_module_resolution_spec.rb +7 -4
- data/spec/integration/path_intake_scenarios_spec.rb +101 -0
- data/spec/integration/path_scenarios_spec.rb +322 -0
- data/spec/integration/quote_include_isolator_spec.rb +1 -1
- data/spec/integration/rake_task_registry_plugins_spec.rb +95 -0
- data/spec/integration/report_build_warnings_log_spec.rb +1 -1
- data/spec/integration/report_tests_log_factory_spec.rb +1 -1
- data/spec/integration/test_executable_manifest_spec.rb +1 -1
- data/spec/integration/test_includes_extractinator_spec.rb +2 -1
- data/spec/integration/test_naming_spec.rb +198 -0
- data/spec/integration/test_source_file_checks_spec.rb +166 -0
- data/spec/integration/tool_validator_spec.rb +1 -1
- data/spec/integration/upgrade_docs_spec.rb +84 -0
- data/spec/integration/vendor_copy_transient_io_retry_spec.rb +192 -75
- data/spec/support/file_tree_helper.rb +88 -0
- data/spec/support/integration/configurator_integration_helper.rb +10 -7
- data/spec/support/integration/dependency_tracker_integration_helper.rb +1 -1
- data/spec/support/integration/partials_generation_integration_helper.rb +2 -2
- data/spec/support/integration/path_scenario_harness.rb +173 -0
- data/spec/support/integration/spec_integration_helper.rb +1 -1
- data/spec/support/integration/test_includes_harness.rb +13 -2
- data/spec/support/spec_helper.rb +2 -0
- data/spec/support/system/spec_system_helper.rb +7 -6
- data/spec/support/system/system_context.rb +21 -3
- data/spec/support/working_dir_tracking.rb +49 -0
- data/spec/system/build_output_mirroring_spec.rb +2 -2
- data/spec/system/conditional_mock_includes_spec.rb +1 -1
- data/spec/system/delta_builds_spec.rb +137 -11
- data/spec/system/gcov_deployment_spec.rb +1 -0
- data/spec/system/header_include_path_validation_spec.rb +4 -4
- data/spec/system/launcher_handoff_spec.rb +82 -0
- data/spec/system/mock_include_folders_spec.rb +91 -0
- data/spec/system/parent_directory_reference_spec.rb +5 -3
- data/spec/system/path_handling_probes_spec.rb +192 -0
- data/spec/system/preprocessing_parameterized_tests_spec.rb +1 -1
- data/spec/system/rake_release_task_path_disambiguation_spec.rb +6 -6
- data/spec/system/rake_test_task_path_disambiguation_spec.rb +85 -0
- data/spec/system/special_path_characters_spec.rb +86 -0
- data/spec/system/support/common_test_cases.rb +23 -21
- data/spec/system/support/gcov_common_test_cases.rb +37 -9
- data/spec/system/support/sanitize_common_test_cases.rb +2 -2
- data/spec/system/support/valgrind_common_test_cases.rb +12 -12
- data/spec/system/test_pipeline_stop_points_spec.rb +4 -4
- data/spec/system/test_runner_system_include_spec.rb +1 -1
- data/spec/system/test_source_file_directive_spec.rb +34 -5
- data/spec/units/bin/actionator_spec.rb +14 -17
- data/spec/units/bin/app_cfg_spec.rb +47 -0
- data/spec/units/bin/cli_handler_spec.rb +284 -7
- data/spec/units/bin/cli_helper_spec.rb +126 -45
- data/spec/units/bin/installation_selector_spec.rb +114 -0
- data/spec/units/bin/launchinator_spec.rb +171 -0
- data/spec/units/bin/path_validator_spec.rb +10 -0
- data/spec/units/bin/versionator_spec.rb +38 -6
- data/spec/units/build_log_writer_spec.rb +57 -0
- data/spec/units/config/build_root_policy_spec.rb +61 -0
- data/spec/units/config/configurator_builder_spec.rb +37 -6
- data/spec/units/config/configurator_paths_validator_spec.rb +292 -0
- data/spec/units/config/configurator_setup_spec.rb +65 -103
- data/spec/units/config/configurator_spec.rb +37 -6
- data/spec/units/config/configurator_validator_spec.rb +1 -172
- data/spec/units/default_tools_quoting_spec.rb +83 -0
- data/spec/units/dependencies/dependency_cache_store_spec.rb +33 -2
- data/spec/units/dependencies/dependency_debug_tree_spec.rb +1 -1
- data/spec/units/dependencies/dependency_hasher_spec.rb +42 -1
- data/spec/units/dependencies/dependency_path_normalizer_spec.rb +1 -1
- data/spec/units/dependencies/dependency_tracker_spec.rb +3 -1
- data/spec/units/filename_extension_spec.rb +11 -0
- data/spec/units/filesystem/build_subpath_spec.rb +122 -0
- data/spec/units/filesystem/destination_collision_spec.rb +30 -0
- data/spec/units/{file_finder_helper_spec.rb → filesystem/file_finder_helper_spec.rb} +46 -1
- data/spec/units/{file_finder_spec.rb → filesystem/file_finder_spec.rb} +188 -22
- data/spec/units/{file_path_collection_utils_spec.rb → filesystem/file_path_collection_utils_spec.rb} +181 -1
- data/spec/units/{file_path_utils_spec.rb → filesystem/file_path_utils_spec.rb} +204 -87
- data/spec/units/filesystem/file_system_wrapper_spec.rb +33 -0
- data/spec/units/filesystem/file_tree_spec.rb +511 -0
- data/spec/units/filesystem/file_tree_windows_spec.rb +78 -0
- data/spec/units/{file_wrapper_spec.rb → filesystem/file_wrapper_spec.rb} +154 -32
- data/spec/units/{path_matcher_spec.rb → filesystem/path_matcher_spec.rb} +12 -1
- data/spec/units/{path_mirror_spec.rb → filesystem/path_mirror_spec.rb} +32 -4
- data/spec/units/filesystem/project_paths_spec.rb +147 -0
- data/spec/units/filesystem/working_dir_spec.rb +71 -0
- data/spec/units/generators/generator_spec.rb +12 -6
- data/spec/units/generators/generator_test_results_backtrace_spec.rb +50 -46
- data/spec/units/generators/generator_test_results_spec.rb +1 -1
- data/spec/units/handoff_spec.rb +205 -0
- data/spec/units/includes/include_factory_spec.rb +3 -3
- data/spec/units/includes/include_pathinator_spec.rb +46 -0
- data/spec/units/installation_spec.rb +228 -0
- data/spec/units/library_resolver_spec.rb +157 -0
- data/spec/units/mock_locator_spec.rb +76 -1
- data/spec/units/partials/partializer_config_spec.rb +7 -0
- data/spec/units/partials/partializer_spec.rb +1 -0
- data/spec/units/partials_manager_spec.rb +17 -0
- data/spec/units/plugins/bullseye_spec.rb +15 -2
- data/spec/units/plugins/console_reportinator_spec.rb +18 -20
- data/spec/units/plugins/dependencies_spec.rb +33 -7
- data/spec/units/plugins/dependency_spec_spec.rb +17 -0
- data/spec/units/plugins/gcov_spec.rb +28 -3
- data/spec/units/plugins/gcovr_reportinator_spec.rb +19 -47
- data/spec/units/plugins/gcovr_thresholds_spec.rb +95 -0
- data/spec/units/plugins/module_generator_spec.rb +5 -5
- data/spec/units/plugins/report_build_warnings_log_spec.rb +3 -3
- data/spec/units/plugins/report_log_writer_plugin_spec.rb +10 -1
- data/spec/units/plugins/report_tests_raw_output_log_spec.rb +13 -2
- data/spec/units/plugins/report_tests_teamcity_stdout_spec.rb +77 -3
- data/spec/units/plugins/reportgenerator_reportinator_spec.rb +1 -1
- data/spec/units/plugins/sanitize_records_spec.rb +3 -1
- data/spec/units/plugins/sanitize_spec.rb +1 -1
- data/spec/units/plugins/valgrind_spec.rb +23 -8
- data/spec/units/preprocess/preprocessinator_comment_stripper_spec.rb +1 -1
- data/spec/units/rake_app/rakefile_component_resolver_spec.rb +21 -0
- data/spec/units/rake_app/test_task_name_spec.rb +47 -0
- data/spec/units/release_build_executor_spec.rb +53 -14
- data/spec/units/release_build_planner_spec.rb +15 -3
- data/spec/units/reportinator_spec.rb +5 -5
- data/spec/units/standin_headerinator_spec.rb +31 -1
- data/spec/units/test_build_executor_spec.rb +107 -5
- data/spec/units/test_build_planner_spec.rb +104 -2
- data/spec/units/test_build_setup_spec.rb +96 -12
- data/spec/units/test_context_extractor_spec.rb +25 -0
- data/spec/units/test_file_preprocessinator_spec.rb +17 -1
- data/spec/units/test_includes_extractinator_spec.rb +5 -1
- data/spec/units/test_invoker/objects_wiring_spec.rb +6 -3
- data/spec/units/test_invoker_spec.rb +14 -1
- data/spec/units/test_source_file_directive_resolver_spec.rb +38 -7
- data/spec/units/tool_executor_spec.rb +26 -0
- data/spec/units/yaml_wrapper_spec.rb +11 -0
- metadata +122 -33
- data/lib/ceedling/file_path_collection_utils.rb +0 -164
- data/site-local/assets/stylesheets/main.ec1eaa64.min.css +0 -1
- data/site-local/assets/stylesheets/main.ec1eaa64.min.css.map +0 -1
- data/site-local/assets/stylesheets/palette.ab4e12ef.min.css +0 -1
- data/site-local/assets/stylesheets/palette.ab4e12ef.min.css.map +0 -1
data/lib/ceedling/defaults.rb
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
require 'ceedling/constants'
|
|
9
9
|
require 'ceedling/system_wrapper'
|
|
10
|
-
require 'ceedling/file_path_utils'
|
|
10
|
+
require 'ceedling/filesystem/file_path_utils'
|
|
11
11
|
|
|
12
12
|
# Assign a default value for system testing where CEEDLING_APPCFG may not be present
|
|
13
13
|
# TODO: Create code config & test structure that does not internalize a test path like this
|
|
@@ -58,14 +58,14 @@ DEFAULT_TEST_LINKER_TOOL = {
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
DEFAULT_TEST_FIXTURE_TOOL = {
|
|
61
|
-
:executable => '${1}'.freeze, # Unity test runner executable
|
|
61
|
+
:executable => '"${1}"'.freeze, # Unity test runner executable, quoted as a path
|
|
62
62
|
:name => 'default_test_fixture'.freeze,
|
|
63
63
|
:optional => false.freeze,
|
|
64
64
|
:arguments => [].freeze
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
DEFAULT_TEST_FIXTURE_SIMPLE_BACKTRACE_TOOL = {
|
|
68
|
-
:executable => '${1}'.freeze, # Unity test runner executable
|
|
68
|
+
:executable => '"${1}"'.freeze, # Unity test runner executable, quoted as a path
|
|
69
69
|
:name => 'default_test_fixture_simple_backtrace'.freeze,
|
|
70
70
|
:optional => false.freeze,
|
|
71
71
|
:arguments => [
|
|
@@ -204,7 +204,7 @@ DEFAULT_TEST_BACKTRACE_GDB_TOOL = {
|
|
|
204
204
|
'--eval-command run'.freeze,
|
|
205
205
|
"--command \"${1}\"".freeze, # Debug script file to run
|
|
206
206
|
'--args'.freeze,
|
|
207
|
-
'${2}'.freeze,
|
|
207
|
+
'"${2}"'.freeze, # Test executable, quoted as a path
|
|
208
208
|
# Test case matching flag and (quoted) value, fully composed by the caller as
|
|
209
209
|
# either `-n "<exact name>"` or `-f "<base name>"` -- see
|
|
210
210
|
# GeneratorTestResultsBacktrace#unity_filter_arg.
|
|
@@ -2,29 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
This subsystem answers one question for any expensive step in either the test or release pipeline: has anything this target depends on changed since it last built successfully? It answers using content hashes rather than timestamps, so neither pipeline ever duplicates work that doesn't need to be done.
|
|
4
4
|
|
|
5
|
-
It is not itself a pipeline with sequential stages the way `test_invoker`/`release_invoker` are. It
|
|
5
|
+
It is not itself a pipeline with sequential stages the way `test_invoker`/`release_invoker` are. It’s a service both pipelines call into, over and over, as a pure oracle. It never decides *when* to run a build step or *what order* steps happen in -- that’s each pipeline’s own stage list -- it only answers, at the moment a step is about to run, whether that build step actually needs to run.
|
|
6
6
|
|
|
7
|
-
## The subsystem
|
|
7
|
+
## The subsystem’s shape
|
|
8
8
|
|
|
9
9
|
Two classes matter most, and everything else exists to serve them:
|
|
10
10
|
|
|
11
|
-
- **`Dependinator`** is the thin, Ceedling-specific facade every pipeline actually talks to. Every method it has except one is a one-line passthrough to `DependencyTracker` below; what it actually owns is the convention for where a given pipeline
|
|
12
|
-
- **`DependencyTracker`** is the actual engine, and it
|
|
11
|
+
- **`Dependinator`** is the thin, Ceedling-specific facade every pipeline actually talks to. Every method it has except one is a one-line passthrough to `DependencyTracker` below; what it actually owns is the convention for where a given pipeline’s cache file lives on disk, keyed by a symbolic identifier (`:test`, `:release`, or a plugin’s own symbol), and the vocabulary translation at `flush` time from the tracker’s own `prune:` parameter to the pipeline-facing `refresh_dependencies:` (see below). Its whole reason for existing is so pipeline code speaks in build verbs -- register this, ask if that’s stale, mark this fresh -- and never has to know anything about cache file paths or tracker internals.
|
|
12
|
+
- **`DependencyTracker`** is the actual engine, and it’s deliberately generic and application-agnostic -- nothing in it knows what a "test" or a "release" is. It holds this run’s registrations in memory and a loaded cache of the previous run’s hashes, and its whole job is answering staleness queries against the two.
|
|
13
13
|
|
|
14
14
|
`DependencyTracker`, in turn, composes six narrowly-scoped collaborators, each doing exactly one job and nothing else. Four serve every run:
|
|
15
15
|
|
|
16
16
|
- `DependencyHasher` decides what makes two things count as "the same."
|
|
17
17
|
- `DependencyPathNormalizer` decides what makes two spellings of a path count as "the same target."
|
|
18
18
|
- `DependencyCacheStore` owns the on-disk cache file itself.
|
|
19
|
-
- `GccDependencyParser` turns a compiler
|
|
19
|
+
- `GccDependencyParser` turns a compiler’s own dependency output into something registerable.
|
|
20
20
|
|
|
21
21
|
The other two, `DependencyDebugTree` and `DependencyDiffer`, serve only the troubleshooting aid described last. The sections below cover each in turn.
|
|
22
22
|
|
|
23
23
|
## The `register` → `stale?` → `mark_fresh` idiom
|
|
24
24
|
|
|
25
|
-
Every call site in both pipelines, regardless of what expensive thing it
|
|
25
|
+
Every call site in both pipelines, regardless of what expensive thing it’s guarding -- a preprocessor subprocess, a mock generation, a real compiler or linker invocation -- follows the same three-part shape. `register` tells the tracker what a target depends on: its antecedent files, and a `meta` value carrying anything else that should count toward staleness even though it isn't a file at all -- compile flags, `-D` defines, search paths, a toolchain version string, a coverage-on-or-off flag. An object file’s target is the `.o` itself; its files are its source plus whatever headers it actually pulled in; its meta is the flags and defines that, if they change, should force a recompile even though the source file itself didn't change a byte.
|
|
26
26
|
|
|
27
|
-
`register` is additive on purpose. Calling it more than once for the same target unions the file lists and merges the meta rather than replacing what came before, so a target
|
|
27
|
+
`register` is additive on purpose. Calling it more than once for the same target unions the file lists and merges the meta rather than replacing what came before, so a target’s dependencies can be built up incrementally across more than one call site without either caller needing full knowledge of what the other already supplied. This is exactly what lets a step register its source file and known flags in one call, then separately register the dependencies its tool discovered, described next.
|
|
28
28
|
|
|
29
29
|
### Dependencies discovered by the tool
|
|
30
30
|
|
|
@@ -32,38 +32,40 @@ A step can't know every file it depends on until its tool has run, since the hea
|
|
|
32
32
|
|
|
33
33
|
Two sources supply these files:
|
|
34
34
|
|
|
35
|
-
- **A compile
|
|
36
|
-
- **A preprocessing run
|
|
35
|
+
- **A compile’s `.d` file.** `-MMD -MF` writes gcc’s own dependency output, in Makefile dialect, and `register_gcc_deps_file` (or `register_gcc_deps_string`, for content already in hand) parses it with `GccDependencyParser` and `register`s each target it describes with that target’s dependency list. A single `.d` file can describe more than one target at once; it’s a small manifest, not just one file’s dependency list.
|
|
36
|
+
- **A preprocessing run’s line markers.** Two stages of the test pipeline preprocess each test file. Stage 4 runs it with `-fdirectives-only`, following its `#include`s, including headers a mock stand-in includes. Stage 11 fully expands it, reading every header it reaches. Each output’s line markers name every file it entered. The pipeline registers the project files among them (`Preprocessinator#preprocessed_dependencies`), so editing a header the test reaches reruns the step. Such a header’s macro can decide which mocks the test needs (stage 4) or which test cases it has (stage 11). System headers are left out. So are generated files below the build root, such as stand-ins and the mocks that replace them, which derive from project files and would only ever look changed.
|
|
37
37
|
|
|
38
38
|
### Fresh, stale, and invalidated
|
|
39
39
|
|
|
40
|
-
Once a target
|
|
40
|
+
Once a target’s inputs are registered, `stale?` is a pure question with no side effects, and `mark_fresh` is the write that happens after a real (re)build succeeds, recording new hashes for the target itself, its meta, and every currently-existing dependency. A dependency that’s gone missing is simply left out of what gets recorded -- `stale?` already treats a missing dependency as stale on its own, so there’s nothing to gain by recording anything else about it. `stale?` weighs only the dependencies registered this run, though. One dropped from the list leaves every remaining dependency as it was, so the drop goes unseen. A target whose dependency list can shrink while the rest stay unchanged -- a link, when a source or library leaves the build -- also registers the list itself as meta, so a shorter list is a meta change. A link’s dependencies include the library files `LibraryResolver` finds for its `:libraries` entries, and those the Dependencies plugin publishes in `LIBRARIES_LINK_FILES`. `invalidate` is the explicit override: it drops a target’s cache entry outright, so its next `stale?` answers true whatever the hashes say, while leaving this run’s registrations alone.
|
|
41
41
|
|
|
42
|
-
Some steps also produce a file beside their target that a later run trusts, such as a test
|
|
42
|
+
Some steps also produce a file beside their target that a later run trusts, such as a test’s cached results or a test file’s cached build directives. These are a target’s `outputs`, registered after the step produces them and before `mark_fresh`, which records their hashes in the target’s cache entry. A fresh target vouches for those outputs: `stale?` reads them from the cache entry, not this run’s registrations, and treats the target as stale when any one is missing or altered. Damage to a trusted artifact then costs one rerun of the step that wrote it. Throughout, only a regular file counts as present. A directory named as a dependency, as a truncated `.d` file can do, reads as missing rather than being hashed.
|
|
43
43
|
|
|
44
44
|
## What makes two things "the same"
|
|
45
45
|
|
|
46
46
|
Two decisions make this whole scheme trustworthy rather than merely convenient, and both trade a little efficiency for a lot of certainty.
|
|
47
47
|
|
|
48
|
-
`DependencyHasher` hashes a file
|
|
48
|
+
`DependencyHasher` hashes a file’s exact on-disk bytes, whitespace included, with no format-aware normalization at all. A file unchanged since before the build began is hashed once a build, keyed by its size and modification time, since many targets share one dependency. Anything modified as the build began or since is hashed every time. Whitespace isn't reliably cosmetic: it’s syntactically meaningful in YAML, and it can even change whether a C macro’s line continuation is recognized. The two ways a hash-based staleness check can be wrong aren't equally bad, either. Reporting something stale when it isn't just costs a wasted rebuild. Reporting it fresh when it isn't means shipping a build that silently doesn't reflect its own source -- exactly the failure this whole subsystem exists to prevent. The same hasher also canonicalizes a `meta` value before hashing it. Hash-key order and Symbol-vs-String keys never matter as a result, while array element order is still preserved, since the order of a flags or defines list can itself be meaningful.
|
|
49
49
|
|
|
50
50
|
`DependencyPathNormalizer` solves a narrower but equally important problem. It resolves a path to its real, on-disk casing before that path is ever used as a cache key. A `#include "Foo.h"` written one way and a file that actually exists on disk as `foo.h` can then never be mistaken for two different targets -- nor can the same real file assembled from project configuration two different ways.
|
|
51
51
|
|
|
52
52
|
## The on-disk cache
|
|
53
53
|
|
|
54
|
-
Each pipeline identifier gets exactly one JSON cache file of its own, never shared with any other identifier. Test and release builds each get their own, and a plugin introducing a third pipeline would get a third, just by choosing its own identifier. This isn't incidental. It
|
|
54
|
+
Each pipeline identifier gets exactly one JSON cache file of its own, never shared with any other identifier. Test and release builds each get their own, and a plugin introducing a third pipeline would get a third, just by choosing its own identifier. This isn't incidental. It’s what makes the pruning behavior described below safe at all -- a shared cache would mean a full test run’s prune could silently evict every cached release entry along with it.
|
|
55
55
|
|
|
56
|
-
Loading that cache is deliberately forgiving in one direction and strict in another. Any structural surprise in the file as a whole degrades the entire store to empty rather than raising. A missing cache, malformed cache, or incompatible version simply triggers a full rebuild instead of a hard failure. A single malformed *entry* within an otherwise-good file, similarly, only drops that one entry (i.e. requires a rebuild step). On every load, entries whose target has since been deleted from disk are pruned, automatically and unconditionally. This pruning is always safe, even during a partial run that only touches a handful of targets, because it only ever removes an entry once its target is confirmed gone from disk -- never merely unregistered this run. That
|
|
56
|
+
Loading that cache is deliberately forgiving in one direction and strict in another. Any structural surprise in the file as a whole degrades the entire store to empty rather than raising. A missing cache, malformed cache, or incompatible version simply triggers a full rebuild instead of a hard failure. A single malformed *entry* within an otherwise-good file, similarly, only drops that one entry (i.e. requires a rebuild step). On every load, entries whose target has since been deleted from disk are pruned, automatically and unconditionally. This pruning is always safe, even during a partial run that only touches a handful of targets, because it only ever removes an entry once its target is confirmed gone from disk -- never merely unregistered this run. That’s exactly what distinguishes it from the opt-in pruning described next. Persisting the cache back out is done via a write-to-temp-file-then-rename, so a reader can never observe a half-written cache.
|
|
57
|
+
|
|
58
|
+
The cache’s schema version also marks the build layout. Its keys are build paths, so a release that moves where a build puts its output bumps the version even when the file’s shape is unchanged. Version 2 marks 1.2.2’s layout, which places each test’s output by the test’s path from the project root, and each object, preprocessed file, and dependencies file by its source’s path from the project root. A cache from an earlier version is set aside like any other incompatible one, so the first build after upgrading rebuilds everything once, and no target can trust an old directory that a different test’s new name happens to reuse. Its warning says so and advises `ceedling clobber`. The old store’s entries go with it, so nothing in the cache refers to the old layout. Only the old files stay on disk, where gcov’s coverage reports would still read them until `clobber` removes them.
|
|
57
59
|
|
|
58
60
|
## `refresh_dependencies`: the special feature
|
|
59
61
|
|
|
60
|
-
A normal `flush` just persists whatever this run registered and learned. Passing `refresh_dependencies: true` additionally drops every cache entry whose target was *not* registered at some point during this run, before persisting. In effect, this treats this run
|
|
62
|
+
A normal `flush` just persists whatever this run registered and learned. Passing `refresh_dependencies: true` additionally drops every cache entry whose target was *not* registered at some point during this run, before persisting. In effect, this treats this run’s registrations as the complete, authoritative set of everything that should still have a cache entry at all. That’s exactly correct for a run that’s guaranteed to have touched every target that matters, such as a full `test:all` or a full release build. It’s exactly wrong for anything less than that. A single test file, `test:pattern`, `test:path`, or the ad hoc single-file `release:compile:`/`release:assemble:` tasks would each, if this were set true, silently wipe out cache entries for every target the partial run simply didn't happen to touch -- forcing an unnecessary full rebuild on the next real run. Both pipelines are careful to set it only when they can actually make that guarantee.
|
|
61
63
|
|
|
62
64
|
## Debugging a stale/fresh surprise
|
|
63
65
|
|
|
64
66
|
Separately from anything either pipeline does on a normal run, this subsystem carries an opt-in troubleshooting aid for a human trying to understand *why* something did or didn't rebuild. Setting the `CEEDLING_DEP_DEBUG` developer-only environment variable turns on a tiered snapshot mechanism, off by default. `CEEDLING_DEP_DEBUG=meta` captures just the canonicalized meta a target was judged against. `CEEDLING_DEP_DEBUG=full` additionally captures the actual content of the target and its dependencies at the moment they were last marked fresh. Both tiers write into a human-readable file tree alongside the cache, capped in size per file so one enormous generated header can't silently balloon that tree.
|
|
65
67
|
|
|
66
|
-
Those snapshots exist to back one thing: `DependencyTracker#diagnose`, an always-available "explain yourself" query that reports which of a target
|
|
68
|
+
Those snapshots exist to back one thing: `DependencyTracker#diagnose`, an always-available "explain yourself" query that reports which of a target’s self-hash, meta-hash, dependency hashes, or output hashes changed. `diagnose` runs regardless of whether `CEEDLING_DEP_DEBUG` is set. Without a snapshot to compare against, it can still report *that* a given hash changed, but not *what* changed. With one available, from a run where the matching tier was active, it produces an actual before/after diff instead.
|
|
67
69
|
|
|
68
70
|
`DependencyDebugTree` writes and reads those snapshots, and `DependencyDiffer` turns a snapshot and the live content into that diff. The differ uses the `diff-lcs` gem, a development-only dependency that an installed release gem does not require. Without it, the explanation degrades to a plain before/after dump rather than failing.
|
|
69
71
|
|
|
@@ -55,7 +55,9 @@ require 'ceedling/dependencies/dependency_hasher'
|
|
|
55
55
|
# only when it knows a run's registrations were comprehensive.
|
|
56
56
|
class DependencyCacheStore
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
# Cache keys are build paths, so a change to where a build puts its output changes the
|
|
59
|
+
# schema too. 2: per-test output is placed by the test's path from the project root.
|
|
60
|
+
CACHE_SCHEMA_VERSION = 2 unless const_defined?(:CACHE_SCHEMA_VERSION, false)
|
|
59
61
|
|
|
60
62
|
constructor :file_wrapper, :loginator
|
|
61
63
|
|
|
@@ -73,6 +75,7 @@ class DependencyCacheStore
|
|
|
73
75
|
parsed = JSON.parse( raw )
|
|
74
76
|
|
|
75
77
|
return degrade( store_path, "does not contain a JSON object" ) unless parsed.is_a?( Hash )
|
|
78
|
+
return upgrade( store_path ) if earlier_layout?( parsed['schema_version'] )
|
|
76
79
|
return degrade( store_path, "has an incompatible or missing schema_version" ) unless parsed['schema_version'] == CACHE_SCHEMA_VERSION
|
|
77
80
|
return degrade( store_path, "has an incompatible or missing hash_algorithm" ) unless parsed['hash_algorithm'] == DependencyHasher::HASH_ALGORITHM
|
|
78
81
|
return degrade( store_path, "is missing its entries object" ) unless parsed['entries'].is_a?( Hash )
|
|
@@ -83,7 +86,7 @@ class DependencyCacheStore
|
|
|
83
86
|
if !valid_entry?( entry )
|
|
84
87
|
@loginator.log(
|
|
85
88
|
"Dependency cache entry for '#{target}' in #{store_path} is malformed -- dropping just that entry.",
|
|
86
|
-
Verbosity::COMPLAIN
|
|
89
|
+
Verbosity::COMPLAIN
|
|
87
90
|
)
|
|
88
91
|
pruned << target
|
|
89
92
|
elsif !@file_wrapper.exist?( target )
|
|
@@ -136,7 +139,21 @@ class DependencyCacheStore
|
|
|
136
139
|
def degrade(store_path, reason)
|
|
137
140
|
@loginator.log(
|
|
138
141
|
"Dependency cache #{store_path} #{reason} -- treating it as absent (full rebuild for tracked targets).",
|
|
139
|
-
Verbosity::COMPLAIN
|
|
142
|
+
Verbosity::COMPLAIN
|
|
143
|
+
)
|
|
144
|
+
empty_store
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def earlier_layout?(version)
|
|
148
|
+
return version.is_a?( Integer ) && version.between?( 1, CACHE_SCHEMA_VERSION - 1 )
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# An earlier build layout leaves its output where this build no longer looks
|
|
152
|
+
def upgrade(store_path)
|
|
153
|
+
@loginator.log(
|
|
154
|
+
"Dependency cache #{store_path} comes from an earlier Ceedling build layout -- rebuilding everything once. " \
|
|
155
|
+
"Run `ceedling clobber` to remove any orphaned build output left in the old layout.",
|
|
156
|
+
Verbosity::COMPLAIN
|
|
140
157
|
)
|
|
141
158
|
empty_store
|
|
142
159
|
end
|
|
@@ -30,8 +30,18 @@ class DependencyHasher
|
|
|
30
30
|
HASH_ALGORITHM = 'sha256'.freeze unless const_defined?(:HASH_ALGORITHM, false)
|
|
31
31
|
DIGEST_RE = /\A[0-9a-f]{64}\z/.freeze unless const_defined?(:DIGEST_RE, false)
|
|
32
32
|
|
|
33
|
+
# The coarsest file timestamp a filesystem keeps (FAT's two seconds), so a file modified as
|
|
34
|
+
# the build began never counts as older than it
|
|
35
|
+
TIMESTAMP_TICK_S = 2
|
|
36
|
+
|
|
33
37
|
constructor :file_wrapper
|
|
34
38
|
|
|
39
|
+
def setup
|
|
40
|
+
@digests = {}
|
|
41
|
+
@digests_lock = Mutex.new
|
|
42
|
+
@unchanged_before = Time.now - TIMESTAMP_TICK_S
|
|
43
|
+
end
|
|
44
|
+
|
|
35
45
|
# SHA-256 hex digest of a file's exact on-disk bytes.
|
|
36
46
|
#
|
|
37
47
|
# Read via `read_binary` rather than `read`: Ruby's default text-mode read
|
|
@@ -62,8 +72,22 @@ class DependencyHasher
|
|
|
62
72
|
# reflect the source -- exactly the failure this class exists to prevent.
|
|
63
73
|
# Any normalization narrows the margin between those two outcomes in
|
|
64
74
|
# exchange for skipping some rebuilds; that trade isn't worth it here.
|
|
75
|
+
#
|
|
76
|
+
# A file unchanged since before the build began hashes once a build, keyed by its size and
|
|
77
|
+
# modification time. Many targets share one dependency, as every test executable shares a
|
|
78
|
+
# library it links. A file modified as the build began or since, as anything the build
|
|
79
|
+
# writes is, hashes afresh every time: a timestamp is too coarse to show a rewrite within
|
|
80
|
+
# one tick.
|
|
65
81
|
def hash_of_file(path)
|
|
66
|
-
|
|
82
|
+
status = @file_wrapper.stat( path )
|
|
83
|
+
return digest_of( path ) unless memoizable?( status )
|
|
84
|
+
|
|
85
|
+
key = [path, status.size, status.mtime]
|
|
86
|
+
@digests_lock.synchronize { return @digests[key] if @digests.key?( key ) }
|
|
87
|
+
|
|
88
|
+
digest = digest_of( path )
|
|
89
|
+
@digests_lock.synchronize { @digests[key] = digest }
|
|
90
|
+
return digest
|
|
67
91
|
end
|
|
68
92
|
|
|
69
93
|
# SHA-256 hex digest of a canonicalized `meta` value, or nil for nil/empty
|
|
@@ -91,4 +115,14 @@ class DependencyHasher
|
|
|
91
115
|
end
|
|
92
116
|
end
|
|
93
117
|
|
|
118
|
+
private
|
|
119
|
+
|
|
120
|
+
def digest_of(path)
|
|
121
|
+
return Digest::SHA256.hexdigest( @file_wrapper.read_binary( path ) )
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def memoizable?(status)
|
|
125
|
+
return !status.nil? && status.mtime < @unchanged_before
|
|
126
|
+
end
|
|
127
|
+
|
|
94
128
|
end
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
# SPDX-License-Identifier: MIT
|
|
6
6
|
# =========================================================================
|
|
7
7
|
|
|
8
|
-
require 'ceedling/file_path_utils'
|
|
8
|
+
require 'ceedling/filesystem/file_path_utils'
|
|
9
9
|
|
|
10
10
|
# A configured filename-extension setting for one file type (source, header,
|
|
11
11
|
# assembly, and so on). A project may name a file type with a single
|
|
@@ -51,6 +51,12 @@ class FilenameExtension
|
|
|
51
51
|
@extensions.include?(File.extname(filepath))
|
|
52
52
|
end
|
|
53
53
|
|
|
54
|
+
# A regex source matching any one of this type's extensions, each escaped, as one
|
|
55
|
+
# non-capturing group, for a pattern naming a file of this type by its ending
|
|
56
|
+
def pattern
|
|
57
|
+
return "(?:#{@extensions.map { |ext| Regexp.escape( ext ) }.join( '|' )})"
|
|
58
|
+
end
|
|
59
|
+
|
|
54
60
|
# Every filename `basename` could take on under this type, one per
|
|
55
61
|
# configured extension. Used to search a directory for a file of this
|
|
56
62
|
# type when its extension isn't known in advance -- a build input file
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
# File Trees
|
|
2
|
+
|
|
3
|
+
Ceedling copies trees of files whose types it must not misjudge. `FileTree` walks and copies those trees without `lstat`. It lists each directory with `Dir.children` and classifies each entry with `File.stat`. It brings a copy up to date file by file, writes each file whole, and replaces any entry of the wrong type. This is best effort. It removes the dependence on `lstat`, and trusts `stat` in its place, which no filesystem guarantees.
|
|
4
|
+
|
|
5
|
+
This document is for developers and coding agents changing code that walks or copies trees. It begins with orientation and ends with detail. The orientation explains the problem, where Ceedling copies trees, the rules the code follows, and the invariants a change must keep. Later sections trace each rule to its methods, then cover limits, platforms, and testing.
|
|
6
|
+
|
|
7
|
+
Statements of fact carry an evidence label where the evidence is not the code itself.
|
|
8
|
+
|
|
9
|
+
| Label | Meaning |
|
|
10
|
+
| --- | --- |
|
|
11
|
+
| **[verified]** | Run and observed, as the testing section describes |
|
|
12
|
+
| **[sourced]** | Taken from vendor documentation, not reproduced here |
|
|
13
|
+
| **[inferred]** | Reasoned from sources, not confirmed |
|
|
14
|
+
| **[unverified]** | A hypothesis awaiting a run on the platform in question |
|
|
15
|
+
|
|
16
|
+
## Orientation
|
|
17
|
+
|
|
18
|
+
### The problem
|
|
19
|
+
|
|
20
|
+
`stat` and `lstat` both report a path's type. `stat` follows a link and reports its target. `lstat` reports the link itself. Code that must not follow links, such as a recursive copy or a recursive delete, classifies entries with `lstat`. Ruby's `FileUtils.cp_r` and `FileUtils.rm_rf` do.
|
|
21
|
+
|
|
22
|
+
On some filesystems `lstat` reports a regular file as a directory while `stat` reports it correctly. Windows reparse points are the known case. A reparse point is a file or directory carrying a tag that a filesystem filter interprets. Some tags make an entry a link to another name, such as a symbolic link or a junction. These are name surrogates. Others mark a transparent storage optimization, such as Data Deduplication (`0x80000013`) or Windows Overlay Filter compression (`0x80000017`). These are not links, and reading such a file returns its contents as usual **[sourced]**. Ruby on Windows can report a non-link reparse point as a directory from `lstat` **[inferred]**. A file on a deduplicated network share has been observed to do so, `stat` reporting it as a file **[sourced]**.
|
|
23
|
+
|
|
24
|
+
A copy that classifies by `lstat` then makes a directory where the file belongs, and fails reading the file as a directory **[verified]**. The stray directory remains, and every later copy collides with it. Retrying cannot help, because the misreport and the leftover both persist.
|
|
25
|
+
|
|
26
|
+
A glob is no safer a walk. `Dir.glob( '**/*' )` lists a linked directory without descending into it **[verified]**. It reads `[`, `{`, `*`, and `?` in a pattern as syntax. How it decides an entry's type on Windows is unknown **[unverified]**.
|
|
27
|
+
|
|
28
|
+
### Where Ceedling copies trees
|
|
29
|
+
|
|
30
|
+
| Copy | Path through the code | When |
|
|
31
|
+
| --- | --- | --- |
|
|
32
|
+
| Unity, CMock, and CException into `build/vendor/` | `ConfiguratorSetup#vendor_frameworks_and_support_files` → `FileWrapper#sync_tree` → `FileTree.sync` | Before build tasks, for a test or release build only |
|
|
33
|
+
| `ceedling.h` and the gdb backtrace script | `ConfiguratorSetup#vendor_test_support_files` → `FileWrapper#sync_file` → `FileTree.sync_file` | Before test build tasks, when Partials or gdb backtraces are on |
|
|
34
|
+
| Ceedling into a project's `vendor/ceedling/`, examples, and docs | `Actionator#copy_directory` → `FileWrapper#tree_files` → `FileTree.each_file` | `ceedling new --local`, `upgrade`, `example`, and `--docs` |
|
|
35
|
+
|
|
36
|
+
### The rules
|
|
37
|
+
|
|
38
|
+
1. **Classify by `stat`.** Every source entry is classified with `File.stat`. A destination entry is checked with `File.directory?`, `File.file?`, and `File.exist?`, which follow links as `stat` does.
|
|
39
|
+
2. **List with `Dir.children`.** A directory's names are read as names, never as a pattern, and sorted.
|
|
40
|
+
3. **Follow links.** A link's target is copied, so a copy holds only regular files and directories. A link loop, a dangling link, a special file such as a socket, or an entry that cannot be read or listed raises a `CeedlingException` naming it.
|
|
41
|
+
4. **Write whole.** A file is written to a temporary name beside its destination and renamed over it. A reader sees the old file or the new one, never part of either.
|
|
42
|
+
5. **Write only what differs.** A destination file matching its source in size and content is left alone, and keeps its modification time.
|
|
43
|
+
6. **Replace the wrong type.** A directory where a file belongs, or a file or dangling link where a directory belongs, is removed and logged before the copy goes on.
|
|
44
|
+
7. **Retry only what clears.** A file's whole sync, from its type check to its write, is retried after each of a series of delays when it fails with `EACCES`, `EBUSY`, `EPERM`, or `EINTR`. Any other error fails at once.
|
|
45
|
+
|
|
46
|
+
### Code organization
|
|
47
|
+
|
|
48
|
+
| File | Module or class | Role |
|
|
49
|
+
| --- | --- | --- |
|
|
50
|
+
| `file_tree.rb` | `FileTree` | The walk, the sync, healing, and retries. A pure module, used by `lib/` and `bin/` alike |
|
|
51
|
+
| `file_wrapper.rb` | `FileWrapper` | `#sync_tree`, `#sync_file`, and `#tree_files` give `FileTree` Ceedling's publish step, retry delays, and logging |
|
|
52
|
+
| `../config/configurator_setup.rb` | `ConfiguratorSetup` | Choosing which frameworks a build vendors, and when |
|
|
53
|
+
| `../../../bin/actionator.rb` | `Actionator` | Copying trees for the command line, with its own status lines and skip rules |
|
|
54
|
+
|
|
55
|
+
### Invariants
|
|
56
|
+
|
|
57
|
+
Every change to this code must keep these invariants. Specs pin each one.
|
|
58
|
+
|
|
59
|
+
- No walk classifies by `lstat`. A copy uses it only on the destination: `FileUtils.rm_r` on a directory with contents, and `File.symlink?` to find a link.
|
|
60
|
+
- A copy holds only regular files and directories.
|
|
61
|
+
- A file that matches its source is never written.
|
|
62
|
+
- A destination file is never visible half-written.
|
|
63
|
+
- A walk ends. A link loop raises rather than recursing.
|
|
64
|
+
- An error that retrying cannot clear surfaces at once.
|
|
65
|
+
|
|
66
|
+
## Walking: `FileTree.each_file` and `.each_entry`
|
|
67
|
+
|
|
68
|
+
`each_entry( root, skip: )` yields each directory and regular file below `root`, a directory ahead of its contents, as its path relative to `root`, its full path, and whether it is a directory. `each_file( root, skip: )` yields only the files, as relative and full paths. Without a block, it returns an Enumerator. `skip`, when given, is a callable given each entry's name. An entry it accepts is neither yielded nor entered, nor even stat'd.
|
|
69
|
+
|
|
70
|
+
Each directory's names come from `Dir.children` and are sorted. A directory that cannot be listed raises "could not be listed". The walk stats each entry. A directory is descended, a regular file is yielded, and anything else raises a `CeedlingException` naming the entry and its type. An entry `stat` cannot read raises "could not be read".
|
|
71
|
+
|
|
72
|
+
`ENOENT` or `ELOOP` from `stat` means a link points nowhere, and raises "is a dangling or unresolvable link". Before descending, the walk resolves the directory with `File.realpath` and compares it to the real paths of the directories above it. A match raises "Link loop". A directory whose real path cannot be resolved stands for itself, so the walk goes on without that safeguard rather than stopping. A root that is not a directory raises at once.
|
|
73
|
+
|
|
74
|
+
## Copying: `FileTree.sync` and `.sync_file`
|
|
75
|
+
|
|
76
|
+
`sync( source, destination, publish:, retry_delays:, sleep:, log: )` brings every file below `source` up to date below `destination`. It returns a `Result` counting files `copied`, `unchanged`, and `healed`. `sync_file( source, destination_dir, … )` does the same for one regular file.
|
|
77
|
+
|
|
78
|
+
`publish` is a callable given a source and a destination path. It does the writing, so the caller decides how a file is written and checked. `retry_delays` lists seconds to wait between attempts. `sleep` and `log` are seams for specs and for Ceedling's logging. `log` receives a message, a `Verbosity` level, and a `LogLabels` value.
|
|
79
|
+
|
|
80
|
+
### Directories
|
|
81
|
+
|
|
82
|
+
A destination directory that exists is kept. A file or a dangling link in its place is removed, counted as healed, and logged. Then `FileUtils.mkdir_p` makes the directory, so empty source directories appear in the copy too.
|
|
83
|
+
|
|
84
|
+
### Files
|
|
85
|
+
|
|
86
|
+
A destination directory in a file's place is removed. A link to a directory is unlinked, `Dir.rmdir` removes an empty directory, and `FileUtils.rm_r` one with contents. Any error removing it reaches the retries. Another run may heal the same entry first. `ENOENT` means it removed the directory, and `ENOTDIR` that it already published the file there. This run then counts no heal, and publishes over that file rather than deleting it, so a reader always finds one. A destination file of the same size and the same bytes, by `FileUtils.compare_file`, is counted unchanged and left alone. Otherwise `publish` writes the file.
|
|
87
|
+
|
|
88
|
+
### Retries
|
|
89
|
+
|
|
90
|
+
A file's sync raising one of `TRANSIENT_ERRORS` at any step waits the next delay and tries the file again. A scanner or sync client can hold the destination open as well as the file being written. Once the delays are spent, the failure is logged and raised as a `CeedlingException` naming the file and the number of retries. Any other error propagates at once, unchanged. Its message names the path.
|
|
91
|
+
|
|
92
|
+
### Log messages
|
|
93
|
+
|
|
94
|
+
| Event | Verbosity, label | Message |
|
|
95
|
+
| --- | --- | --- |
|
|
96
|
+
| A directory replaced by a file | COMPLAIN, NOTICE | `Replaced a directory where the file '<path>' belongs` |
|
|
97
|
+
| A file replaced by a directory | COMPLAIN, NOTICE | `Replaced a file where the directory '<path>' belongs` |
|
|
98
|
+
| A retry | DEBUG | `Retrying copy to '<path>' after a transient error: <message>` |
|
|
99
|
+
| Retries spent | ERRORS | `Failed to copy to '<path>' after <n> retries ⏩️ <message>`, then advice to delete `build/` or reinstall the gem |
|
|
100
|
+
|
|
101
|
+
## FileWrapper's part
|
|
102
|
+
|
|
103
|
+
`#sync_tree` and `#sync_file` pass `FileTree` four things:
|
|
104
|
+
- `#replace_file` as the publish step;
|
|
105
|
+
- `TRANSIENT_IO_RETRY_DELAYS`, four delays totalling 0.37 s;
|
|
106
|
+
- `sleep`;
|
|
107
|
+
- the Loginator.
|
|
108
|
+
|
|
109
|
+
`#replace_file` copies the source to `.<name>.<pid>.<thread id>.tmp` beside the destination and renames it into place. It checks that temporary path's length against the platform's limit first. It makes the copy owner-writable, and a read-only destination file too, since Windows refuses to rename over a read-only file. A destination link is replaced, and the file it names keeps its mode. Execute bits carry over from the source.
|
|
110
|
+
|
|
111
|
+
`#tree_files( dir, skip: )` returns every regular file below `dir` as a full path, sorted as one list. Sorting the full paths puts `a.c` ahead of `a/b.c`, the same order a sorted glob listing gives.
|
|
112
|
+
|
|
113
|
+
## Actionator's part
|
|
114
|
+
|
|
115
|
+
`#copy_directory` copies each file `FileWrapper#tree_files` lists, in that order. It passes `skip: Actionator::HIDDEN`, so entries named with a leading dot are never listed or entered, and nothing inside a hidden directory can stop a copy. It leaves out the junk files its exclude pattern matches too. It compares, forces, and reports each file as it always does, so its status lines match Thor's.
|
|
116
|
+
|
|
117
|
+
## Limits
|
|
118
|
+
|
|
119
|
+
- **`stat` is trusted.** The design assumes `stat` reports a type correctly wherever `lstat` does not. That is the observation behind it, not a guarantee.
|
|
120
|
+
- **`lstat` remains on the destination.** `FileUtils.rm_r` classifies by `lstat` when it removes a destination directory with contents, and never follows a link out of it. `File.symlink?` asks `lstat` whether a destination entry is a link. Both work on the destination, which Ceedling creates, not on a source a filesystem may misreport.
|
|
121
|
+
- **Special files raise.** A socket, a FIFO, or a device in a source tree stops the copy.
|
|
122
|
+
- **Extra files stay.** A copy never removes destination files its source lacks.
|
|
123
|
+
- **Every file is read.** Comparing a file reads both copies on every build. Vendored trees hold a few hundred small files, so this costs little. Comparing by modification time would cost less, but would trust timestamps.
|
|
124
|
+
- **Timestamps.** A written file takes the time of writing. A skipped file keeps its own. Source times are not preserved, and nothing in Ceedling depends on them.
|
|
125
|
+
- **Concurrent builds.** Each file is renamed into place whole, so two builds sharing one `build/vendor/` never read a partial file. A heal never deletes a file another build already published, and publishes over it instead. A spec simulates that race. No spec runs two processes.
|
|
126
|
+
|
|
127
|
+
## Platforms
|
|
128
|
+
|
|
129
|
+
- **Windows.**
|
|
130
|
+
- Reparse points are the case this design exists for.
|
|
131
|
+
- A junction is a link to a directory. Ruby reports it as one, `File.symlink?` true and `lstat` a link, and the walk descends it **[verified]**. Windows makes junctions only on local NTFS volumes, never on a UNC share **[verified]**.
|
|
132
|
+
- `File.rename` replaces an existing file. Windows refuses to replace a read-only one, though Ruby may clear the attribute itself **[inferred]**. `#replace_file` clears it first either way.
|
|
133
|
+
- Names cannot hold `*` or `?`.
|
|
134
|
+
- Paths stop near 260 characters. The temporary name adds about 20 characters beyond the destination's, and `check_path_length` warns as a path nears the limit.
|
|
135
|
+
- **macOS and Linux.**
|
|
136
|
+
- Symlinks are followed.
|
|
137
|
+
- `rename` over a read-only file succeeds, since a directory's permissions govern it.
|
|
138
|
+
- On Linux, `Dir.glob` makes no stat-family calls of its own, reading types from the directory instead **[verified]**.
|
|
139
|
+
- **FAT filesystems.**
|
|
140
|
+
- FAT has no links, refuses `*` and `?` in names, and keeps time to 2 s.
|
|
141
|
+
- The specs skip what FAT cannot hold.
|
|
142
|
+
|
|
143
|
+
## Testing and coverage
|
|
144
|
+
|
|
145
|
+
### Approach
|
|
146
|
+
|
|
147
|
+
The specs test the shape of the problem rather than the infrastructure that produces it. Real network shares and real Data Deduplication are too heavy for CI, and unnecessary once the failure is expressed as `lstat` misreporting a type.
|
|
148
|
+
|
|
149
|
+
### The stand-in
|
|
150
|
+
|
|
151
|
+
`spec/support/file_tree_helper.rb` defines a `LyingStat`, a stat result that claims a directory. `lie_about_lstat( pattern )` stubs `File.lstat` through RSpec to return one for each path matching `pattern`. `FileUtils` classifies entries by calling `File.lstat` from Ruby, so the stub reaches it **[verified]**. RSpec removes the stub after each example. A global patch of `File` would leak between examples.
|
|
152
|
+
|
|
153
|
+
Three kinds of example stub `File.lstat`. The control and the survival examples use the stand-in, and the guard a stricter stub.
|
|
154
|
+
|
|
155
|
+
1. **The control.** `FileUtils.cp_r` fails under the stand-in, making a directory where the file belongs **[verified]**. This shows the stand-in reproduces the failure. If a later Ruby stops failing here, review the control rather than deleting it.
|
|
156
|
+
2. **Survival.** `FileTree.sync` copies every file as a file while every entry lies, and `FileTree.each_file` lists every file **[verified]**.
|
|
157
|
+
3. **The guard.** With `File.lstat` stubbed to raise on any call, a sync that heals an empty stray directory still succeeds **[verified]**.
|
|
158
|
+
|
|
159
|
+
The stand-in proves `FileTree` never classifies by `lstat`. It does not prove a real filesystem behaves as the stand-in does, or that `stat` is right everywhere. It exercises the symptom, not the cause.
|
|
160
|
+
|
|
161
|
+
### Real trees
|
|
162
|
+
|
|
163
|
+
`spec/units/filesystem/file_tree_spec.rb` builds real trees in a temporary directory and covers:
|
|
164
|
+
- nested trees, dotfiles, and empty directories;
|
|
165
|
+
- names holding glob metacharacters;
|
|
166
|
+
- an unchanged file keeping its modification time, and a changed file replaced, even at the same size;
|
|
167
|
+
- extra destination files left alone;
|
|
168
|
+
- each kind of healing, the destination root, a dangling link, and a link to a directory included, and a heal another run made first;
|
|
169
|
+
- links: a linked root, a linked file, a linked directory, a loop, and a dangling link;
|
|
170
|
+
- retries on each transient error, on a destination that cannot yet be read, and on each kind of heal, retries spent, and errors that fail at once;
|
|
171
|
+
- entries `skip` names, directories that cannot be listed, entries that cannot be read, and real paths that cannot be resolved;
|
|
172
|
+
- `.sync_file`.
|
|
173
|
+
|
|
174
|
+
**The canary.** One example shows `Dir.glob( '**/*' )` skipping a linked directory's contents. It characterizes Ruby, not `FileTree`. If it ever fails, Ruby changed, and rule 2's reasoning deserves another look.
|
|
175
|
+
|
|
176
|
+
Examples needing links skip themselves where links cannot be made. The glob-name examples write only names the filesystem accepts.
|
|
177
|
+
|
|
178
|
+
Specs elsewhere cover the callers:
|
|
179
|
+
- `spec/units/filesystem/file_wrapper_spec.rb` covers retries through `#sync_tree`, `#replace_file`'s writable bits and linked destinations, and `#tree_files`.
|
|
180
|
+
- `spec/integration/vendor_copy_transient_io_retry_spec.rb` vendors real trees through `ConfiguratorSetup`: a stray directory at any file, a misreporting `lstat`, read-only files, and faults that clear, persist, or cannot clear.
|
|
181
|
+
- `spec/integration/actionator_file_operations_spec.rb` covers sorted status lines, hidden entries and the hidden directories never entered, glob-named directories, linked directories, and a misreporting `lstat`.
|
|
182
|
+
|
|
183
|
+
### Windows reparse points
|
|
184
|
+
|
|
185
|
+
`spec/units/filesystem/file_tree_windows_spec.rb` runs only on Windows.
|
|
186
|
+
- **Junction.** The first example makes a junction with `mklink /J` and copies through it. A canary line prints what `File.symlink?`, `lstat`, and `stat` report for the junction, without asserting any of them. On a UNC share, where Windows cannot make a junction, it skips without trying.
|
|
187
|
+
- **WOF canary.** The second compresses a file with `compact /exe:lzx`, confirms its reparse tag is `0x80000017`, and prints the same canary line for the file. It then asserts the copy is a regular file of full size.
|
|
188
|
+
- Each skips when its runner cannot make the reparse point. The skip names the command that failed, its exit status, and its output. A failed `compact` also reports the state of the WOF driver.
|
|
189
|
+
|
|
190
|
+
On hosted Windows runners:
|
|
191
|
+
- The junction example passes on the `C:` and `D:` drives and on a VHD-backed NTFS volume. Its canary reads `symlink?=true lstat.ftype=link stat.ftype=directory` on each **[verified]**.
|
|
192
|
+
- The WOF example cannot run on a hosted runner, though the WOF driver runs there **[verified]**. It skips on every volume, saying why:
|
|
193
|
+
|
|
194
|
+
| Volume | What happens |
|
|
195
|
+
| --- | --- |
|
|
196
|
+
| `D:`, and a VHD-backed NTFS volume | `compact /exe` fails with "The file system does not support compression" |
|
|
197
|
+
| `C:` | `compact /exe` succeeds, but the file carries no reparse point |
|
|
198
|
+
| The UNC share | `compact /exe` fails with "The request is not supported" |
|
|
199
|
+
|
|
200
|
+
- Whether Ruby misreports a WOF file is therefore unknown **[unverified]**. The example still runs, and its canary still prints, on a Windows machine that can compress a file this way.
|
|
201
|
+
|
|
202
|
+
### Other filesystems
|
|
203
|
+
|
|
204
|
+
Every `FileTree` spec carries `filesystem: true`, so `rspec spec/units/filesystem --tag filesystem` selects them. `CEEDLING_TEST_FS_ROOT` names a directory on another filesystem, and the specs build their trees there instead of the system temporary directory.
|
|
205
|
+
|
|
206
|
+
| Filesystem | What differs | Where it runs | Status |
|
|
207
|
+
| --- | --- | --- | --- |
|
|
208
|
+
| APFS | Case-insensitive by default | macOS test legs, and locally | **[verified]** |
|
|
209
|
+
| ext4 | The Linux baseline | Linux test legs, and locally in Docker | **[verified]** |
|
|
210
|
+
| tmpfs | Memory-backed | The Linux Ruby 3.3 leg, with `CEEDLING_TEST_FS_ROOT=/dev/shm` | **[verified]** in CI and locally in Docker |
|
|
211
|
+
| NTFS | The Windows baseline | Windows test legs | **[verified]** |
|
|
212
|
+
| UNC administrative share | Reaches the local drive through the SMB client | The Filesystem Substrates job | **[verified]**, the junction and WOF examples skipping |
|
|
213
|
+
| System drive `C:` | The Windows system volume | The Filesystem Substrates job | **[verified]**, the WOF example skipping |
|
|
214
|
+
| VHD-backed NTFS volume | A separate volume made with `diskpart` | The Filesystem Substrates job | **[verified]**, the WOF example skipping |
|
|
215
|
+
| Loopback FAT image | No links, restricted names, 2 s timestamps | The Filesystem Substrates job | **[verified]**, the link examples skipping |
|
|
216
|
+
|
|
217
|
+
A failing spec in the Filesystem Substrates job fails CI Gate. A step that cannot set up its filesystem warns, names why, and skips, since that says nothing about Ceedling. Each step runs whatever the others did. The UNC step first enables Windows' evaluation of links on a share that name places on a share.
|
|
218
|
+
|
|
219
|
+
### Proof the specs can fail
|
|
220
|
+
|
|
221
|
+
**Mutations.** `FileTree` was broken on purpose, one change at a time **[verified]**:
|
|
222
|
+
- `File.stat` changed to `File.lstat` in the walk made 7 examples fail;
|
|
223
|
+
- `Dir.children` changed to `Dir.glob` in the walk made 5 fail.
|
|
224
|
+
|
|
225
|
+
**Seed sweeps.** 100 random orderings of the spec file pass on APFS, ext4, and tmpfs **[verified]**.
|
|
226
|
+
|
|
227
|
+
### Coverage
|
|
228
|
+
|
|
229
|
+
| Scenario | Layer | Linux | macOS | Windows |
|
|
230
|
+
| --- | --- | --- | --- | --- |
|
|
231
|
+
| `lstat` misreports a file | Stand-in | yes | yes | yes |
|
|
232
|
+
| No walk or copy calls `File.lstat` | Stand-in guard | yes | yes | yes |
|
|
233
|
+
| Tree shapes, glob names, unchanged and changed files | Real trees | yes | yes | yes |
|
|
234
|
+
| Healing entries of the wrong type | Real trees | yes | yes | yes |
|
|
235
|
+
| Links | Real trees, skipping without links | yes | yes | where permitted |
|
|
236
|
+
| Junctions | Windows real trees, canary | n/a | n/a | yes, except on a UNC share, which cannot hold one |
|
|
237
|
+
| WOF reparse points | Windows real trees, canary | n/a | n/a | not on hosted runners, which cannot make a WOF file |
|
|
238
|
+
| Retries and failures | Injected faults | yes | yes | yes |
|
|
239
|
+
| Other filesystems | `CEEDLING_TEST_FS_ROOT` | tmpfs, FAT | none | UNC, `C:`, VHD |
|
|
240
|
+
|
|
241
|
+
### Not covered
|
|
242
|
+
|
|
243
|
+
- A real network share whose `lstat` answers change from call to call, and real Data Deduplication.
|
|
244
|
+
- Any non-link reparse point on a hosted runner. WOF compression, the one tried, cannot be applied there.
|
|
245
|
+
- Antivirus and cloud-sync software holding files open.
|
|
246
|
+
- Two Ceedling processes copying into one `build/vendor/` at once.
|
|
247
|
+
- Windows paths near 260 characters with the temporary name's suffix.
|
|
248
|
+
- `stat` misreporting a type. The design assumes it does not.
|
|
249
|
+
|
|
250
|
+
## Sources
|
|
251
|
+
|
|
252
|
+
- Microsoft, [Reparse Tags](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/c8e77b37-3909-4fe6-a4ea-2b9d423b1ee4) (MS-FSCC 2.1.2.1), for tag values and name surrogates.
|
|
253
|
+
- Microsoft, [Data Deduplication overview](https://learn.microsoft.com/en-us/windows-server/storage/data-deduplication/overview), for how optimized files are stored.
|