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.
Files changed (383) hide show
  1. checksums.yaml +4 -4
  2. data/GIT_COMMIT_SHA +1 -1
  3. data/README.md +57 -58
  4. data/Rakefile +5 -3
  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 +35 -1
  10. data/bin/ceedling +13 -3
  11. data/bin/cli.rb +25 -6
  12. data/bin/cli_handler.rb +119 -73
  13. data/bin/cli_helper.rb +56 -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 +84 -3
  23. data/docs/KnownIssues.md +90 -4
  24. data/docs/ReleaseNotes.md +13 -0
  25. data/docs/overrides/main.html +11 -8
  26. data/lib/ceedling/build_log_writer.rb +33 -0
  27. data/lib/ceedling/config/build_root_policy.rb +70 -0
  28. data/lib/ceedling/config/configurator.rb +19 -18
  29. data/lib/ceedling/config/configurator_builder.rb +58 -28
  30. data/lib/ceedling/config/configurator_paths_validator.rb +179 -0
  31. data/lib/ceedling/config/configurator_plugins.rb +1 -1
  32. data/lib/ceedling/config/configurator_setup.rb +39 -41
  33. data/lib/ceedling/config/configurator_validator.rb +1 -110
  34. data/lib/ceedling/defaults.rb +4 -4
  35. data/lib/ceedling/dependencies/README.md +18 -16
  36. data/lib/ceedling/dependencies/dependency_cache_store.rb +20 -3
  37. data/lib/ceedling/dependencies/dependency_hasher.rb +35 -1
  38. data/lib/ceedling/filename_extension.rb +7 -1
  39. data/lib/ceedling/filesystem/FILE_TREE.md +253 -0
  40. data/lib/ceedling/filesystem/PATH_HANDLING.md +304 -0
  41. data/lib/ceedling/filesystem/README.md +41 -0
  42. data/lib/ceedling/filesystem/build_subpath.rb +102 -0
  43. data/lib/ceedling/filesystem/destination_collision.rb +25 -0
  44. data/lib/ceedling/{file_finder.rb → filesystem/file_finder.rb} +159 -104
  45. data/lib/ceedling/{file_finder_helper.rb → filesystem/file_finder_helper.rb} +36 -31
  46. data/lib/ceedling/filesystem/file_path_collection_utils.rb +183 -0
  47. data/lib/ceedling/{file_path_utils.rb → filesystem/file_path_utils.rb} +108 -97
  48. data/lib/ceedling/{file_system_wrapper.rb → filesystem/file_system_wrapper.rb} +6 -5
  49. data/lib/ceedling/filesystem/file_tree.rb +241 -0
  50. data/lib/ceedling/{file_wrapper.rb → filesystem/file_wrapper.rb} +65 -28
  51. data/lib/ceedling/filesystem/path_exclusions.rb +30 -0
  52. data/lib/ceedling/{path_matcher.rb → filesystem/path_matcher.rb} +27 -23
  53. data/lib/ceedling/{path_mirror.rb → filesystem/path_mirror.rb} +19 -18
  54. data/lib/ceedling/filesystem/path_spellings.rb +55 -0
  55. data/lib/ceedling/filesystem/project_paths.rb +184 -0
  56. data/lib/ceedling/filesystem/working_dir.rb +40 -0
  57. data/lib/ceedling/generators/generator.rb +5 -6
  58. data/lib/ceedling/generators/generator_partials.rb +1 -1
  59. data/lib/ceedling/generators/generator_test_results_backtrace.rb +7 -7
  60. data/lib/ceedling/handoff.rb +121 -0
  61. data/lib/ceedling/includes/include_factory.rb +3 -3
  62. data/lib/ceedling/includes/include_pathinator.rb +67 -10
  63. data/lib/ceedling/includes/includes.rb +1 -1
  64. data/lib/ceedling/installation.rb +92 -0
  65. data/lib/ceedling/library_resolver.rb +99 -0
  66. data/lib/ceedling/objects.yml +23 -2
  67. data/lib/ceedling/partials/partializer.rb +4 -4
  68. data/lib/ceedling/partials/partializer_config.rb +3 -1
  69. data/lib/ceedling/plugins/report_log_writer_plugin.rb +3 -5
  70. data/lib/ceedling/preprocess/preprocessinator_file_assembler.rb +1 -1
  71. data/lib/ceedling/preprocess/preprocessinator_includes_handler.rb +1 -1
  72. data/lib/ceedling/preprocess/preprocessinator_line_marker_includes_extractor.rb +1 -1
  73. data/lib/ceedling/rake_app/rakefile_component_resolver.rb +10 -17
  74. data/lib/ceedling/rake_app/test_task_name.rb +29 -0
  75. data/lib/ceedling/rakefile.rb +16 -1
  76. data/lib/ceedling/rakefiles/base/tasks_base.rake +1 -1
  77. data/lib/ceedling/rakefiles/release/rules_release.rake +4 -7
  78. data/lib/ceedling/rakefiles/tests/rules_tests.rake +3 -20
  79. data/lib/ceedling/rakefiles/tests/rules_tests_generate.rake +4 -30
  80. data/lib/ceedling/release_invoker/release_build_executor.rb +33 -44
  81. data/lib/ceedling/release_invoker/release_build_planner.rb +14 -6
  82. data/lib/ceedling/reportinator.rb +3 -4
  83. data/lib/ceedling/setupinator.rb +95 -87
  84. data/lib/ceedling/test_context_extractor.rb +5 -1
  85. data/lib/ceedling/test_invoker/PIPELINE.md +9 -7
  86. data/lib/ceedling/test_invoker/mock_locator.rb +60 -2
  87. data/lib/ceedling/test_invoker/partials_manager.rb +12 -0
  88. data/lib/ceedling/test_invoker/standin_headerinator.rb +22 -13
  89. data/lib/ceedling/test_invoker/test_build_executor.rb +42 -41
  90. data/lib/ceedling/test_invoker/test_build_planner.rb +37 -18
  91. data/lib/ceedling/test_invoker/test_build_setup.rb +17 -12
  92. data/lib/ceedling/test_invoker/test_build_validations.rb +16 -31
  93. data/lib/ceedling/test_invoker/test_file_preprocessinator.rb +1 -1
  94. data/lib/ceedling/test_invoker/test_invoker.rb +6 -1
  95. data/lib/ceedling/test_invoker/test_pipeline_helpers.rb +1 -1
  96. data/lib/ceedling/test_invoker/test_source_file_directive_resolver.rb +40 -20
  97. data/lib/ceedling/tool_executor.rb +9 -3
  98. data/lib/ceedling/yaml_wrapper.rb +3 -2
  99. data/lib/version.rb +1 -1
  100. data/plugins/bullseye/bullseye.rake +6 -9
  101. data/plugins/bullseye/config/defaults_bullseye.rb +1 -1
  102. data/plugins/bullseye/lib/bullseye.rb +9 -7
  103. data/plugins/cppcheck/config/defaults_cppcheck.rb +1 -1
  104. data/plugins/dependencies/config/defaults.yml +6 -6
  105. data/plugins/dependencies/dependencies.rake +4 -1
  106. data/plugins/dependencies/lib/dependencies.rb +18 -8
  107. data/plugins/dependencies/lib/dependency_spec.rb +17 -5
  108. data/plugins/fff/lib/fff.rb +45 -39
  109. data/plugins/fff/spec/fff_cmock_wrapper_spec.rb +128 -0
  110. data/plugins/gcov/config/defaults_gcov.rb +1 -1
  111. data/plugins/gcov/gcov.rake +6 -9
  112. data/plugins/gcov/lib/console_reportinator.rb +4 -18
  113. data/plugins/gcov/lib/gcov.rb +9 -2
  114. data/plugins/gcov/lib/gcovr_reportinator.rb +8 -40
  115. data/plugins/gcov/lib/gcovr_thresholds.rb +74 -0
  116. data/plugins/gcov/lib/reportgenerator_reportinator.rb +1 -1
  117. data/plugins/module_generator/lib/module_generator.rb +3 -3
  118. data/plugins/report_tests_teamcity_stdout/lib/report_tests_teamcity_stdout.rb +53 -80
  119. data/plugins/sanitize/lib/sanitize_records.rb +3 -6
  120. data/plugins/sanitize/sanitize.rake +6 -6
  121. data/plugins/valgrind/lib/valgrind.rb +14 -7
  122. data/plugins/valgrind/valgrind.rake +6 -6
  123. data/site-local/404.html +6 -20
  124. data/site-local/assets/javascripts/{bundle.d7400e89.min.js → bundle.c21346df.min.js} +3 -3
  125. data/site-local/assets/javascripts/{bundle.d7400e89.min.js.map → bundle.c21346df.min.js.map} +2 -2
  126. data/site-local/assets/javascripts/workers/{search.2c215733.min.js → search.a7633da7.min.js} +2 -2
  127. data/site-local/assets/javascripts/workers/{search.2c215733.min.js.map → search.a7633da7.min.js.map} +1 -1
  128. data/site-local/assets/stylesheets/main.57a0d79d.min.css +6 -0
  129. data/site-local/assets/stylesheets/main.57a0d79d.min.css.map +1 -0
  130. data/site-local/assets/stylesheets/palette.fb2fffe2.min.css +4 -0
  131. data/site-local/assets/stylesheets/palette.fb2fffe2.min.css.map +1 -0
  132. data/site-local/configuration/environment-vars.html +6 -20
  133. data/site-local/configuration/global-collections.html +6 -20
  134. data/site-local/configuration/index.html +6 -20
  135. data/site-local/configuration/loading.html +6 -20
  136. data/site-local/configuration/mixins.html +10 -24
  137. data/site-local/configuration/parallel-builds.html +6 -20
  138. data/site-local/configuration/project-file.html +8 -22
  139. data/site-local/configuration/reference/cexception.html +6 -20
  140. data/site-local/configuration/reference/cmock.html +8 -21
  141. data/site-local/configuration/reference/defines.html +16 -30
  142. data/site-local/configuration/reference/environment.html +10 -24
  143. data/site-local/configuration/reference/extension.html +6 -20
  144. data/site-local/configuration/reference/files.html +22 -30
  145. data/site-local/configuration/reference/flags.html +13 -27
  146. data/site-local/configuration/reference/index.html +6 -20
  147. data/site-local/configuration/reference/libraries.html +27 -23
  148. data/site-local/configuration/reference/mixins.html +6 -20
  149. data/site-local/configuration/reference/partials.html +6 -20
  150. data/site-local/configuration/reference/paths.html +38 -29
  151. data/site-local/configuration/reference/plugins.html +6 -20
  152. data/site-local/configuration/reference/project.html +32 -36
  153. data/site-local/configuration/reference/release-build.html +8 -20
  154. data/site-local/configuration/reference/test-build.html +6 -20
  155. data/site-local/configuration/reference/test-runner.html +6 -20
  156. data/site-local/configuration/reference/tools.html +27 -32
  157. data/site-local/configuration/reference/unity.html +6 -20
  158. data/site-local/configuration/which-ceedling.html +383 -37
  159. data/site-local/development/index.html +6 -20
  160. data/site-local/development/plugins/configuration.html +6 -20
  161. data/site-local/development/plugins/index.html +6 -20
  162. data/site-local/development/plugins/plugin-subclass.html +54 -68
  163. data/site-local/development/plugins/rake-tasks.html +8 -22
  164. data/site-local/development/releases.html +84 -29
  165. data/site-local/development/workflow.html +62 -71
  166. data/site-local/getting-started/builds.html +8 -20
  167. data/site-local/getting-started/command-line.html +62 -24
  168. data/site-local/getting-started/example-projects/cipher-quest.html +6 -20
  169. data/site-local/getting-started/example-projects/index.html +6 -20
  170. data/site-local/getting-started/example-projects/temp-sensor.html +6 -20
  171. data/site-local/getting-started/example-projects/wondrous-forest.html +6 -20
  172. data/site-local/getting-started/index.html +6 -20
  173. data/site-local/getting-started/installation.html +6 -20
  174. data/site-local/getting-started/quick-start.html +6 -20
  175. data/site-local/help.html +6 -20
  176. data/site-local/index.html +11 -24
  177. data/site-local/overview/build-system.html +6 -20
  178. data/site-local/overview/index.html +6 -20
  179. data/site-local/overview/test-environments.html +6 -20
  180. data/site-local/overview/tools-and-frameworks.html +6 -20
  181. data/site-local/plugins/beep.html +6 -20
  182. data/site-local/plugins/bullseye/index.html +13 -24
  183. data/site-local/plugins/bullseye/licensing.html +14 -28
  184. data/site-local/plugins/command-hooks.html +8 -22
  185. data/site-local/plugins/compile-commands-json-db.html +6 -20
  186. data/site-local/plugins/cppcheck.html +8 -22
  187. data/site-local/plugins/dependencies.html +16 -27
  188. data/site-local/plugins/fff.html +49 -32
  189. data/site-local/plugins/gcov/examples.html +7 -21
  190. data/site-local/plugins/gcov/gcovr.html +22 -33
  191. data/site-local/plugins/gcov/index.html +6 -20
  192. data/site-local/plugins/gcov/overview.html +9 -20
  193. data/site-local/plugins/gcov/reportgenerator.html +9 -23
  194. data/site-local/plugins/gcov/reporting.html +6 -20
  195. data/site-local/plugins/gcov/setup.html +8 -21
  196. data/site-local/plugins/gcov/tool-versions.html +6 -20
  197. data/site-local/plugins/gcov/troubleshooting.html +10 -24
  198. data/site-local/plugins/index.html +6 -20
  199. data/site-local/plugins/module-generator.html +21 -35
  200. data/site-local/plugins/report-build-warnings-log.html +53 -67
  201. data/site-local/plugins/report-tests-gtestlike-stdout.html +6 -20
  202. data/site-local/plugins/report-tests-ide-stdout.html +43 -57
  203. data/site-local/plugins/report-tests-log-factory.html +50 -64
  204. data/site-local/plugins/report-tests-pretty-stdout.html +7 -21
  205. data/site-local/plugins/report-tests-raw-output-log.html +9 -21
  206. data/site-local/plugins/report-tests-teamcity-stdout.html +20 -31
  207. data/site-local/plugins/sanitize.html +56 -65
  208. data/site-local/plugins/valgrind.html +49 -58
  209. data/site-local/project/index.html +6 -20
  210. data/site-local/project/release-history.html +6 -20
  211. data/site-local/project/sbom.html +6 -20
  212. data/site-local/project/upgrade.html +9 -20
  213. data/site-local/reference/build-directives.html +6 -20
  214. data/site-local/reference/command-line.html +15 -22
  215. data/site-local/reference/environment-vars.html +6 -20
  216. data/site-local/reference/gcov-plugin.html +6 -20
  217. data/site-local/reference/global-collections.html +6 -20
  218. data/site-local/reference/index.html +6 -20
  219. data/site-local/reference/partials-macros.html +6 -20
  220. data/site-local/reference/project-configuration.html +6 -20
  221. data/site-local/sitemap.xml.gz +0 -0
  222. data/site-local/snapshot/examples/cipher_quest/index.html +16 -30
  223. data/site-local/snapshot/examples/temp_sensor/index.html +8 -22
  224. data/site-local/snapshot/examples/wondrous_forest/index.html +6 -20
  225. data/site-local/snapshot/plugins/bullseye/config/defaults_bullseye.rb +1 -1
  226. data/site-local/snapshot/plugins/gcov/config/defaults_gcov.rb +1 -1
  227. data/site-local/testing-guide/build-directives.html +14 -28
  228. data/site-local/testing-guide/conventions.html +66 -27
  229. data/site-local/testing-guide/frameworks.html +11 -25
  230. data/site-local/testing-guide/index.html +6 -20
  231. data/site-local/testing-guide/partials/configuration.html +8 -22
  232. data/site-local/testing-guide/partials/conventions.html +8 -22
  233. data/site-local/testing-guide/partials/directives.html +16 -30
  234. data/site-local/testing-guide/partials/example.html +14 -28
  235. data/site-local/testing-guide/partials/index.html +6 -20
  236. data/site-local/testing-guide/partials/overview.html +6 -20
  237. data/site-local/testing-guide/partials/variables.html +11 -25
  238. data/site-local/testing-guide/test-cases.html +13 -27
  239. data/site-local/testing-guide/test-sample.html +12 -26
  240. data/site-local/testing-guide/test-suite-anatomy.html +6 -20
  241. data/spec/integration/actionator_file_operations_spec.rb +91 -1
  242. data/spec/integration/compile_commands_json_db_spec.rb +1 -1
  243. data/spec/integration/configurator_pipeline_spec.rb +10 -0
  244. data/spec/integration/configurator_validation_spec.rb +1 -1
  245. data/spec/integration/dependencies/dependency_tracker_cache_hardening_spec.rb +26 -0
  246. data/spec/integration/dependencies/dependency_tracker_core_spec.rb +62 -0
  247. data/spec/integration/launcher_handoff_spec.rb +297 -0
  248. data/spec/integration/mixin_loading_spec.rb +1 -1
  249. data/spec/integration/module_generator_spec.rb +3 -3
  250. data/spec/integration/nested_test_artifacts_spec.rb +99 -0
  251. data/spec/integration/partials_module_resolution_spec.rb +7 -4
  252. data/spec/integration/path_intake_scenarios_spec.rb +101 -0
  253. data/spec/integration/path_scenarios_spec.rb +322 -0
  254. data/spec/integration/quote_include_isolator_spec.rb +1 -1
  255. data/spec/integration/rake_task_registry_plugins_spec.rb +95 -0
  256. data/spec/integration/report_build_warnings_log_spec.rb +1 -1
  257. data/spec/integration/report_tests_log_factory_spec.rb +1 -1
  258. data/spec/integration/test_executable_manifest_spec.rb +1 -1
  259. data/spec/integration/test_includes_extractinator_spec.rb +2 -1
  260. data/spec/integration/test_naming_spec.rb +198 -0
  261. data/spec/integration/test_source_file_checks_spec.rb +166 -0
  262. data/spec/integration/tool_validator_spec.rb +1 -1
  263. data/spec/integration/upgrade_docs_spec.rb +84 -0
  264. data/spec/integration/vendor_copy_transient_io_retry_spec.rb +192 -75
  265. data/spec/support/file_tree_helper.rb +88 -0
  266. data/spec/support/integration/configurator_integration_helper.rb +10 -7
  267. data/spec/support/integration/dependency_tracker_integration_helper.rb +1 -1
  268. data/spec/support/integration/partials_generation_integration_helper.rb +2 -2
  269. data/spec/support/integration/path_scenario_harness.rb +173 -0
  270. data/spec/support/integration/spec_integration_helper.rb +1 -1
  271. data/spec/support/integration/test_includes_harness.rb +13 -2
  272. data/spec/support/spec_helper.rb +2 -0
  273. data/spec/support/system/spec_system_helper.rb +7 -6
  274. data/spec/support/system/system_context.rb +21 -3
  275. data/spec/support/working_dir_tracking.rb +49 -0
  276. data/spec/system/build_output_mirroring_spec.rb +2 -2
  277. data/spec/system/conditional_mock_includes_spec.rb +1 -1
  278. data/spec/system/delta_builds_spec.rb +137 -11
  279. data/spec/system/gcov_deployment_spec.rb +1 -0
  280. data/spec/system/header_include_path_validation_spec.rb +4 -4
  281. data/spec/system/launcher_handoff_spec.rb +82 -0
  282. data/spec/system/mock_include_folders_spec.rb +91 -0
  283. data/spec/system/parent_directory_reference_spec.rb +5 -3
  284. data/spec/system/path_handling_probes_spec.rb +192 -0
  285. data/spec/system/preprocessing_parameterized_tests_spec.rb +1 -1
  286. data/spec/system/rake_release_task_path_disambiguation_spec.rb +6 -6
  287. data/spec/system/rake_test_task_path_disambiguation_spec.rb +85 -0
  288. data/spec/system/special_path_characters_spec.rb +86 -0
  289. data/spec/system/support/common_test_cases.rb +23 -21
  290. data/spec/system/support/gcov_common_test_cases.rb +37 -9
  291. data/spec/system/support/sanitize_common_test_cases.rb +2 -2
  292. data/spec/system/support/valgrind_common_test_cases.rb +12 -12
  293. data/spec/system/test_pipeline_stop_points_spec.rb +4 -4
  294. data/spec/system/test_runner_system_include_spec.rb +1 -1
  295. data/spec/system/test_source_file_directive_spec.rb +34 -5
  296. data/spec/units/bin/actionator_spec.rb +14 -17
  297. data/spec/units/bin/app_cfg_spec.rb +47 -0
  298. data/spec/units/bin/cli_handler_spec.rb +284 -7
  299. data/spec/units/bin/cli_helper_spec.rb +126 -45
  300. data/spec/units/bin/installation_selector_spec.rb +114 -0
  301. data/spec/units/bin/launchinator_spec.rb +171 -0
  302. data/spec/units/bin/path_validator_spec.rb +10 -0
  303. data/spec/units/bin/versionator_spec.rb +38 -6
  304. data/spec/units/build_log_writer_spec.rb +57 -0
  305. data/spec/units/config/build_root_policy_spec.rb +61 -0
  306. data/spec/units/config/configurator_builder_spec.rb +37 -6
  307. data/spec/units/config/configurator_paths_validator_spec.rb +292 -0
  308. data/spec/units/config/configurator_setup_spec.rb +65 -103
  309. data/spec/units/config/configurator_spec.rb +37 -6
  310. data/spec/units/config/configurator_validator_spec.rb +1 -172
  311. data/spec/units/default_tools_quoting_spec.rb +83 -0
  312. data/spec/units/dependencies/dependency_cache_store_spec.rb +33 -2
  313. data/spec/units/dependencies/dependency_debug_tree_spec.rb +1 -1
  314. data/spec/units/dependencies/dependency_hasher_spec.rb +42 -1
  315. data/spec/units/dependencies/dependency_path_normalizer_spec.rb +1 -1
  316. data/spec/units/dependencies/dependency_tracker_spec.rb +3 -1
  317. data/spec/units/filename_extension_spec.rb +11 -0
  318. data/spec/units/filesystem/build_subpath_spec.rb +122 -0
  319. data/spec/units/filesystem/destination_collision_spec.rb +30 -0
  320. data/spec/units/{file_finder_helper_spec.rb → filesystem/file_finder_helper_spec.rb} +46 -1
  321. data/spec/units/{file_finder_spec.rb → filesystem/file_finder_spec.rb} +188 -22
  322. data/spec/units/{file_path_collection_utils_spec.rb → filesystem/file_path_collection_utils_spec.rb} +181 -1
  323. data/spec/units/{file_path_utils_spec.rb → filesystem/file_path_utils_spec.rb} +204 -87
  324. data/spec/units/filesystem/file_system_wrapper_spec.rb +33 -0
  325. data/spec/units/filesystem/file_tree_spec.rb +511 -0
  326. data/spec/units/filesystem/file_tree_windows_spec.rb +78 -0
  327. data/spec/units/{file_wrapper_spec.rb → filesystem/file_wrapper_spec.rb} +154 -32
  328. data/spec/units/{path_matcher_spec.rb → filesystem/path_matcher_spec.rb} +12 -1
  329. data/spec/units/{path_mirror_spec.rb → filesystem/path_mirror_spec.rb} +32 -4
  330. data/spec/units/filesystem/project_paths_spec.rb +147 -0
  331. data/spec/units/filesystem/working_dir_spec.rb +71 -0
  332. data/spec/units/generators/generator_spec.rb +12 -6
  333. data/spec/units/generators/generator_test_results_backtrace_spec.rb +50 -46
  334. data/spec/units/generators/generator_test_results_spec.rb +1 -1
  335. data/spec/units/handoff_spec.rb +205 -0
  336. data/spec/units/includes/include_factory_spec.rb +3 -3
  337. data/spec/units/includes/include_pathinator_spec.rb +46 -0
  338. data/spec/units/installation_spec.rb +228 -0
  339. data/spec/units/library_resolver_spec.rb +157 -0
  340. data/spec/units/mock_locator_spec.rb +76 -1
  341. data/spec/units/partials/partializer_config_spec.rb +7 -0
  342. data/spec/units/partials/partializer_spec.rb +1 -0
  343. data/spec/units/partials_manager_spec.rb +17 -0
  344. data/spec/units/plugins/bullseye_spec.rb +15 -2
  345. data/spec/units/plugins/console_reportinator_spec.rb +18 -20
  346. data/spec/units/plugins/dependencies_spec.rb +33 -7
  347. data/spec/units/plugins/dependency_spec_spec.rb +17 -0
  348. data/spec/units/plugins/gcov_spec.rb +28 -3
  349. data/spec/units/plugins/gcovr_reportinator_spec.rb +19 -47
  350. data/spec/units/plugins/gcovr_thresholds_spec.rb +95 -0
  351. data/spec/units/plugins/module_generator_spec.rb +5 -5
  352. data/spec/units/plugins/report_build_warnings_log_spec.rb +3 -3
  353. data/spec/units/plugins/report_log_writer_plugin_spec.rb +10 -1
  354. data/spec/units/plugins/report_tests_raw_output_log_spec.rb +13 -2
  355. data/spec/units/plugins/report_tests_teamcity_stdout_spec.rb +77 -3
  356. data/spec/units/plugins/reportgenerator_reportinator_spec.rb +1 -1
  357. data/spec/units/plugins/sanitize_records_spec.rb +3 -1
  358. data/spec/units/plugins/sanitize_spec.rb +1 -1
  359. data/spec/units/plugins/valgrind_spec.rb +23 -8
  360. data/spec/units/preprocess/preprocessinator_comment_stripper_spec.rb +1 -1
  361. data/spec/units/rake_app/rakefile_component_resolver_spec.rb +21 -0
  362. data/spec/units/rake_app/test_task_name_spec.rb +47 -0
  363. data/spec/units/release_build_executor_spec.rb +53 -14
  364. data/spec/units/release_build_planner_spec.rb +15 -3
  365. data/spec/units/reportinator_spec.rb +5 -5
  366. data/spec/units/standin_headerinator_spec.rb +31 -1
  367. data/spec/units/test_build_executor_spec.rb +107 -5
  368. data/spec/units/test_build_planner_spec.rb +104 -2
  369. data/spec/units/test_build_setup_spec.rb +96 -12
  370. data/spec/units/test_context_extractor_spec.rb +25 -0
  371. data/spec/units/test_file_preprocessinator_spec.rb +17 -1
  372. data/spec/units/test_includes_extractinator_spec.rb +5 -1
  373. data/spec/units/test_invoker/objects_wiring_spec.rb +6 -3
  374. data/spec/units/test_invoker_spec.rb +14 -1
  375. data/spec/units/test_source_file_directive_resolver_spec.rb +38 -7
  376. data/spec/units/tool_executor_spec.rb +26 -0
  377. data/spec/units/yaml_wrapper_spec.rb +11 -0
  378. metadata +122 -33
  379. data/lib/ceedling/file_path_collection_utils.rb +0 -164
  380. data/site-local/assets/stylesheets/main.ec1eaa64.min.css +0 -1
  381. data/site-local/assets/stylesheets/main.ec1eaa64.min.css.map +0 -1
  382. data/site-local/assets/stylesheets/palette.ab4e12ef.min.css +0 -1
  383. data/site-local/assets/stylesheets/palette.ab4e12ef.min.css.map +0 -1
@@ -0,0 +1,241 @@
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 'fileutils'
9
+ require 'ceedling/constants'
10
+ require 'ceedling/exceptions'
11
+
12
+ # Walks and copies trees of regular files without `lstat`. On some filesystems `lstat`
13
+ # reports a regular file as a directory, and any copy that classifies by it puts a directory
14
+ # where the file belongs. FileTree lists a directory with `Dir.children`, which reads names
15
+ # without glob syntax, and classifies each entry with `File.stat`, which follows links.
16
+ #
17
+ # Links in a source tree are followed, and a copy holds only regular files and directories.
18
+ # A link loop or a dangling link raises rather than copying part of a tree.
19
+ #
20
+ # This is best effort. It removes the dependence on `lstat`, and trusts `stat` in its place.
21
+ module FileTree
22
+
23
+ # Errors that clear on their own, such as a scanner or a sync client holding a file open.
24
+ # Any other error fails a copy at once.
25
+ TRANSIENT_ERRORS = %i[EACCES EBUSY EPERM EINTR].filter_map { |name| Errno.const_get( name ) if Errno.const_defined?( name ) }.freeze
26
+
27
+ Result = Struct.new( :copied, :unchanged, :healed, keyword_init: true )
28
+
29
+ # Yields each regular file below `root` as its path relative to `root` and its full path,
30
+ # sorted within each directory. Returns an Enumerator without a block. An entry whose name
31
+ # `skip` accepts is neither yielded nor entered.
32
+ def self.each_file(root, skip: nil)
33
+ return enum_for( :each_file, root, skip: skip ) unless block_given?
34
+
35
+ each_entry( root, skip: skip ) { |relative, path, directory| yield( relative, path ) unless directory }
36
+ end
37
+
38
+ # Yields each directory and regular file below `root`, a directory ahead of its contents,
39
+ # as its relative path, its full path, and whether it is a directory
40
+ def self.each_entry(root, skip: nil, &block)
41
+ raise CeedlingException, "'#{root}' is not a directory" unless File.directory?( root )
42
+
43
+ walk( root, nil, [real_path( root )], skip, &block )
44
+ end
45
+
46
+ # Brings every file below `source` up to date below `destination` and returns a Result. A
47
+ # file that matches its source is left alone. One that differs or is missing is written by
48
+ # `publish`, a callable given the source and destination paths. A directory where a file
49
+ # belongs, or a file where a directory belongs, is removed first. Files `destination` has
50
+ # beyond `source` are left alone.
51
+ #
52
+ # `publish` is retried after each of `retry_delays` on an error in TRANSIENT_ERRORS.
53
+ # `log`, when given, receives a message, a Verbosity, and a LogLabels value.
54
+ def self.sync(source, destination, publish:, retry_delays: [], sleep: Kernel.method( :sleep ), log: nil)
55
+ return Sync.new( publish: publish, retry_delays: retry_delays, sleep: sleep, log: log ).tree( source, destination )
56
+ end
57
+
58
+ # As .sync, for one regular file copied into `destination_dir`
59
+ def self.sync_file(source, destination_dir, publish:, retry_delays: [], sleep: Kernel.method( :sleep ), log: nil)
60
+ return Sync.new( publish: publish, retry_delays: retry_delays, sleep: sleep, log: log ).file( source, destination_dir )
61
+ end
62
+
63
+ def self.walk(dir, relative, ancestors, skip, &block)
64
+ names_in( dir ).each do |name|
65
+ next if skip&.call( name )
66
+
67
+ path = File.join( dir, name )
68
+ entry = relative ? File.join( relative, name ) : name
69
+ next yield( entry, path, false ) unless directory_entry?( path )
70
+
71
+ yield( entry, path, true )
72
+ walk( path, entry, ancestors + [real_directory( path, ancestors )], skip, &block )
73
+ end
74
+ end
75
+
76
+ def self.names_in(dir)
77
+ return Dir.children( dir ).sort
78
+ rescue SystemCallError => e
79
+ raise CeedlingException, "'#{dir}' could not be listed ⏩️ #{e.message}"
80
+ end
81
+
82
+ # Whether a listed entry is a directory. Anything but a directory or a regular file raises.
83
+ def self.directory_entry?(path)
84
+ status = stat_of( path )
85
+ return true if status.directory?
86
+ return false if status.file?
87
+
88
+ raise CeedlingException, "'#{path}' is a #{status.ftype}, not a file or directory"
89
+ end
90
+
91
+ def self.stat_of(path)
92
+ return File.stat( path )
93
+ rescue Errno::ENOENT, Errno::ELOOP
94
+ raise CeedlingException, "'#{path}' is a dangling or unresolvable link"
95
+ rescue SystemCallError => e
96
+ raise CeedlingException, "'#{path}' could not be read ⏩️ #{e.message}"
97
+ end
98
+
99
+ def self.real_directory(path, ancestors)
100
+ real = real_path( path )
101
+ raise CeedlingException, "Link loop: '#{path}' resolves to its own ancestor '#{real}'" if ancestors.include?( real )
102
+
103
+ return real
104
+ end
105
+
106
+ # A directory's real path, for finding loops. A path that cannot be resolved stands for
107
+ # itself, so the walk goes on without that safeguard rather than stopping.
108
+ def self.real_path(path)
109
+ return File.realpath( path )
110
+ rescue SystemCallError
111
+ return File.expand_path( path )
112
+ end
113
+
114
+ private_class_method :walk, :names_in, :directory_entry?, :stat_of, :real_directory, :real_path
115
+
116
+ # One copy's settings and tallies
117
+ class Sync
118
+ def initialize(publish:, retry_delays:, sleep:, log:)
119
+ @publish = publish
120
+ @retry_delays = retry_delays
121
+ @sleep = sleep
122
+ @log = log
123
+ @result = Result.new( copied: 0, unchanged: 0, healed: 0 )
124
+ end
125
+
126
+ def tree(source, destination)
127
+ raise CeedlingException, "'#{source}' is not a directory" unless File.directory?( source )
128
+
129
+ directory( destination )
130
+ FileTree.each_entry( source ) do |relative, path, is_directory|
131
+ target = File.join( destination, relative )
132
+ is_directory ? directory( target ) : publish_file( path, target )
133
+ end
134
+ return @result
135
+ end
136
+
137
+ def file(source, destination_dir)
138
+ raise CeedlingException, "'#{source}' is not a regular file" unless File.file?( source )
139
+
140
+ directory( destination_dir )
141
+ publish_file( source, File.join( destination_dir, File.basename( source ) ) )
142
+ return @result
143
+ end
144
+
145
+ private
146
+
147
+ # Destination checks follow links, as `stat` does, so they share the source side's view.
148
+ # A dangling link counts as a file.
149
+ def directory(path)
150
+ return if File.directory?( path )
151
+
152
+ with_retries( path ) do
153
+ heal( "Replaced a file where the directory '#{path}' belongs" ) { remove_file( path ) } if File.exist?( path ) || File.symlink?( path )
154
+ FileUtils.mkdir_p( path )
155
+ end
156
+ end
157
+
158
+ # Every step is retried, since a scanner or sync client can hold the destination too
159
+ def publish_file(source, destination)
160
+ with_retries( destination ) do
161
+ if File.directory?( destination )
162
+ heal( "Replaced a directory where the file '#{destination}' belongs" ) { remove_directory( destination ) }
163
+ elsif identical?( source, destination )
164
+ next @result.unchanged += 1
165
+ end
166
+
167
+ @publish.call( source, destination )
168
+ @result.copied += 1
169
+ end
170
+ end
171
+
172
+ # Counts and logs a heal the block reports it made
173
+ def heal(message)
174
+ return unless yield
175
+
176
+ @result.healed += 1
177
+ log( message, Verbosity::COMPLAIN, LogLabels::NOTICE )
178
+ end
179
+
180
+ # Whether this removed the file or link. An error removing it reaches the retries.
181
+ def remove_file(path)
182
+ File.unlink( path )
183
+ return true
184
+ rescue Errno::ENOENT
185
+ return false
186
+ end
187
+
188
+ # Whether this removed the directory, or the link to one. Another run healing the same
189
+ # entry may have removed it already, or published a file in its place, which this run
190
+ # then publishes over. An error removing it reaches the retries.
191
+ def remove_directory(path)
192
+ return remove_file( path ) if File.symlink?( path )
193
+
194
+ Dir.rmdir( path )
195
+ return true
196
+ rescue Errno::ENOENT, Errno::ENOTDIR
197
+ return false
198
+ rescue Errno::ENOTEMPTY, Errno::EEXIST
199
+ return remove_tree( path )
200
+ end
201
+
202
+ # FileUtils.rm_r classifies by `lstat`, so it is reached only for a directory with
203
+ # contents. It works on the destination, and never follows a link out of it.
204
+ def remove_tree(path)
205
+ FileUtils.rm_r( path )
206
+ return true
207
+ rescue Errno::ENOENT
208
+ return false
209
+ end
210
+
211
+ def identical?(source, destination)
212
+ return File.file?( destination ) && File.size( source ) == File.size( destination ) && FileUtils.compare_file( source, destination )
213
+ end
214
+
215
+ def with_retries(destination)
216
+ delays = @retry_delays.dup
217
+ begin
218
+ yield
219
+ rescue *TRANSIENT_ERRORS => e
220
+ raise_exhausted( destination, e ) if delays.empty?
221
+
222
+ log( "Retrying copy to '#{destination}' after a transient error: #{e.message}", Verbosity::DEBUG, LogLabels::AUTO )
223
+ @sleep.call( delays.shift )
224
+ retry
225
+ end
226
+ end
227
+
228
+ def raise_exhausted(destination, error)
229
+ message = "Failed to copy to '#{destination}' after #{@retry_delays.size} retries ⏩️ #{error.message}\n" \
230
+ "Try deleting this project's build/ directory and rebuilding -- or, if the error names a " \
231
+ "path under your Ruby gem install, reinstall the ceedling gem."
232
+ log( message, Verbosity::ERRORS, LogLabels::AUTO )
233
+ raise CeedlingException, message
234
+ end
235
+
236
+ def log(message, verbosity, label)
237
+ @log&.call( message, verbosity, label )
238
+ end
239
+ end
240
+
241
+ end
@@ -17,6 +17,7 @@ require 'digest/sha2'
17
17
  require 'ceedling/constants'
18
18
  require 'ceedling/system_wrapper'
19
19
  require 'ceedling/exceptions'
20
+ require 'ceedling/filesystem/file_tree'
20
21
 
21
22
 
22
23
  class FileWrapper
@@ -88,6 +89,13 @@ class FileWrapper
88
89
  return File.basename(path)
89
90
  end
90
91
 
92
+ # A file's status (size, modification time, ...), or nil when it can't be read
93
+ def stat(filepath)
94
+ return File.stat( filepath )
95
+ rescue SystemCallError
96
+ return nil
97
+ end
98
+
91
99
  def exist?(filepath)
92
100
  return true if (filepath == NULL_FILE_PATH)
93
101
  return File.exist?(filepath)
@@ -118,6 +126,13 @@ class FileWrapper
118
126
  return Dir.glob(glob, File::FNM_PATHNAME).sort()
119
127
  end
120
128
 
129
+ # Every regular file below dir as a full path, sorted. Unlike a glob, the walk classifies
130
+ # entries by `stat`, follows links, and reads names without pattern syntax (see FileTree).
131
+ # An entry whose name `skip` accepts is neither listed nor entered.
132
+ def tree_files(dir, skip: nil)
133
+ return FileTree.each_file( dir, skip: skip ).map { |_relative, path| path }.sort
134
+ end
135
+
121
136
  # Entry names within a single directory, `.` and `..` excluded.
122
137
  #
123
138
  # Complements `directory_listing()` for the case a glob cannot serve: matching a
@@ -238,34 +253,35 @@ class FileWrapper
238
253
  false
239
254
  end
240
255
 
241
- # As #cp_r, but retries the whole copy on a transient OS-level error instead of
242
- # surfacing it on the first attempt -- the same transient-visibility race
243
- # #open_with_retry guards against, here hitting FileUtils.cp_r's own internal
244
- # stat-then-open sequence on either the source or destination tree. A source/dest
245
- # entry briefly reporting the wrong type (a directory where a file was just stat'd,
246
- # or vice versa) mid-copy raises Errno::ENOTDIR/EISDIR from deep inside cp_r; a
247
- # retry a moment later re-walks the tree fresh rather than trusting stale type info
248
- # from the failed attempt. A failure that never clears still surfaces as a
249
- # CeedlingException naming the destination and how many attempts were made, not a
250
- # bare, unhelpful Errno.
251
- def cp_r_with_retry(source, destination, options={})
252
- attempts_remaining = TRANSIENT_IO_RETRY_DELAYS.dup
253
- begin
254
- cp_r(source, destination, options)
255
- rescue SystemCallError => e
256
- if attempts_remaining.empty?
257
- msg = "Failed to copy into '#{destination}' after #{TRANSIENT_IO_RETRY_DELAYS.size} retries ⏩️ #{e.message}\n" \
258
- "This path may be corrupted (a file exists where Ceedling expects a directory, or vice versa). " \
259
- "Try deleting this project's build/ directory and rebuilding -- or, if the error names a path " \
260
- "under your Ruby gem install, reinstall the ceedling gem."
261
- @loginator.log(msg, Verbosity::ERRORS)
262
- raise CeedlingException, msg
263
- end
264
- delay = attempts_remaining.shift
265
- @loginator.log("Retrying copy into '#{destination}' after a transient error: #{e.message}", Verbosity::DEBUG)
266
- sleep(delay)
267
- retry
268
- end
256
+ # Brings every file beneath source_dir up to date beneath destination_dir (see FileTree.sync).
257
+ # A file that already matches its source is not written, so a reader elsewhere never sees it
258
+ # change. One that differs or is missing is replaced whole (see #replace_file). An entry of
259
+ # the wrong type is replaced. Files the destination has beyond the source are left alone.
260
+ #
261
+ # A write is retried on an error that clears on its own, such as another process holding a
262
+ # file open on Windows. One that never clears surfaces as a CeedlingException naming the file.
263
+ def sync_tree(source_dir, destination_dir)
264
+ FileTree.sync( source_dir, destination_dir, **sync_options )
265
+ end
266
+
267
+ # As #sync_tree, for a single file copied into destination_dir
268
+ def sync_file(source, destination_dir)
269
+ FileTree.sync_file( source, destination_dir, **sync_options )
270
+ end
271
+
272
+ # Copies source to a temporary name beside destination, then renames it over destination.
273
+ # A concurrent reader sees the old file or the new one, never a partial one. The copy is
274
+ # owner-writable, and so is a destination file it replaces, since Windows refuses to rename
275
+ # over a read-only file. A destination link is replaced, and the file it names left alone.
276
+ def replace_file(source, destination)
277
+ temp = File.join( File.dirname( destination ), ".#{File.basename( destination )}.#{Process.pid}.#{Thread.current.object_id}.tmp" )
278
+ check_path_length( temp, origin: 'FileWrapper#replace_file' )
279
+ FileUtils.cp( source, temp )
280
+ owner_writable( temp )
281
+ owner_writable( destination ) if File.file?( destination ) && !File.symlink?( destination )
282
+ File.rename( temp, destination )
283
+ ensure
284
+ FileUtils.rm_f( temp ) if temp
269
285
  end
270
286
 
271
287
  def read(filepath, length=nil)
@@ -330,6 +346,13 @@ class FileWrapper
330
346
  return FileUtils.mkdir_p(folder)
331
347
  end
332
348
 
349
+ # Makes the directory a file is about to be written in and returns the filepath. Safe
350
+ # when build threads make the same directory at once.
351
+ def mkdir_for(filepath)
352
+ mkdir( File.dirname( filepath ) )
353
+ return filepath
354
+ end
355
+
333
356
  # Creates a uniquely-named, empty directory nested inside `parent` and returns its path.
334
357
  # `parent` must already exist. Collision-free even across concurrent callers targeting
335
358
  # the same `parent` -- Dir.mktmpdir retries internally on name clash.
@@ -371,6 +394,20 @@ class FileWrapper
371
394
 
372
395
  private
373
396
 
397
+ def sync_options
398
+ return {
399
+ publish: method( :replace_file ),
400
+ retry_delays: TRANSIENT_IO_RETRY_DELAYS,
401
+ sleep: ->( delay ) { sleep( delay ) },
402
+ log: ->( message, verbosity, label ) { @loginator.log( message, verbosity, label ) }
403
+ }
404
+ end
405
+
406
+ def owner_writable(path)
407
+ mode = File.stat( path ).mode & 0o7777
408
+ File.chmod( mode | 0o200, path ) if (mode & 0o200).zero?
409
+ end
410
+
374
411
  def path_length_limit
375
412
  return PATH_LENGTH_LIMITS[:windows] if SystemWrapper.windows?
376
413
  return PATH_LENGTH_LIMITS[:macos] if SystemWrapper.macos?
@@ -0,0 +1,30 @@
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 'set'
9
+ require 'ceedling/filesystem/path_spellings'
10
+
11
+ # What collection leaves out, each a set of spellings: `-:` entries' places, outweighed by
12
+ # an entry based nearer; excluded places, outweighed by an entry based no farther away; and
13
+ # reserved places, never collected
14
+ PathExclusions = Struct.new( :minus, :excluded, :reserved ) do
15
+ def self.of(minus, excluded, reserved)
16
+ return new( *[minus, excluded, reserved].map { |paths| PathSpellings.of( paths ) } )
17
+ end
18
+
19
+ def none?
20
+ return [minus, excluded, reserved].all?( &:empty? )
21
+ end
22
+
23
+ # Whether a place, by `spellings` of it and `distance` below the base of the entry naming
24
+ # it, is collected
25
+ def keeps?(spellings, distance)
26
+ return distance < PathSpellings.nearer( spellings, minus ) &&
27
+ distance <= PathSpellings.nearer( spellings, excluded ) &&
28
+ PathSpellings.nearer( spellings, reserved ) == Float::INFINITY
29
+ end
30
+ end
@@ -88,37 +88,40 @@ class PathMatcher
88
88
  query_segments = segments(query)
89
89
  return query unless query_segments.include?('..')
90
90
 
91
- if anchor.nil?
92
- raise CeedlingException.new(
93
- "Relative path reference '..' in '#{query}' has no file context to resolve against here."
94
- )
95
- end
91
+ require_anchor!(query, anchor)
96
92
 
97
93
  # segments() strips the empty string a leading "/" splits off, so a Unix
98
94
  # absolute anchor's own "/"-ness has to be remembered separately here and
99
95
  # reapplied below -- otherwise the result quietly turns into a relative-looking
100
96
  # path carrying the same text minus its leading "/".
101
- anchor_was_absolute = anchor.start_with?('/')
97
+ absolute = anchor.start_with?('/')
102
98
  resolved = segments(anchor)
103
-
104
- # A `..` beyond a relative anchor's own segments names a directory above it, as a
105
- # project's paths outside its own root do (`../shared`), so it stays in the result.
106
- # Nothing lies above an absolute anchor's root.
107
- query_segments.each do |segment|
108
- if segment != '..'
109
- resolved << segment
110
- elsif !resolved.empty? and resolved.last != '..'
111
- resolved.pop
112
- elsif anchor_was_absolute
113
- raise CeedlingException.new("Relative path reference '..' in '#{query}' climbs above the filesystem root.")
114
- else
115
- resolved << '..'
116
- end
117
- end
99
+ query_segments.each { |segment| step(resolved, segment, query, absolute) }
118
100
 
119
101
  joined = resolved.join('/')
120
- anchor_was_absolute ? "/#{joined}" : joined
102
+ absolute ? "/#{joined}" : joined
103
+ end
104
+
105
+ def self.require_anchor!(query, anchor)
106
+ return unless anchor.nil?
107
+
108
+ raise CeedlingException.new( "Relative path reference '..' in '#{query}' has no file context to resolve against here." )
109
+ end
110
+ private_class_method :require_anchor!
111
+
112
+ # Applies one query segment to the segments resolved so far. A `..` beyond a relative
113
+ # anchor's own segments names a directory above it, as a project's paths outside its own
114
+ # root do (`../shared`), so it stays in the result. Nothing lies above an absolute
115
+ # anchor's root.
116
+ def self.step(resolved, segment, query, absolute)
117
+ return resolved << segment if segment != '..'
118
+ return resolved.pop if !resolved.empty? and resolved.last != '..'
119
+
120
+ raise CeedlingException.new("Relative path reference '..' in '#{query}' climbs above the filesystem root.") if absolute
121
+
122
+ resolved << '..'
121
123
  end
124
+ private_class_method :step
122
125
 
123
126
  # Two filepaths correspond if the shorter one's path segments equal the longer
124
127
  # one's own trailing segments, exactly, in order -- checked without regard for
@@ -138,8 +141,9 @@ class PathMatcher
138
141
 
139
142
  ### Private ###
140
143
 
144
+ # A `.` segment names nothing, so it is dropped with the empty ones
141
145
  def self.segments(path)
142
- path.split(/[\\\/]/).reject(&:empty?)
146
+ path.split(/[\\\/]/).reject { |segment| segment.empty? || segment == '.' }
143
147
  end
144
148
  private_class_method :segments
145
149
 
@@ -5,7 +5,8 @@
5
5
  # SPDX-License-Identifier: MIT
6
6
  # =========================================================================
7
7
 
8
- require 'ceedling/file_path_utils'
8
+ require 'ceedling/filesystem/file_path_utils'
9
+ require 'ceedling/filesystem/project_paths'
9
10
 
10
11
  # Answers the question build output asks when it wants to avoid same-basename
11
12
  # collisions: given a resolved file and the raw (still possibly glob/decorator-laden)
@@ -23,12 +24,13 @@ class PathMirror
23
24
  end
24
25
 
25
26
  # Strips decorators and glob specifiers from each entry in `roots`, dropping any that
26
- # end up empty. A caller mirroring many files against the same `roots` list -- a whole
27
+ # end up empty and every `-:` exclusion, which names no place a file lives below. A caller mirroring many files against the same `roots` list -- a whole
27
28
  # build's worth of sources against one project's configured paths, say -- can compute
28
29
  # this once and feed the result to `relative_subdir_from_clean_roots` for every file,
29
30
  # rather than every file's own call re-parsing the same still-decorated list from scratch.
30
31
  def self.clean_roots(roots)
31
32
  roots
33
+ .select { |root| FilePathUtils.add_path?(root) }
32
34
  .map { |root| FilePathUtils.no_decorators(root) }
33
35
  .reject(&:empty?)
34
36
  end
@@ -38,12 +40,13 @@ class PathMirror
38
40
  # fallback for placing build output but hides the difference from a caller deciding
39
41
  # whether a path belongs to a configured tree at all.
40
42
  def self.relative_subdir_if_rooted(filepath, roots)
41
- clean = clean_roots( roots )
42
- file_dir = File.dirname( filepath )
43
-
44
- return nil unless clean.any? { |root| file_dir == root || file_dir.start_with?( root + '/' ) }
43
+ return relative_subdir_if_rooted_in_clean_roots( filepath, clean_roots( roots ) )
44
+ end
45
45
 
46
- return relative_subdir_from_clean_roots( filepath, clean )
46
+ # As `relative_subdir_if_rooted`, but takes roots already reduced by `clean_roots`, or
47
+ # canonical roots such as configuration's mirror roots
48
+ def self.relative_subdir_if_rooted_in_clean_roots(filepath, clean_roots)
49
+ return containing_subdir( File.dirname( filepath ), clean_roots )
47
50
  end
48
51
 
49
52
  # A path as lowercased segments, split on either separator. One spelling for every caller
@@ -56,18 +59,16 @@ class PathMirror
56
59
  # As `relative_subdir`, but takes roots already reduced by `clean_roots` instead of
57
60
  # raw, possibly glob/decorator-laden `:paths` entries.
58
61
  def self.relative_subdir_from_clean_roots(filepath, clean_roots)
59
- file_dir = File.dirname(filepath)
60
-
61
- matching_roots = clean_roots.select do |root|
62
- file_dir == root || file_dir.start_with?(root + '/')
63
- end
64
-
65
- return '' if matching_roots.empty?
66
-
67
- best_root = matching_roots.max_by(&:length)
68
- return '' if file_dir == best_root
62
+ return containing_subdir( File.dirname( filepath ), clean_roots ) || ''
63
+ end
69
64
 
70
- file_dir[(best_root.length + 1)..]
65
+ # The directories from the most specific root truly containing `dir` down to it, or nil
66
+ # when no root does. Roots and `dir` are compared as places, however each is spelled, so
67
+ # the result never climbs out of its root.
68
+ def self.containing_subdir(dir, clean_roots)
69
+ subdirs = clean_roots.map { |root| ProjectPaths.subdir_below( root, dir ) }.compact
70
+ return subdirs.min_by( &:length )
71
71
  end
72
+ private_class_method :containing_subdir
72
73
 
73
74
  end
@@ -0,0 +1,55 @@
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 'set'
9
+ require 'ceedling/filesystem/project_paths'
10
+
11
+ # A place reached through a symlink has two spellings: as listed and as it really is.
12
+ # Collection compares places by both, and measures how far one lies below another.
13
+ module PathSpellings
14
+
15
+ # A path as compared: Windows compares paths without regard to case
16
+ def self.key(path)
17
+ return ProjectPaths::WINDOWS ? path.downcase : path
18
+ end
19
+
20
+ # Both spellings of each of `paths`, as compared, so either reaches it
21
+ def self.of(paths)
22
+ return Set.new( paths.flat_map { |path| [ProjectPaths.absolute( path ), ProjectPaths.real_absolute( path )] }.map { |path| key( path ) } )
23
+ end
24
+
25
+ # How many directories above a place, by any of `spellings` of it, the nearest one in
26
+ # `spelled` lies: 0 for the place itself, or infinity for none
27
+ def self.nearer(spellings, spelled)
28
+ return Float::INFINITY if spelled.empty?
29
+
30
+ return spellings.map { |path| distance( path, spelled ) }.min
31
+ end
32
+
33
+ # `paths`, found below `base`, respelled below the base's real path
34
+ def self.through_real(paths, base)
35
+ real = ProjectPaths.real_absolute( base )
36
+ return paths if base == real
37
+
38
+ return paths.map { |path| ProjectPaths.same?( path, base ) || ProjectPaths.starts_with?( path, "#{base}/" ) ? real + path[base.length..] : path }
39
+ end
40
+
41
+ def self.distance(path, spelled)
42
+ steps = 0
43
+ loop do
44
+ return steps if spelled.include?( key( path ) )
45
+
46
+ parent = File.dirname( path )
47
+ return Float::INFINITY if parent == path
48
+
49
+ path = parent
50
+ steps += 1
51
+ end
52
+ end
53
+ private_class_method :distance
54
+
55
+ end