ceedling 1.2.1 → 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 +11 -9
- data/Rakefile +2 -1
- 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 +14 -1
- data/bin/ceedling +13 -3
- data/bin/cli.rb +25 -6
- data/bin/cli_handler.rb +113 -73
- data/bin/cli_helper.rb +51 -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 +74 -3
- data/docs/KnownIssues.md +90 -4
- data/docs/ReleaseNotes.md +13 -0
- 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 +14 -16
- 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 +15 -28
- 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} +45 -38
- 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/rakefile.rb +15 -1
- data/lib/ceedling/rakefiles/base/tasks_base.rake +1 -1
- data/lib/ceedling/rakefiles/release/rules_release.rake +4 -7
- 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 +94 -90
- 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 +24 -40
- 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/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/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/valgrind/lib/valgrind.rb +14 -7
- data/site-local/configuration/reference/cmock.html +2 -1
- data/site-local/configuration/reference/files.html +9 -3
- data/site-local/configuration/reference/libraries.html +21 -3
- data/site-local/configuration/reference/paths.html +29 -6
- data/site-local/configuration/reference/project.html +17 -7
- data/site-local/configuration/reference/release-build.html +2 -0
- data/site-local/configuration/reference/tools.html +9 -0
- data/site-local/configuration/which-ceedling.html +377 -17
- data/site-local/development/releases.html +5 -1
- data/site-local/development/workflow.html +48 -43
- data/site-local/getting-started/builds.html +2 -0
- data/site-local/getting-started/command-line.html +56 -4
- data/site-local/plugins/dependencies.html +6 -3
- data/site-local/plugins/fff.html +42 -11
- data/site-local/plugins/gcov/gcovr.html +15 -12
- data/site-local/plugins/gcov/setup.html +2 -1
- data/site-local/plugins/report-tests-raw-output-log.html +3 -1
- data/site-local/plugins/report-tests-teamcity-stdout.html +6 -3
- data/site-local/plugins/sanitize.html +7 -6
- data/site-local/plugins/valgrind.html +8 -7
- data/site-local/project/upgrade.html +3 -0
- data/site-local/reference/command-line.html +9 -2
- data/site-local/sitemap.xml.gz +0 -0
- 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/conventions.html +53 -0
- 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_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 +22 -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/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 +74 -5
- data/spec/support/file_tree_helper.rb +88 -0
- data/spec/support/integration/configurator_integration_helper.rb +6 -6
- 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 +37 -0
- data/spec/units/bin/cli_handler_spec.rb +282 -6
- data/spec/units/bin/cli_helper_spec.rb +116 -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 +9 -65
- data/spec/units/config/configurator_spec.rb +27 -5
- 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} +95 -12
- 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/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 +65 -3
- 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 +109 -25
- data/lib/ceedling/file_path_collection_utils.rb +0 -164
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
# =========================================================================
|
|
2
|
+
# Ceedling - Test-Centered Build System for C
|
|
3
|
+
# ThrowTheSwitch.org
|
|
4
|
+
# Copyright (c) 2010-26 Mike Karlesky, Mark VanderVoord, & Greg Williams
|
|
5
|
+
# SPDX-License-Identifier: MIT
|
|
6
|
+
# =========================================================================
|
|
7
|
+
|
|
8
|
+
require 'spec_helper'
|
|
9
|
+
require 'file_tree_helper'
|
|
10
|
+
require 'ceedling/filesystem/file_tree'
|
|
11
|
+
require 'ceedling/constants'
|
|
12
|
+
require 'ceedling/exceptions'
|
|
13
|
+
|
|
14
|
+
# GH #1376 (a follow-up to #1292): on a Windows share with Data Deduplication, `File.lstat`
|
|
15
|
+
# reports deduplicated files as directories while `File.stat` reports them correctly. A
|
|
16
|
+
# copy that classifies by `lstat` makes a directory where the file belongs, and every later
|
|
17
|
+
# copy trips over it. FileTree must never depend on `lstat`, and must heal what it finds.
|
|
18
|
+
describe FileTree, filesystem: true do
|
|
19
|
+
include FileTreeHelper
|
|
20
|
+
|
|
21
|
+
around(:each) do |example|
|
|
22
|
+
with_tree_root do |root|
|
|
23
|
+
@root = root
|
|
24
|
+
@src = File.join( root, 'src' )
|
|
25
|
+
@dst = File.join( root, 'dst' )
|
|
26
|
+
example.run
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def sync(source = @src, destination = @dst, **options)
|
|
31
|
+
FileTree.sync( source, destination, publish: FileTreeHelper::PUBLISH, **options )
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def files_of(root)
|
|
35
|
+
FileTree.each_file( root ).to_a
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Writes files named with glob metacharacters below `dir`, returning the names this
|
|
39
|
+
# filesystem accepts. Windows and FAT refuse `*` and `?`.
|
|
40
|
+
def written_glob_names(dir)
|
|
41
|
+
return ['a[1].c', 'b{x,y}.c', 'c*.c', 'd?.c'].select do |name|
|
|
42
|
+
write( File.join( dir, name ) )
|
|
43
|
+
true
|
|
44
|
+
rescue SystemCallError
|
|
45
|
+
false
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# ---- the reported failure, and its absence --------------------------------------------
|
|
50
|
+
|
|
51
|
+
describe 'under an lstat that reports a regular file as a directory' do
|
|
52
|
+
before(:each) do
|
|
53
|
+
write( File.join( @src, 'unity.c' ), 'int x;' )
|
|
54
|
+
write( File.join( @src, 'unity.h' ), 'int y;' )
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# The control: stock FileUtils.cp_r fails as reported, so the stand-in reproduces it
|
|
58
|
+
it 'makes FileUtils.cp_r fail on the misreported file' do
|
|
59
|
+
lie_about_lstat( %r{unity\.c\z} )
|
|
60
|
+
|
|
61
|
+
expect { FileUtils.cp_r( "#{@src}/.", @dst ) }.to raise_error( SystemCallError, /unity\.c/ )
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
it 'leaves .sync copying every file as a file' do
|
|
65
|
+
lie_about_lstat( // )
|
|
66
|
+
|
|
67
|
+
result = sync
|
|
68
|
+
|
|
69
|
+
expect( result.copied ).to eq( 2 )
|
|
70
|
+
expect( File.file?( File.join( @dst, 'unity.c' ) ) ).to be( true )
|
|
71
|
+
expect( File.read( File.join( @dst, 'unity.c' ) ) ).to eq( 'int x;' )
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
it 'leaves .each_file listing every file' do
|
|
75
|
+
lie_about_lstat( // )
|
|
76
|
+
|
|
77
|
+
expect( files_of( @src ).map( &:first ) ).to eq( ['unity.c', 'unity.h'] )
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# The guard: correctness never rests on lstat, healing included
|
|
82
|
+
it 'never calls File.lstat while walking, copying, and healing an empty stray directory' do
|
|
83
|
+
write( File.join( @src, 'a/b/c.c' ), 'c' )
|
|
84
|
+
FileUtils.mkdir_p( File.join( @dst, 'a/b/c.c' ) )
|
|
85
|
+
forbid_lstat
|
|
86
|
+
|
|
87
|
+
sync
|
|
88
|
+
|
|
89
|
+
expect( File.file?( File.join( @dst, 'a/b/c.c' ) ) ).to be( true )
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# ---- walking ---------------------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
describe '.each_file' do
|
|
95
|
+
it 'yields each regular file by its relative path and its source path, sorted, dotfiles included' do
|
|
96
|
+
write( File.join( @src, 'top.c' ) )
|
|
97
|
+
write( File.join( @src, 'sub/deep/inner.h' ) )
|
|
98
|
+
write( File.join( @src, '.hidden' ) )
|
|
99
|
+
FileUtils.mkdir_p( File.join( @src, 'empty' ) )
|
|
100
|
+
|
|
101
|
+
expect( files_of( @src ) ).to eq( [
|
|
102
|
+
['.hidden', File.join( @src, '.hidden' )],
|
|
103
|
+
['sub/deep/inner.h', File.join( @src, 'sub/deep/inner.h' )],
|
|
104
|
+
['top.c', File.join( @src, 'top.c' )]
|
|
105
|
+
] )
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
it 'lists names holding glob metacharacters literally' do
|
|
109
|
+
names = written_glob_names( File.join( @src, 'dir[1]' ) )
|
|
110
|
+
|
|
111
|
+
expect( files_of( @src ).map( &:first ) ).to eq( names.map { |name| "dir[1]/#{name}" }.sort )
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
it 'raises when the root is not a directory' do
|
|
115
|
+
expect { files_of( File.join( @root, 'nope' ) ) }.to raise_error( CeedlingException, /nope/ )
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
it 'neither lists nor enters an entry `skip` names' do
|
|
119
|
+
write( File.join( @src, 'keep.c' ) )
|
|
120
|
+
write( File.join( @src, '.hidden/x.c' ) )
|
|
121
|
+
symlink_or_skip( File.join( @root, 'nowhere' ), File.join( @src, '.hidden/dangling.c' ) )
|
|
122
|
+
|
|
123
|
+
listed = FileTree.each_file( @src, skip: ->( name ) { name.start_with?( '.' ) } ).to_a.map( &:first )
|
|
124
|
+
|
|
125
|
+
expect( listed ).to eq( ['keep.c'] )
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
it 'names a directory it cannot list' do
|
|
129
|
+
FileUtils.mkdir_p( File.join( @src, 'locked' ) )
|
|
130
|
+
allow(Dir).to receive(:children).and_call_original
|
|
131
|
+
allow(Dir).to receive(:children).with( File.join( @src, 'locked' ) ).and_raise( Errno::EACCES )
|
|
132
|
+
|
|
133
|
+
expect { files_of( @src ) }.to raise_error( CeedlingException, /locked.*could not be listed/ )
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
it 'names an entry it cannot read' do
|
|
137
|
+
write( File.join( @src, 'locked.c' ) )
|
|
138
|
+
allow(File).to receive(:stat).and_call_original
|
|
139
|
+
allow(File).to receive(:stat).with( File.join( @src, 'locked.c' ) ).and_raise( Errno::EACCES )
|
|
140
|
+
|
|
141
|
+
expect { files_of( @src ) }.to raise_error( CeedlingException, /locked\.c.*could not be read/ )
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Loop detection is a safeguard, and must not stop a walk it cannot apply to
|
|
145
|
+
it 'walks a directory whose real path cannot be resolved' do
|
|
146
|
+
write( File.join( @src, 'sub/u.c' ) )
|
|
147
|
+
allow(File).to receive(:realpath).and_call_original
|
|
148
|
+
allow(File).to receive(:realpath).with( File.join( @src, 'sub' ) ).and_raise( Errno::EACCES )
|
|
149
|
+
|
|
150
|
+
expect( files_of( @src ).map( &:first ) ).to eq( ['sub/u.c'] )
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# ---- copying ---------------------------------------------------------------------------
|
|
155
|
+
|
|
156
|
+
describe '.sync' do
|
|
157
|
+
it 'copies a nested tree, dotfiles and empty directories included' do
|
|
158
|
+
write( File.join( @src, 'top.c' ) )
|
|
159
|
+
write( File.join( @src, 'sub/deep/inner.h' ), 'inner' )
|
|
160
|
+
write( File.join( @src, '.hidden' ) )
|
|
161
|
+
FileUtils.mkdir_p( File.join( @src, 'empty' ) )
|
|
162
|
+
|
|
163
|
+
sync
|
|
164
|
+
|
|
165
|
+
expect( tree_of( @dst ) ).to eq( tree_of( @src ) )
|
|
166
|
+
expect( File.read( File.join( @dst, 'sub/deep/inner.h' ) ) ).to eq( 'inner' )
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
it 'copies names holding glob metacharacters' do
|
|
170
|
+
names = written_glob_names( File.join( @src, 'dir[1]' ) )
|
|
171
|
+
|
|
172
|
+
sync
|
|
173
|
+
|
|
174
|
+
names.each { |name| expect( File.file?( File.join( @dst, 'dir[1]', name ) ) ).to be( true ), name }
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
it 'writes nothing when every file already matches, keeping each file\'s modification time' do
|
|
178
|
+
write( File.join( @src, 'u.c' ), 'same' )
|
|
179
|
+
sync
|
|
180
|
+
old = Time.at( 1_000_000_000 )
|
|
181
|
+
File.utime( old, old, File.join( @dst, 'u.c' ) )
|
|
182
|
+
|
|
183
|
+
result = sync
|
|
184
|
+
|
|
185
|
+
expect( [result.copied, result.unchanged] ).to eq( [0, 1] )
|
|
186
|
+
expect( File.mtime( File.join( @dst, 'u.c' ) ).to_i ).to eq( old.to_i )
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
it 'replaces a file whose contents differ, even at the same size' do
|
|
190
|
+
write( File.join( @src, 'u.c' ), 'v1' )
|
|
191
|
+
sync
|
|
192
|
+
write( File.join( @src, 'u.c' ), 'v2' )
|
|
193
|
+
|
|
194
|
+
expect( sync.copied ).to eq( 1 )
|
|
195
|
+
expect( File.read( File.join( @dst, 'u.c' ) ) ).to eq( 'v2' )
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
it 'leaves files the source lacks' do
|
|
199
|
+
write( File.join( @src, 'u.c' ) )
|
|
200
|
+
write( File.join( @dst, 'extra.c' ) )
|
|
201
|
+
|
|
202
|
+
sync
|
|
203
|
+
|
|
204
|
+
expect( File.file?( File.join( @dst, 'extra.c' ) ) ).to be( true )
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
it 'raises when the source is not a directory' do
|
|
208
|
+
expect { sync( File.join( @root, 'nope' ) ) }.to raise_error( CeedlingException, /nope/ )
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# ---- healing ---------------------------------------------------------------------------
|
|
213
|
+
|
|
214
|
+
describe '.sync healing' do
|
|
215
|
+
it 'replaces an empty directory where a file belongs, and says so' do
|
|
216
|
+
write( File.join( @src, 'unity.h' ), 'int x;' )
|
|
217
|
+
FileUtils.mkdir_p( File.join( @dst, 'unity.h' ) )
|
|
218
|
+
logged = []
|
|
219
|
+
|
|
220
|
+
result = sync( log: ->( *entry ) { logged << entry } )
|
|
221
|
+
|
|
222
|
+
expect( result.healed ).to eq( 1 )
|
|
223
|
+
expect( File.read( File.join( @dst, 'unity.h' ) ) ).to eq( 'int x;' )
|
|
224
|
+
expect( logged ).to include( ["Replaced a directory where the file '#{File.join( @dst, 'unity.h' )}' belongs", Verbosity::COMPLAIN, LogLabels::NOTICE] )
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
it 'replaces a directory with contents where a file belongs' do
|
|
228
|
+
write( File.join( @src, 'unity.c' ), 'int x;' )
|
|
229
|
+
write( File.join( @dst, 'unity.c', 'junk.txt' ) )
|
|
230
|
+
|
|
231
|
+
sync
|
|
232
|
+
|
|
233
|
+
expect( File.read( File.join( @dst, 'unity.c' ) ) ).to eq( 'int x;' )
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
it 'replaces a file where a directory belongs, and says so' do
|
|
237
|
+
write( File.join( @src, 'sub/u.c' ), 'int x;' )
|
|
238
|
+
write( File.join( @dst, 'sub' ), 'i am a file' )
|
|
239
|
+
logged = []
|
|
240
|
+
|
|
241
|
+
result = sync( log: ->( *entry ) { logged << entry } )
|
|
242
|
+
|
|
243
|
+
expect( result.healed ).to eq( 1 )
|
|
244
|
+
expect( File.read( File.join( @dst, 'sub/u.c' ) ) ).to eq( 'int x;' )
|
|
245
|
+
expect( logged ).to include( ["Replaced a file where the directory '#{File.join( @dst, 'sub' )}' belongs", Verbosity::COMPLAIN, LogLabels::NOTICE] )
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
it 'replaces a file where the destination directory itself belongs' do
|
|
249
|
+
write( File.join( @src, 'u.c' ), 'int x;' )
|
|
250
|
+
write( @dst, 'i am a file' )
|
|
251
|
+
|
|
252
|
+
sync
|
|
253
|
+
|
|
254
|
+
expect( File.read( File.join( @dst, 'u.c' ) ) ).to eq( 'int x;' )
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
it 'replaces a dangling link where a directory belongs' do
|
|
258
|
+
write( File.join( @src, 'sub/u.c' ), 'int x;' )
|
|
259
|
+
FileUtils.mkdir_p( @dst )
|
|
260
|
+
symlink_or_skip( File.join( @root, 'nowhere' ), File.join( @dst, 'sub' ) )
|
|
261
|
+
|
|
262
|
+
sync
|
|
263
|
+
|
|
264
|
+
expect( File.read( File.join( @dst, 'sub/u.c' ) ) ).to eq( 'int x;' )
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
# Another run can heal the same entry first, publishing its file where this run found
|
|
268
|
+
# a directory. That file is never deleted, only published over, so a reader always finds
|
|
269
|
+
# one.
|
|
270
|
+
it 'publishes over a file another run put in place of the directory it found, without deleting it' do
|
|
271
|
+
write( File.join( @src, 'unity.h' ), 'int x;' )
|
|
272
|
+
FileUtils.mkdir_p( File.join( @dst, 'unity.h' ) )
|
|
273
|
+
allow(Dir).to receive(:rmdir).and_wrap_original do |original, path|
|
|
274
|
+
original.call( path )
|
|
275
|
+
File.write( path, 'published by another run' )
|
|
276
|
+
raise Errno::ENOTDIR, path
|
|
277
|
+
end
|
|
278
|
+
allow(FileUtils).to receive(:rm_r).and_call_original
|
|
279
|
+
|
|
280
|
+
result = sync
|
|
281
|
+
|
|
282
|
+
expect(FileUtils).not_to have_received(:rm_r)
|
|
283
|
+
expect( [result.copied, result.healed] ).to eq( [1, 0] )
|
|
284
|
+
expect( File.read( File.join( @dst, 'unity.h' ) ) ).to eq( 'int x;' )
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
it 'replaces a link to a directory where a file belongs, and says so' do
|
|
288
|
+
write( File.join( @src, 'unity.h' ), 'int x;' )
|
|
289
|
+
FileUtils.mkdir_p( File.join( @root, 'elsewhere' ) )
|
|
290
|
+
FileUtils.mkdir_p( @dst )
|
|
291
|
+
symlink_or_skip( File.join( @root, 'elsewhere' ), File.join( @dst, 'unity.h' ) )
|
|
292
|
+
|
|
293
|
+
result = sync
|
|
294
|
+
|
|
295
|
+
expect( result.healed ).to eq( 1 )
|
|
296
|
+
expect( File.read( File.join( @dst, 'unity.h' ) ) ).to eq( 'int x;' )
|
|
297
|
+
expect( File.directory?( File.join( @root, 'elsewhere' ) ) ).to be( true )
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
# ---- links (each skips where links cannot be made) -------------------------------------
|
|
302
|
+
|
|
303
|
+
describe 'links' do
|
|
304
|
+
it 'copies a tree whose root is a link' do
|
|
305
|
+
write( File.join( @root, 'real/u.c' ), 'int x;' )
|
|
306
|
+
symlink_or_skip( File.join( @root, 'real' ), @src )
|
|
307
|
+
|
|
308
|
+
sync
|
|
309
|
+
|
|
310
|
+
expect( File.read( File.join( @dst, 'u.c' ) ) ).to eq( 'int x;' )
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
it 'copies a linked file as a regular file with its target\'s contents' do
|
|
314
|
+
write( File.join( @root, 'elsewhere/real.c' ), 'int x;' )
|
|
315
|
+
FileUtils.mkdir_p( @src )
|
|
316
|
+
symlink_or_skip( File.join( @root, 'elsewhere/real.c' ), File.join( @src, 'link.c' ) )
|
|
317
|
+
|
|
318
|
+
sync
|
|
319
|
+
|
|
320
|
+
expect( File.symlink?( File.join( @dst, 'link.c' ) ) ).to be( false )
|
|
321
|
+
expect( File.read( File.join( @dst, 'link.c' ) ) ).to eq( 'int x;' )
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
it 'descends a linked directory' do
|
|
325
|
+
write( File.join( @root, 'elsewhere/inner.c' ), 'int x;' )
|
|
326
|
+
FileUtils.mkdir_p( @src )
|
|
327
|
+
symlink_or_skip( File.join( @root, 'elsewhere' ), File.join( @src, 'linked_dir' ) )
|
|
328
|
+
|
|
329
|
+
expect( files_of( @src ).map( &:first ) ).to eq( ['linked_dir/inner.c'] )
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
# The canary: characterizes Ruby, not FileTree. A copy walking with Dir.glob('**/*')
|
|
333
|
+
# silently misses a linked directory's contents. If this ever fails, Ruby changed.
|
|
334
|
+
it 'is not descended by Dir.glob(\'**/*\')' do
|
|
335
|
+
write( File.join( @root, 'elsewhere/inner.c' ), 'int x;' )
|
|
336
|
+
FileUtils.mkdir_p( @src )
|
|
337
|
+
symlink_or_skip( File.join( @root, 'elsewhere' ), File.join( @src, 'linked_dir' ) )
|
|
338
|
+
|
|
339
|
+
found = Dir.glob( '**/*', base: @src )
|
|
340
|
+
|
|
341
|
+
expect( found ).to include( 'linked_dir' )
|
|
342
|
+
expect( found ).to_not include( 'linked_dir/inner.c' )
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
it 'raises on a link loop instead of recursing forever' do
|
|
346
|
+
write( File.join( @src, 'a/u.c' ) )
|
|
347
|
+
symlink_or_skip( @src, File.join( @src, 'a/back' ) )
|
|
348
|
+
|
|
349
|
+
expect { files_of( @src ) }.to raise_error( CeedlingException, /loop/i )
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
it 'raises on a dangling link, naming it' do
|
|
353
|
+
FileUtils.mkdir_p( @src )
|
|
354
|
+
symlink_or_skip( File.join( @root, 'nowhere' ), File.join( @src, 'dangling.c' ) )
|
|
355
|
+
|
|
356
|
+
expect { files_of( @src ) }.to raise_error( CeedlingException, /dangling\.c/ )
|
|
357
|
+
end
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
# ---- retries ---------------------------------------------------------------------------
|
|
361
|
+
|
|
362
|
+
describe '.sync retries' do
|
|
363
|
+
let(:delays) { [0.01, 0.02, 0.04] }
|
|
364
|
+
|
|
365
|
+
# A publish step that raises `error` for its first `failures` calls
|
|
366
|
+
def failing_publish(failures, error)
|
|
367
|
+
calls = 0
|
|
368
|
+
lambda do |source, destination|
|
|
369
|
+
calls += 1
|
|
370
|
+
raise error, 'simulated' if calls <= failures
|
|
371
|
+
|
|
372
|
+
FileTreeHelper::PUBLISH.call( source, destination )
|
|
373
|
+
end
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
def sync_with(publish)
|
|
377
|
+
sleeps = []
|
|
378
|
+
logged = []
|
|
379
|
+
write( File.join( @src, 'u.c' ), 'ok' )
|
|
380
|
+
outcome = begin
|
|
381
|
+
FileTree.sync( @src, @dst, publish: publish, retry_delays: delays, sleep: ->( delay ) { sleeps << delay }, log: ->( *entry ) { logged << entry } )
|
|
382
|
+
rescue StandardError => e
|
|
383
|
+
e
|
|
384
|
+
end
|
|
385
|
+
[outcome, sleeps, logged]
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
[Errno::EACCES, Errno::EBUSY, Errno::EPERM, Errno::EINTR].each do |error|
|
|
389
|
+
it "retries #{error.name.split( '::' ).last} with each delay in turn, then succeeds" do
|
|
390
|
+
outcome, sleeps, = sync_with( failing_publish( 2, error ) )
|
|
391
|
+
|
|
392
|
+
expect( outcome.copied ).to eq( 1 )
|
|
393
|
+
expect( sleeps ).to eq( delays.first( 2 ) )
|
|
394
|
+
expect( File.read( File.join( @dst, 'u.c' ) ) ).to eq( 'ok' )
|
|
395
|
+
end
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
it 'fails with a CeedlingException naming the file and the retries once every delay is spent, and logs it' do
|
|
399
|
+
outcome, sleeps, logged = sync_with( failing_publish( 99, Errno::EACCES ) )
|
|
400
|
+
|
|
401
|
+
expect( outcome ).to be_a( CeedlingException )
|
|
402
|
+
expect( outcome.message ).to match( /#{Regexp.escape( File.join( @dst, 'u.c' ) )}.* after 3 retries/ )
|
|
403
|
+
expect( sleeps ).to eq( delays )
|
|
404
|
+
expect( logged.map { |message, verbosity, *| [message, verbosity] } ).to include( [outcome.message, Verbosity::ERRORS] )
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
it 'logs each retry for debugging' do
|
|
408
|
+
_, _, logged = sync_with( failing_publish( 1, Errno::EBUSY ) )
|
|
409
|
+
|
|
410
|
+
expect( logged.map { |message, verbosity, *| [message[/\ARetrying copy to/], verbosity] } ).to include( ['Retrying copy to', Verbosity::DEBUG] )
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
# A scanner or sync client can hold the destination too, not only the file being written
|
|
414
|
+
it 'retries a destination file it cannot yet read' do
|
|
415
|
+
write( File.join( @dst, 'u.c' ), 'no' )
|
|
416
|
+
reads = 0
|
|
417
|
+
allow(FileUtils).to receive(:compare_file).and_wrap_original do |original, *args|
|
|
418
|
+
reads += 1
|
|
419
|
+
raise Errno::EACCES, 'simulated' if reads == 1
|
|
420
|
+
|
|
421
|
+
original.call( *args )
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
outcome, sleeps, = sync_with( FileTreeHelper::PUBLISH )
|
|
425
|
+
|
|
426
|
+
expect( outcome.copied ).to eq( 1 )
|
|
427
|
+
expect( sleeps ).to eq( delays.first( 1 ) )
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
it 'retries a heal that fails transiently' do
|
|
431
|
+
FileUtils.mkdir_p( File.join( @dst, 'u.c' ) )
|
|
432
|
+
calls = 0
|
|
433
|
+
allow(Dir).to receive(:rmdir).and_wrap_original do |original, *args|
|
|
434
|
+
calls += 1
|
|
435
|
+
raise Errno::EACCES, 'simulated' if calls == 1
|
|
436
|
+
|
|
437
|
+
original.call( *args )
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
outcome, sleeps, = sync_with( FileTreeHelper::PUBLISH )
|
|
441
|
+
|
|
442
|
+
expect( [outcome.copied, outcome.healed] ).to eq( [1, 1] )
|
|
443
|
+
expect( sleeps ).to eq( delays.first( 1 ) )
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
it 'retries a stray directory with contents it cannot yet remove, counting one heal' do
|
|
447
|
+
write( File.join( @dst, 'u.c', 'junk' ) )
|
|
448
|
+
calls = 0
|
|
449
|
+
allow(FileUtils).to receive(:rm_r).and_wrap_original do |original, *args|
|
|
450
|
+
calls += 1
|
|
451
|
+
raise Errno::EACCES, 'simulated' if calls == 1
|
|
452
|
+
|
|
453
|
+
original.call( *args )
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
outcome, sleeps, logged = sync_with( FileTreeHelper::PUBLISH )
|
|
457
|
+
|
|
458
|
+
expect( [outcome.copied, outcome.healed] ).to eq( [1, 1] )
|
|
459
|
+
expect( sleeps ).to eq( delays.first( 1 ) )
|
|
460
|
+
expect( logged.count { |message, *| message.start_with?( 'Replaced a directory' ) } ).to eq( 1 )
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
it 'retries a file it cannot yet remove where a directory belongs' do
|
|
464
|
+
write( File.join( @dst, 'sub' ), 'i am a file' )
|
|
465
|
+
write( File.join( @src, 'sub', 'v.c' ), 'v' )
|
|
466
|
+
calls = 0
|
|
467
|
+
allow(File).to receive(:unlink).and_wrap_original do |original, *args|
|
|
468
|
+
calls += 1
|
|
469
|
+
raise Errno::EACCES, 'simulated' if calls == 1
|
|
470
|
+
|
|
471
|
+
original.call( *args )
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
outcome, sleeps, = sync_with( FileTreeHelper::PUBLISH )
|
|
475
|
+
|
|
476
|
+
expect( outcome.healed ).to eq( 1 )
|
|
477
|
+
expect( sleeps ).to eq( delays.first( 1 ) )
|
|
478
|
+
expect( File.read( File.join( @dst, 'sub', 'v.c' ) ) ).to eq( 'v' )
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
[Errno::ENOSPC, Errno::EISDIR, Errno::ENOTDIR].each do |error|
|
|
482
|
+
it "fails at once on #{error.name.split( '::' ).last}, which retrying cannot clear" do
|
|
483
|
+
outcome, sleeps, = sync_with( failing_publish( 1, error ) )
|
|
484
|
+
|
|
485
|
+
expect( outcome ).to be_a( error )
|
|
486
|
+
expect( sleeps ).to be_empty
|
|
487
|
+
end
|
|
488
|
+
end
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
# ---- a single file ---------------------------------------------------------------------
|
|
492
|
+
|
|
493
|
+
describe '.sync_file' do
|
|
494
|
+
it 'copies one file into a directory, healing and skipping as .sync does' do
|
|
495
|
+
write( File.join( @src, 'ceedling.h' ), 'v1' )
|
|
496
|
+
FileUtils.mkdir_p( File.join( @dst, 'ceedling.h' ) )
|
|
497
|
+
|
|
498
|
+
first = FileTree.sync_file( File.join( @src, 'ceedling.h' ), @dst, publish: FileTreeHelper::PUBLISH )
|
|
499
|
+
second = FileTree.sync_file( File.join( @src, 'ceedling.h' ), @dst, publish: FileTreeHelper::PUBLISH )
|
|
500
|
+
|
|
501
|
+
expect( [first.copied, first.healed, second.unchanged] ).to eq( [1, 1, 1] )
|
|
502
|
+
expect( File.read( File.join( @dst, 'ceedling.h' ) ) ).to eq( 'v1' )
|
|
503
|
+
end
|
|
504
|
+
|
|
505
|
+
it 'raises when the source is not a regular file' do
|
|
506
|
+
FileUtils.mkdir_p( File.join( @src, 'dir.h' ) )
|
|
507
|
+
|
|
508
|
+
expect { FileTree.sync_file( File.join( @src, 'dir.h' ), @dst, publish: FileTreeHelper::PUBLISH ) }.to raise_error( CeedlingException, /dir\.h/ )
|
|
509
|
+
end
|
|
510
|
+
end
|
|
511
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# =========================================================================
|
|
2
|
+
# Ceedling - Test-Centered Build System for C
|
|
3
|
+
# ThrowTheSwitch.org
|
|
4
|
+
# Copyright (c) 2010-26 Mike Karlesky, Mark VanderVoord, & Greg Williams
|
|
5
|
+
# SPDX-License-Identifier: MIT
|
|
6
|
+
# =========================================================================
|
|
7
|
+
|
|
8
|
+
require 'open3'
|
|
9
|
+
require 'spec_helper'
|
|
10
|
+
require 'file_tree_helper'
|
|
11
|
+
require 'ceedling/filesystem/file_tree'
|
|
12
|
+
|
|
13
|
+
# GH #1376: Windows reparse points. A junction is a link to a directory, and Ruby reports it
|
|
14
|
+
# as one. A file compressed by the Windows Overlay Filter (WOF) carries a reparse point that
|
|
15
|
+
# is not a link, like a deduplicated file. Each example skips itself when its runner cannot
|
|
16
|
+
# make the reparse point, saying why, and prints a canary line recording how Ruby classifies
|
|
17
|
+
# it. Hosted CI runners make no WOF file on any volume tried, so the WOF example skips there.
|
|
18
|
+
describe 'FileTree on Windows reparse points', filesystem: true, if: Gem.win_platform? do
|
|
19
|
+
include FileTreeHelper
|
|
20
|
+
|
|
21
|
+
around(:each) do |example|
|
|
22
|
+
with_tree_root do |root|
|
|
23
|
+
@root = root
|
|
24
|
+
@src = File.join( root, 'src' )
|
|
25
|
+
@dst = File.join( root, 'dst' )
|
|
26
|
+
example.run
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def windows(path)
|
|
31
|
+
path.tr( '/', '\\' )
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Whether a command succeeded, and a line saying how it ended, for a skip to report
|
|
35
|
+
def run(*command)
|
|
36
|
+
output, status = Open3.capture2e( *command )
|
|
37
|
+
return status.success?, "`#{command.join( ' ' )}` exited #{status.exitstatus}: #{output.split.join( ' ' )[0, 300]}"
|
|
38
|
+
rescue SystemCallError => e
|
|
39
|
+
return false, "`#{command.first}` could not run: #{e.message}"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def canary(label, path)
|
|
43
|
+
$stdout.puts "\n[canary] #{label}: symlink?=#{File.symlink?( path )} lstat.ftype=#{File.lstat( path ).ftype} stat.ftype=#{File.stat( path ).ftype}"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
it 'descends a junction to a directory' do
|
|
47
|
+
skip 'Windows makes junctions only on local NTFS volumes, not on a UNC share' if @root.start_with?( '//', '\\\\' )
|
|
48
|
+
|
|
49
|
+
write( File.join( @root, 'elsewhere/inner.c' ), 'int x;' )
|
|
50
|
+
FileUtils.mkdir_p( @src )
|
|
51
|
+
made, detail = run( 'cmd', '/c', 'mklink', '/J', windows( File.join( @src, 'junction' ) ), windows( File.join( @root, 'elsewhere' ) ) )
|
|
52
|
+
skip "could not make a junction: #{detail}" unless made
|
|
53
|
+
|
|
54
|
+
# The canary: records how Ruby classifies a junction. It asserts nothing.
|
|
55
|
+
canary( 'junction', File.join( @src, 'junction' ) )
|
|
56
|
+
|
|
57
|
+
FileTree.sync( @src, @dst, publish: FileTreeHelper::PUBLISH )
|
|
58
|
+
|
|
59
|
+
expect( File.read( File.join( @dst, 'junction/inner.c' ) ) ).to eq( 'int x;' )
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
it 'copies a WOF-compressed file as a regular file of its full size' do
|
|
63
|
+
path = File.join( @src, 'unity.c' )
|
|
64
|
+
write( path, 'x' * 100_000 )
|
|
65
|
+
compressed, detail = run( 'compact', '/c', '/exe:lzx', '/f', windows( path ) )
|
|
66
|
+
skip "compact /exe failed: #{detail}; WOF driver: #{run( 'sc', 'query', 'wof' ).last}" unless compressed
|
|
67
|
+
tagged, query = run( 'fsutil', 'reparsepoint', 'query', windows( path ) )
|
|
68
|
+
skip "the file did not become a WOF reparse point: #{query}" unless tagged && query.match?( /0x80000017/i )
|
|
69
|
+
|
|
70
|
+
# The canary: records how Ruby classifies a non-link reparse point. It asserts nothing.
|
|
71
|
+
canary( 'WOF file', path )
|
|
72
|
+
|
|
73
|
+
FileTree.sync( @src, @dst, publish: FileTreeHelper::PUBLISH )
|
|
74
|
+
|
|
75
|
+
expect( File.file?( File.join( @dst, 'unity.c' ) ) ).to be( true )
|
|
76
|
+
expect( File.size( File.join( @dst, 'unity.c' ) ) ).to eq( 100_000 )
|
|
77
|
+
end
|
|
78
|
+
end
|