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.
Files changed (276) hide show
  1. checksums.yaml +4 -4
  2. data/GIT_COMMIT_SHA +1 -1
  3. data/README.md +11 -9
  4. data/Rakefile +2 -1
  5. data/assets/features/ceedling +1 -1
  6. data/assets/fixtures/test_source_file_duplicate_basenames/test_calc_typo.c +14 -0
  7. data/assets/fixtures/tests_with_duplicate_test_names/test_foo.c +21 -0
  8. data/bin/actionator.rb +6 -12
  9. data/bin/app_cfg.rb +14 -1
  10. data/bin/ceedling +13 -3
  11. data/bin/cli.rb +25 -6
  12. data/bin/cli_handler.rb +113 -73
  13. data/bin/cli_helper.rb +51 -78
  14. data/bin/installation_selector.rb +81 -0
  15. data/bin/launchinator.rb +69 -0
  16. data/bin/mixin_resolvinator.rb +26 -36
  17. data/bin/objects.yml +13 -0
  18. data/bin/path_validator.rb +25 -29
  19. data/bin/versionator.rb +47 -48
  20. data/ceedling.gemspec +19 -15
  21. data/docs/BreakingChanges.md +70 -1
  22. data/docs/Changelog.md +74 -3
  23. data/docs/KnownIssues.md +90 -4
  24. data/docs/ReleaseNotes.md +13 -0
  25. data/lib/ceedling/build_log_writer.rb +33 -0
  26. data/lib/ceedling/config/build_root_policy.rb +70 -0
  27. data/lib/ceedling/config/configurator.rb +14 -16
  28. data/lib/ceedling/config/configurator_builder.rb +58 -28
  29. data/lib/ceedling/config/configurator_paths_validator.rb +179 -0
  30. data/lib/ceedling/config/configurator_plugins.rb +1 -1
  31. data/lib/ceedling/config/configurator_setup.rb +15 -28
  32. data/lib/ceedling/config/configurator_validator.rb +1 -110
  33. data/lib/ceedling/defaults.rb +4 -4
  34. data/lib/ceedling/dependencies/README.md +18 -16
  35. data/lib/ceedling/dependencies/dependency_cache_store.rb +20 -3
  36. data/lib/ceedling/dependencies/dependency_hasher.rb +35 -1
  37. data/lib/ceedling/filename_extension.rb +7 -1
  38. data/lib/ceedling/filesystem/FILE_TREE.md +253 -0
  39. data/lib/ceedling/filesystem/PATH_HANDLING.md +304 -0
  40. data/lib/ceedling/filesystem/README.md +41 -0
  41. data/lib/ceedling/filesystem/build_subpath.rb +102 -0
  42. data/lib/ceedling/filesystem/destination_collision.rb +25 -0
  43. data/lib/ceedling/{file_finder.rb → filesystem/file_finder.rb} +159 -104
  44. data/lib/ceedling/{file_finder_helper.rb → filesystem/file_finder_helper.rb} +36 -31
  45. data/lib/ceedling/filesystem/file_path_collection_utils.rb +183 -0
  46. data/lib/ceedling/{file_path_utils.rb → filesystem/file_path_utils.rb} +108 -97
  47. data/lib/ceedling/{file_system_wrapper.rb → filesystem/file_system_wrapper.rb} +6 -5
  48. data/lib/ceedling/filesystem/file_tree.rb +241 -0
  49. data/lib/ceedling/{file_wrapper.rb → filesystem/file_wrapper.rb} +45 -38
  50. data/lib/ceedling/filesystem/path_exclusions.rb +30 -0
  51. data/lib/ceedling/{path_matcher.rb → filesystem/path_matcher.rb} +27 -23
  52. data/lib/ceedling/{path_mirror.rb → filesystem/path_mirror.rb} +19 -18
  53. data/lib/ceedling/filesystem/path_spellings.rb +55 -0
  54. data/lib/ceedling/filesystem/project_paths.rb +184 -0
  55. data/lib/ceedling/filesystem/working_dir.rb +40 -0
  56. data/lib/ceedling/generators/generator.rb +5 -6
  57. data/lib/ceedling/generators/generator_partials.rb +1 -1
  58. data/lib/ceedling/generators/generator_test_results_backtrace.rb +7 -7
  59. data/lib/ceedling/handoff.rb +121 -0
  60. data/lib/ceedling/includes/include_factory.rb +3 -3
  61. data/lib/ceedling/includes/include_pathinator.rb +67 -10
  62. data/lib/ceedling/includes/includes.rb +1 -1
  63. data/lib/ceedling/installation.rb +92 -0
  64. data/lib/ceedling/library_resolver.rb +99 -0
  65. data/lib/ceedling/objects.yml +23 -2
  66. data/lib/ceedling/partials/partializer.rb +4 -4
  67. data/lib/ceedling/partials/partializer_config.rb +3 -1
  68. data/lib/ceedling/plugins/report_log_writer_plugin.rb +3 -5
  69. data/lib/ceedling/preprocess/preprocessinator_file_assembler.rb +1 -1
  70. data/lib/ceedling/preprocess/preprocessinator_includes_handler.rb +1 -1
  71. data/lib/ceedling/preprocess/preprocessinator_line_marker_includes_extractor.rb +1 -1
  72. data/lib/ceedling/rake_app/rakefile_component_resolver.rb +10 -17
  73. data/lib/ceedling/rakefile.rb +15 -1
  74. data/lib/ceedling/rakefiles/base/tasks_base.rake +1 -1
  75. data/lib/ceedling/rakefiles/release/rules_release.rake +4 -7
  76. data/lib/ceedling/release_invoker/release_build_executor.rb +33 -44
  77. data/lib/ceedling/release_invoker/release_build_planner.rb +14 -6
  78. data/lib/ceedling/reportinator.rb +3 -4
  79. data/lib/ceedling/setupinator.rb +94 -90
  80. data/lib/ceedling/test_context_extractor.rb +5 -1
  81. data/lib/ceedling/test_invoker/PIPELINE.md +9 -7
  82. data/lib/ceedling/test_invoker/mock_locator.rb +60 -2
  83. data/lib/ceedling/test_invoker/partials_manager.rb +12 -0
  84. data/lib/ceedling/test_invoker/standin_headerinator.rb +22 -13
  85. data/lib/ceedling/test_invoker/test_build_executor.rb +24 -40
  86. data/lib/ceedling/test_invoker/test_build_planner.rb +37 -18
  87. data/lib/ceedling/test_invoker/test_build_setup.rb +17 -12
  88. data/lib/ceedling/test_invoker/test_build_validations.rb +16 -31
  89. data/lib/ceedling/test_invoker/test_file_preprocessinator.rb +1 -1
  90. data/lib/ceedling/test_invoker/test_invoker.rb +6 -1
  91. data/lib/ceedling/test_invoker/test_pipeline_helpers.rb +1 -1
  92. data/lib/ceedling/test_invoker/test_source_file_directive_resolver.rb +40 -20
  93. data/lib/ceedling/tool_executor.rb +9 -3
  94. data/lib/ceedling/yaml_wrapper.rb +3 -2
  95. data/lib/version.rb +1 -1
  96. data/plugins/bullseye/config/defaults_bullseye.rb +1 -1
  97. data/plugins/bullseye/lib/bullseye.rb +9 -7
  98. data/plugins/cppcheck/config/defaults_cppcheck.rb +1 -1
  99. data/plugins/dependencies/config/defaults.yml +6 -6
  100. data/plugins/dependencies/dependencies.rake +4 -1
  101. data/plugins/dependencies/lib/dependencies.rb +18 -8
  102. data/plugins/dependencies/lib/dependency_spec.rb +17 -5
  103. data/plugins/fff/lib/fff.rb +45 -39
  104. data/plugins/fff/spec/fff_cmock_wrapper_spec.rb +128 -0
  105. data/plugins/gcov/config/defaults_gcov.rb +1 -1
  106. data/plugins/gcov/lib/console_reportinator.rb +4 -18
  107. data/plugins/gcov/lib/gcov.rb +9 -2
  108. data/plugins/gcov/lib/gcovr_reportinator.rb +8 -40
  109. data/plugins/gcov/lib/gcovr_thresholds.rb +74 -0
  110. data/plugins/gcov/lib/reportgenerator_reportinator.rb +1 -1
  111. data/plugins/module_generator/lib/module_generator.rb +3 -3
  112. data/plugins/report_tests_teamcity_stdout/lib/report_tests_teamcity_stdout.rb +53 -80
  113. data/plugins/sanitize/lib/sanitize_records.rb +3 -6
  114. data/plugins/valgrind/lib/valgrind.rb +14 -7
  115. data/site-local/configuration/reference/cmock.html +2 -1
  116. data/site-local/configuration/reference/files.html +9 -3
  117. data/site-local/configuration/reference/libraries.html +21 -3
  118. data/site-local/configuration/reference/paths.html +29 -6
  119. data/site-local/configuration/reference/project.html +17 -7
  120. data/site-local/configuration/reference/release-build.html +2 -0
  121. data/site-local/configuration/reference/tools.html +9 -0
  122. data/site-local/configuration/which-ceedling.html +377 -17
  123. data/site-local/development/releases.html +5 -1
  124. data/site-local/development/workflow.html +48 -43
  125. data/site-local/getting-started/builds.html +2 -0
  126. data/site-local/getting-started/command-line.html +56 -4
  127. data/site-local/plugins/dependencies.html +6 -3
  128. data/site-local/plugins/fff.html +42 -11
  129. data/site-local/plugins/gcov/gcovr.html +15 -12
  130. data/site-local/plugins/gcov/setup.html +2 -1
  131. data/site-local/plugins/report-tests-raw-output-log.html +3 -1
  132. data/site-local/plugins/report-tests-teamcity-stdout.html +6 -3
  133. data/site-local/plugins/sanitize.html +7 -6
  134. data/site-local/plugins/valgrind.html +8 -7
  135. data/site-local/project/upgrade.html +3 -0
  136. data/site-local/reference/command-line.html +9 -2
  137. data/site-local/sitemap.xml.gz +0 -0
  138. data/site-local/snapshot/plugins/bullseye/config/defaults_bullseye.rb +1 -1
  139. data/site-local/snapshot/plugins/gcov/config/defaults_gcov.rb +1 -1
  140. data/site-local/testing-guide/conventions.html +53 -0
  141. data/spec/integration/actionator_file_operations_spec.rb +91 -1
  142. data/spec/integration/compile_commands_json_db_spec.rb +1 -1
  143. data/spec/integration/configurator_validation_spec.rb +1 -1
  144. data/spec/integration/dependencies/dependency_tracker_cache_hardening_spec.rb +26 -0
  145. data/spec/integration/dependencies/dependency_tracker_core_spec.rb +22 -0
  146. data/spec/integration/launcher_handoff_spec.rb +297 -0
  147. data/spec/integration/mixin_loading_spec.rb +1 -1
  148. data/spec/integration/module_generator_spec.rb +3 -3
  149. data/spec/integration/nested_test_artifacts_spec.rb +99 -0
  150. data/spec/integration/partials_module_resolution_spec.rb +7 -4
  151. data/spec/integration/path_intake_scenarios_spec.rb +101 -0
  152. data/spec/integration/path_scenarios_spec.rb +322 -0
  153. data/spec/integration/quote_include_isolator_spec.rb +1 -1
  154. data/spec/integration/report_build_warnings_log_spec.rb +1 -1
  155. data/spec/integration/report_tests_log_factory_spec.rb +1 -1
  156. data/spec/integration/test_executable_manifest_spec.rb +1 -1
  157. data/spec/integration/test_includes_extractinator_spec.rb +2 -1
  158. data/spec/integration/test_naming_spec.rb +198 -0
  159. data/spec/integration/test_source_file_checks_spec.rb +166 -0
  160. data/spec/integration/tool_validator_spec.rb +1 -1
  161. data/spec/integration/upgrade_docs_spec.rb +84 -0
  162. data/spec/integration/vendor_copy_transient_io_retry_spec.rb +74 -5
  163. data/spec/support/file_tree_helper.rb +88 -0
  164. data/spec/support/integration/configurator_integration_helper.rb +6 -6
  165. data/spec/support/integration/dependency_tracker_integration_helper.rb +1 -1
  166. data/spec/support/integration/partials_generation_integration_helper.rb +2 -2
  167. data/spec/support/integration/path_scenario_harness.rb +173 -0
  168. data/spec/support/integration/spec_integration_helper.rb +1 -1
  169. data/spec/support/integration/test_includes_harness.rb +13 -2
  170. data/spec/support/spec_helper.rb +2 -0
  171. data/spec/support/system/spec_system_helper.rb +7 -6
  172. data/spec/support/system/system_context.rb +21 -3
  173. data/spec/support/working_dir_tracking.rb +49 -0
  174. data/spec/system/build_output_mirroring_spec.rb +2 -2
  175. data/spec/system/conditional_mock_includes_spec.rb +1 -1
  176. data/spec/system/delta_builds_spec.rb +137 -11
  177. data/spec/system/gcov_deployment_spec.rb +1 -0
  178. data/spec/system/header_include_path_validation_spec.rb +4 -4
  179. data/spec/system/launcher_handoff_spec.rb +82 -0
  180. data/spec/system/mock_include_folders_spec.rb +91 -0
  181. data/spec/system/parent_directory_reference_spec.rb +5 -3
  182. data/spec/system/path_handling_probes_spec.rb +192 -0
  183. data/spec/system/preprocessing_parameterized_tests_spec.rb +1 -1
  184. data/spec/system/rake_release_task_path_disambiguation_spec.rb +6 -6
  185. data/spec/system/rake_test_task_path_disambiguation_spec.rb +85 -0
  186. data/spec/system/special_path_characters_spec.rb +86 -0
  187. data/spec/system/support/common_test_cases.rb +23 -21
  188. data/spec/system/support/gcov_common_test_cases.rb +37 -9
  189. data/spec/system/support/sanitize_common_test_cases.rb +2 -2
  190. data/spec/system/support/valgrind_common_test_cases.rb +12 -12
  191. data/spec/system/test_pipeline_stop_points_spec.rb +4 -4
  192. data/spec/system/test_runner_system_include_spec.rb +1 -1
  193. data/spec/system/test_source_file_directive_spec.rb +34 -5
  194. data/spec/units/bin/actionator_spec.rb +14 -17
  195. data/spec/units/bin/app_cfg_spec.rb +37 -0
  196. data/spec/units/bin/cli_handler_spec.rb +282 -6
  197. data/spec/units/bin/cli_helper_spec.rb +116 -45
  198. data/spec/units/bin/installation_selector_spec.rb +114 -0
  199. data/spec/units/bin/launchinator_spec.rb +171 -0
  200. data/spec/units/bin/path_validator_spec.rb +10 -0
  201. data/spec/units/bin/versionator_spec.rb +38 -6
  202. data/spec/units/build_log_writer_spec.rb +57 -0
  203. data/spec/units/config/build_root_policy_spec.rb +61 -0
  204. data/spec/units/config/configurator_builder_spec.rb +37 -6
  205. data/spec/units/config/configurator_paths_validator_spec.rb +292 -0
  206. data/spec/units/config/configurator_setup_spec.rb +9 -65
  207. data/spec/units/config/configurator_spec.rb +27 -5
  208. data/spec/units/config/configurator_validator_spec.rb +1 -172
  209. data/spec/units/default_tools_quoting_spec.rb +83 -0
  210. data/spec/units/dependencies/dependency_cache_store_spec.rb +33 -2
  211. data/spec/units/dependencies/dependency_debug_tree_spec.rb +1 -1
  212. data/spec/units/dependencies/dependency_hasher_spec.rb +42 -1
  213. data/spec/units/dependencies/dependency_path_normalizer_spec.rb +1 -1
  214. data/spec/units/dependencies/dependency_tracker_spec.rb +3 -1
  215. data/spec/units/filename_extension_spec.rb +11 -0
  216. data/spec/units/filesystem/build_subpath_spec.rb +122 -0
  217. data/spec/units/filesystem/destination_collision_spec.rb +30 -0
  218. data/spec/units/{file_finder_helper_spec.rb → filesystem/file_finder_helper_spec.rb} +46 -1
  219. data/spec/units/{file_finder_spec.rb → filesystem/file_finder_spec.rb} +188 -22
  220. data/spec/units/{file_path_collection_utils_spec.rb → filesystem/file_path_collection_utils_spec.rb} +181 -1
  221. data/spec/units/{file_path_utils_spec.rb → filesystem/file_path_utils_spec.rb} +204 -87
  222. data/spec/units/filesystem/file_system_wrapper_spec.rb +33 -0
  223. data/spec/units/filesystem/file_tree_spec.rb +511 -0
  224. data/spec/units/filesystem/file_tree_windows_spec.rb +78 -0
  225. data/spec/units/{file_wrapper_spec.rb → filesystem/file_wrapper_spec.rb} +95 -12
  226. data/spec/units/{path_matcher_spec.rb → filesystem/path_matcher_spec.rb} +12 -1
  227. data/spec/units/{path_mirror_spec.rb → filesystem/path_mirror_spec.rb} +32 -4
  228. data/spec/units/filesystem/project_paths_spec.rb +147 -0
  229. data/spec/units/filesystem/working_dir_spec.rb +71 -0
  230. data/spec/units/generators/generator_spec.rb +12 -6
  231. data/spec/units/generators/generator_test_results_backtrace_spec.rb +50 -46
  232. data/spec/units/generators/generator_test_results_spec.rb +1 -1
  233. data/spec/units/handoff_spec.rb +205 -0
  234. data/spec/units/includes/include_factory_spec.rb +3 -3
  235. data/spec/units/includes/include_pathinator_spec.rb +46 -0
  236. data/spec/units/installation_spec.rb +228 -0
  237. data/spec/units/library_resolver_spec.rb +157 -0
  238. data/spec/units/mock_locator_spec.rb +76 -1
  239. data/spec/units/partials/partializer_config_spec.rb +7 -0
  240. data/spec/units/partials/partializer_spec.rb +1 -0
  241. data/spec/units/partials_manager_spec.rb +17 -0
  242. data/spec/units/plugins/bullseye_spec.rb +15 -2
  243. data/spec/units/plugins/console_reportinator_spec.rb +18 -20
  244. data/spec/units/plugins/dependencies_spec.rb +33 -7
  245. data/spec/units/plugins/dependency_spec_spec.rb +17 -0
  246. data/spec/units/plugins/gcov_spec.rb +28 -3
  247. data/spec/units/plugins/gcovr_reportinator_spec.rb +19 -47
  248. data/spec/units/plugins/gcovr_thresholds_spec.rb +95 -0
  249. data/spec/units/plugins/module_generator_spec.rb +5 -5
  250. data/spec/units/plugins/report_build_warnings_log_spec.rb +3 -3
  251. data/spec/units/plugins/report_log_writer_plugin_spec.rb +10 -1
  252. data/spec/units/plugins/report_tests_raw_output_log_spec.rb +13 -2
  253. data/spec/units/plugins/report_tests_teamcity_stdout_spec.rb +77 -3
  254. data/spec/units/plugins/reportgenerator_reportinator_spec.rb +1 -1
  255. data/spec/units/plugins/sanitize_records_spec.rb +3 -1
  256. data/spec/units/plugins/sanitize_spec.rb +1 -1
  257. data/spec/units/plugins/valgrind_spec.rb +23 -8
  258. data/spec/units/preprocess/preprocessinator_comment_stripper_spec.rb +1 -1
  259. data/spec/units/rake_app/rakefile_component_resolver_spec.rb +21 -0
  260. data/spec/units/release_build_executor_spec.rb +53 -14
  261. data/spec/units/release_build_planner_spec.rb +15 -3
  262. data/spec/units/reportinator_spec.rb +5 -5
  263. data/spec/units/standin_headerinator_spec.rb +31 -1
  264. data/spec/units/test_build_executor_spec.rb +65 -3
  265. data/spec/units/test_build_planner_spec.rb +104 -2
  266. data/spec/units/test_build_setup_spec.rb +96 -12
  267. data/spec/units/test_context_extractor_spec.rb +25 -0
  268. data/spec/units/test_file_preprocessinator_spec.rb +17 -1
  269. data/spec/units/test_includes_extractinator_spec.rb +5 -1
  270. data/spec/units/test_invoker/objects_wiring_spec.rb +6 -3
  271. data/spec/units/test_invoker_spec.rb +14 -1
  272. data/spec/units/test_source_file_directive_resolver_spec.rb +38 -7
  273. data/spec/units/tool_executor_spec.rb +26 -0
  274. data/spec/units/yaml_wrapper_spec.rb +11 -0
  275. metadata +109 -25
  276. 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