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,183 @@
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 'pathname'
10
+ require 'fileutils'
11
+ require 'ceedling/filesystem/file_path_utils'
12
+ require 'ceedling/exceptions'
13
+ require 'ceedling/filesystem/project_paths'
14
+ require 'ceedling/filesystem/path_spellings'
15
+ require 'ceedling/filesystem/path_exclusions'
16
+
17
+
18
+ class FilePathCollectionUtils
19
+
20
+ # One collected directory: its spelling through its entry's real base, both of its
21
+ # spellings, and how far below the nearest base of an entry naming it it lies
22
+ Named = Struct.new( :spelled, :spellings, :distance )
23
+
24
+ constructor :file_wrapper
25
+
26
+ # The directories one or more (+:/-:) paths and globs name, once each, spelled through
27
+ # each entry's base as the base really is, as an #include or a test name reaches them. A
28
+ # `-:` entry drops the directories it names and every directory below them, except one an
29
+ # additive entry naming it names nearer. Each path in `exclude` does so too, except where
30
+ # an entry naming a directory is based no farther away. Each path in `reserved`, and every
31
+ # directory below it, is never collected.
32
+ def collect_paths(paths, exclude: [], reserved: [])
33
+ # Nil input (e.g. unconfigured section) is treated as an empty list
34
+ return [] if paths.nil?
35
+
36
+ additive, subtractive = paths.partition { |entry| FilePathUtils.add_path?( entry ) }
37
+ minus = PathExclusions.of( directories_of( subtractive ), exclude, reserved )
38
+
39
+ kept = named_directories( additive ).select { |named| minus.keeps?( named.spellings, named.distance ) }
40
+ return kept.map { |named| ProjectPaths.relative( named.spelled ) }
41
+ end
42
+
43
+ # Given a file list, add to it or remove from it considering (+:/-:) aggregation operators.
44
+ # Rake's FileList does not robustly handle relative filepaths and patterns.
45
+ # So, we rebuild the FileList ourselves and return it. A revision adds no file below a
46
+ # path in `exclude` unless the revision is based no farther away, and none below a path in
47
+ # `reserved` at all.
48
+ def revise_filelist(list, revisions, exclude: [], reserved: [])
49
+ # Nil list produces an empty FileList; nil revisions leaves the list unchanged
50
+ return FileList.new([]) if list.nil?
51
+
52
+ additive, subtractive = (revisions || []).partition { |revision| FilePathUtils.add_path?( revision ) }
53
+ kept = kept_files( list, additive, subtractive, PathExclusions.of( [], exclude, reserved ) )
54
+
55
+ # #104 -- every entry here is already a concrete, real file, but plain FileList.new or
56
+ # #include re-reads each as a glob, silently dropping a literal `[`/`]`.
57
+ # FileWrapper#instantiate_file_list_literal adds every entry as-is.
58
+ result = @file_wrapper.instantiate_file_list_literal( kept.map { |path| ProjectPaths.relative( path ) } )
59
+ result.resolve() # Force expansion to prevent race conditions in threaded context
60
+ return result
61
+ end
62
+
63
+ # The places files below `entries` mirror below, each spelled canonically: the base of a
64
+ # `/**` or `/*` entry, every directory a glob within an entry names, or the entry itself.
65
+ # A `-:` exclusion roots nothing. A `[`/`]` is part of a name (#104), never a glob.
66
+ def collect_mirror_roots(entries)
67
+ return [] if entries.nil?
68
+
69
+ roots = entries.select { |entry| FilePathUtils.add_path?( entry ) }.flat_map { |entry| entry_roots( entry ) }
70
+ return roots.map { |root| ProjectPaths.canonical( root ) }.uniq
71
+ end
72
+
73
+ # A path relative to the working directory, or absolute when it has no relative form
74
+ def shortest_path_from_working(path)
75
+ return ProjectPaths.relative( ProjectPaths.absolute( path ) )
76
+ end
77
+
78
+ private
79
+
80
+ # Every directory additive entries name, once each by its real spelling, with its distance
81
+ # below the nearest base of the entries naming it
82
+ def named_directories(entries)
83
+ named = {}
84
+ entries.each do |entry|
85
+ bases = PathSpellings.of( [FilePathUtils.no_decorators( entry )] )
86
+ entry_directories( entry ).each { |dir| name_directory( named, dir, bases ) }
87
+ end
88
+
89
+ return named.values
90
+ end
91
+
92
+ # Records `dir`, the first spelling of its real place kept, nearer if these bases are
93
+ def name_directory(named, dir, bases)
94
+ both = [dir, ProjectPaths.real_absolute( dir )]
95
+ found = (named[PathSpellings.key( both.last )] ||= Named.new( dir, both, Float::INFINITY ))
96
+ found.distance = [found.distance, PathSpellings.nearer( both, bases )].min
97
+ end
98
+
99
+ # The listed files and those additive revisions name, less those `-:` revisions name, each
100
+ # compared through its directory as it really is
101
+ def kept_files(list, additive, subtractive, excluded)
102
+ real = {}
103
+ minus = Set.new( files_of( subtractive ).map { |path| real_key( path, real ) } )
104
+ added = added_files( additive, excluded, real )
105
+ return (list.map { |path| ProjectPaths.absolute( path ) } + added).uniq { |path| real_key( path, real ) }.reject { |path| minus.include?( real_key( path, real ) ) }
106
+ end
107
+
108
+ # The files additive revisions name that `excluded` keeps, each judged by its distance
109
+ # below the base of the revision naming it
110
+ def added_files(additive, excluded, real)
111
+ return files_of( additive ) if excluded.none?
112
+
113
+ return additive.flat_map do |revision|
114
+ bases = PathSpellings.of( [FilePathUtils.no_decorators( revision )] )
115
+ revision_files( revision ).select do |file|
116
+ spellings = [file, real_spelling( file, real )]
117
+ excluded.keeps?( spellings, PathSpellings.nearer( spellings, bases ) )
118
+ end
119
+ end
120
+ end
121
+
122
+ def directories_of(entries)
123
+ return entries.flat_map { |entry| entry_directories( entry ) }
124
+ end
125
+
126
+ def files_of(revisions)
127
+ return revisions.flat_map { |revision| revision_files( revision ) }
128
+ end
129
+
130
+ # Every directory one entry names, absolute and spelled through its base as the base
131
+ # really is, so one place is never collected under two spellings of that base
132
+ def entry_directories(entry)
133
+ path = ProjectPaths.standardize( FilePathUtils.no_aggregation_decorators( entry ) )
134
+ dirs = listed_directories( path )
135
+ dirs = with_parents( dirs, path ) if path.end_with?( '/**', '/*' )
136
+
137
+ return PathSpellings.through_real( dirs.map { |dir| ProjectPaths.absolute( dir ) }, ProjectPaths.absolute( FilePathUtils.no_decorators( path ) ) )
138
+ end
139
+
140
+ # #104 -- FilePathUtils.subdirectory_glob escapes a literal `[`/`]` in the path itself, so
141
+ # it's matched literally rather than read as glob syntax. Sorted for Github Issue #860.
142
+ def listed_directories(path)
143
+ entries = @file_wrapper.directory_listing( FilePathUtils.subdirectory_glob( path ) ).sort
144
+ return entries.select { |entry| @file_wrapper.directory?( entry ) }
145
+ end
146
+
147
+ # Ceedling's recursive glob convention includes the directory above each match, unlike
148
+ # Ruby's, and the base directory itself when nothing below it matches
149
+ def with_parents(dirs, path)
150
+ parents = dirs.map { |dir| File.join( dir, '..' ) }
151
+ parents << FilePathUtils.no_decorators( path ) if dirs.empty?
152
+ return parents + dirs
153
+ end
154
+
155
+ def entry_roots(entry)
156
+ path = ProjectPaths.standardize( FilePathUtils.no_aggregation_decorators( entry ) ).sub( %r{(?:/\*\*?)+\z}, '' )
157
+ return [path] unless path.match?( /[*?{]/ )
158
+
159
+ listing = @file_wrapper.directory_listing( FilePathUtils.escape_glob_brackets( path ) )
160
+ return listing.sort.select { |listed| @file_wrapper.directory?( listed ) }
161
+ end
162
+
163
+ # Every file one :files revision names, absolute and spelled through its base as the base
164
+ # really is. #104 -- a literal `[`/`]` is escaped as in `listed_directories`.
165
+ def revision_files(revision)
166
+ path = ProjectPaths.standardize( FilePathUtils.no_aggregation_decorators( revision ) )
167
+ files = @file_wrapper.directory_listing( FilePathUtils.escape_glob_brackets( path ) ).sort.reject { |entry| @file_wrapper.directory?( entry ) }
168
+
169
+ return PathSpellings.through_real( files.map { |file| ProjectPaths.absolute( file ) }, ProjectPaths.absolute( FilePathUtils.no_decorators( path ) ) )
170
+ end
171
+
172
+ # A file compared through its directory as it really is, keeping its own name. Each
173
+ # directory is resolved once per call, in `real`.
174
+ def real_key(file, real)
175
+ return PathSpellings.key( real_spelling( file, real ) )
176
+ end
177
+
178
+ def real_spelling(file, real)
179
+ dir = File.dirname( file )
180
+ return File.join( (real[dir] ||= ProjectPaths.real_absolute( dir )), File.basename( file ) )
181
+ end
182
+
183
+ end
@@ -8,10 +8,16 @@
8
8
  require 'rubygems'
9
9
  require 'rake' # for ext()
10
10
  require 'fileutils'
11
+ require 'pathname'
11
12
  require 'ceedling/exceptions'
13
+ require 'set'
12
14
  require 'ceedling/system_wrapper'
13
15
  require 'ceedling/constants'
14
- require 'ceedling/path_mirror'
16
+ require 'ceedling/filesystem/path_mirror'
17
+ require 'ceedling/filesystem/path_matcher'
18
+ require 'ceedling/filesystem/project_paths'
19
+ require 'ceedling/filesystem/build_subpath'
20
+ require 'ceedling/filesystem/destination_collision'
15
21
 
16
22
  # global utility methods (for plugins, project files, etc.)
17
23
  def ceedling_form_filepath(destination_path, original_filepath, new_extension=nil)
@@ -24,6 +30,10 @@ class FilePathUtils
24
30
 
25
31
  constructor :configurator, :file_wrapper
26
32
 
33
+ def setup
34
+ @formed_lock = Mutex.new
35
+ @formed_paths = Set.new
36
+ end
27
37
 
28
38
  ######### Class methods ##########
29
39
 
@@ -32,9 +42,7 @@ class FilePathUtils
32
42
  def self.standardize_in_place(path)
33
43
  if path.is_a? String
34
44
  raise CeedlingException.new("Attempted to standardize path in frozen string ⏩️ #{path.inspect}") if path.frozen?
35
- path.strip!
36
- path.tr!("\\", '/')
37
- path.chomp!('/')
45
+ path.replace( ProjectPaths.standardize( path ) )
38
46
  end
39
47
  return path
40
48
  end
@@ -149,31 +157,24 @@ class FilePathUtils
149
157
  # ['a/b', 'a/c', 'a'] => ['a']
150
158
  # ['src', 'lib/core', 'lib/utils'] => ['src', 'lib/core', 'lib/utils'] (unchanged)
151
159
  #
152
- # Comparison uses forward-slash-normalized copies; returned paths preserve the original
153
- # form supplied by the caller.
160
+ # Paths compare as the directories they name (ProjectPaths.contains?), so `..` holds
161
+ # `../sib` but not `../../far/x`. Returned paths preserve the form the caller supplied.
154
162
  def self.collapse_to_common_parents(paths)
155
163
  return paths if paths.nil? || paths.length <= 1
156
164
 
157
- # Pair each original path with a normalized form used only for ancestry comparison.
158
- # Normalize to forward slashes and strip any trailing separator.
159
- pairs = paths.map { |p| [p, p.tr('\\', '/').chomp('/')] }
160
-
161
- # Sort shallowest-first so ancestors are always encountered before their descendants.
162
- # No tiebreaker for same-depth entries needed: Ruby's sort_by doesn't guarantee a
163
- # stable order among ties, but two same-depth paths can never be each other's
164
- # ancestor, so the ancestor-exclusion check below is correct regardless of which
165
- # order same-depth ties come out in.
166
- pairs.sort_by! { |_, normalized| normalized.count('/') }
167
-
168
- kept = []
169
- pairs.each do |original, normalized|
170
- # Skip this path if any already-kept path is a proper ancestor of it.
171
- next if kept.any? { |_, k| normalized.start_with?(k + '/') }
172
- kept << [original, normalized]
173
- end
165
+ # Shortest absolute spelling first, so an ancestor is always met before what it holds
166
+ pairs = paths.map { |path| [path, ProjectPaths.absolute( path )] }.sort_by { |_, absolute| absolute.length }
167
+
168
+ return outermost( pairs ).map { |original, _| original }
169
+ end
174
170
 
175
- kept.map { |original, _| original }
171
+ # The [original, absolute] pairs no earlier pair's directory holds
172
+ def self.outermost(pairs)
173
+ return pairs.each_with_object( [] ) do |pair, kept|
174
+ kept << pair unless kept.any? { |_, held| ProjectPaths.contains?( held, pair.last ) }
175
+ end
176
176
  end
177
+ private_class_method :outermost
177
178
 
178
179
 
179
180
  ######### Instance methods ##########
@@ -192,12 +193,7 @@ class FilePathUtils
192
193
  end
193
194
 
194
195
  def form_release_build_objects_filelist(files)
195
- return mirror_build_objects(
196
- files,
197
- root: @configurator.project_release_build_output_path,
198
- ext: @configurator.extension_object.primary,
199
- roots: @configurator.paths_source
200
- )
196
+ return mirror_build_objects( files, root: @configurator.project_release_build_output_path, ext: @configurator.extension_object.primary )
201
197
  end
202
198
 
203
199
  # `filepath` is an already-mirrored object path -- its own directory is exactly where its
@@ -207,32 +203,34 @@ class FilePathUtils
207
203
  end
208
204
 
209
205
  def form_release_dependencies_filelist(files)
210
- return mirror_build_objects(
211
- files,
212
- root: @configurator.project_release_dependencies_path,
213
- ext: @configurator.extension_dependencies.primary,
214
- roots: @configurator.paths_source
215
- )
206
+ return mirror_build_objects( files, root: @configurator.project_release_dependencies_path, ext: @configurator.extension_dependencies.primary )
216
207
  end
217
208
 
218
209
  ### Tests ###
219
210
 
211
+ # A test is named by its path from the project root, without its extension. The name places
212
+ # the test's build, results, runner, mock, preprocessing and log directories, and a path is
213
+ # unique, so no two tests share them. A test outside the project keeps its whole path too,
214
+ # with each `..` written as `_up_`, and a path with no relative form (another Windows drive)
215
+ # placed under `_abs_`, so no name reaches outside the build directory.
216
+ def form_test_name(filepath)
217
+ return BuildSubpath.encode( filepath.to_s.ext( '' ) )
218
+ end
219
+
220
220
  def form_test_build_path(name, context: nil)
221
221
  form_build_context_path(BUILD_OUT_DIR, name: name, context: context)
222
222
  end
223
223
 
224
+ # Where a build places the object of `filepath`: below its test's build path, or below the
225
+ # context's own output root for a source compiled outside any test
224
226
  def form_test_object_filepath(filepath, name: nil, context: nil)
225
- File.join(
226
- form_build_context_path(BUILD_OUT_DIR, name: name, context: context),
227
- File.basename(filepath).ext(@configurator.extension_object.primary)
228
- )
227
+ root = form_build_context_path( BUILD_OUT_DIR, name: name, context: context )
228
+ return build_filepath( filepath, root: root, ext: @configurator.extension_object.primary, generated_roots: generated_roots( name ) )
229
229
  end
230
230
 
231
- # As `form_test_runners_path`: a flat test's own identity forms the shared flat results
232
- # directory, untouched -- only a genuinely nested test's own mirrored subdirectory
233
- # becomes an extra directory level here, so two same-named tests land in distinct
234
- # results directories without perturbing the overwhelmingly common flat, non-duplicate
235
- # case.
231
+ # As `form_test_runners_path`: a test's results sit beneath the directories of its name,
232
+ # so same-named tests in different directories keep apart. Only a test at the project
233
+ # root uses the results directory itself.
236
234
  def form_test_results_path(name = nil, context: nil)
237
235
  subdir = name.nil? ? '' : mirrored_test_subdir(name)
238
236
  return form_build_context_path(BUILD_RESULTS_DIR, context: context) if subdir.empty?
@@ -253,51 +251,46 @@ class FilePathUtils
253
251
  form_build_context_path(BUILD_DEPENDENCIES_DIR, name: name, context: context)
254
252
  end
255
253
 
256
- # `filepath` is an already-mirrored object path -- recovering the mirrored subdirectory
257
- # beyond the test's own build root and re-rooting it under this test's dependencies path
258
- # keeps this dependencies file alongside the same-named object it accompanies, rather
259
- # than colliding with another object's dependencies file at a shared flat basename.
260
- # Without a test identity to recover that build root from, there's no mirroring to do --
261
- # the dependencies file stays exactly where its context-level directory alone places it.
254
+ # `filepath` is an object path. Its dependencies file sits at the object's own place below
255
+ # the dependencies root, so it never collides with another same-named object's. Without a
256
+ # test, that place is below the context's own output root.
262
257
  def form_test_dependencies_filepath(filepath, name: nil, context: nil)
263
258
  root = form_build_context_path(BUILD_DEPENDENCIES_DIR, name: name, context: context)
264
259
  basename = File.basename(filepath).ext(@configurator.extension_dependencies.primary)
265
260
 
266
- return File.join(root, basename) if name.nil?
267
-
268
- subdir = PathMirror.relative_subdir( filepath, [form_test_build_path(name, context: context)] )
261
+ subdir = ProjectPaths.subdir_below( form_test_build_path( name, context: context ), File.dirname( filepath ) ) || ''
269
262
  return subdir.empty? ? File.join(root, basename) : File.join(root, subdir, basename)
270
263
  end
271
264
 
272
- def form_test_mocks_path(name, context: nil)
265
+ def form_test_mocks_path(name)
273
266
  form_named_path(@configurator.cmock_mock_path, name)
274
267
  end
275
268
 
276
- def form_test_partials_path(name, context: nil)
269
+ def form_test_partials_path(name)
277
270
  form_named_path(@configurator.project_test_partials_path, name)
278
271
  end
279
272
 
280
- def form_test_preprocess_includes_path(name, context: nil)
273
+ def form_test_preprocess_includes_path(name)
281
274
  form_named_path(@configurator.project_test_preprocess_includes_path, name)
282
275
  end
283
276
 
284
- def form_test_preprocess_files_path(name, context: nil)
277
+ def form_test_preprocess_files_path(name)
285
278
  form_named_path(@configurator.project_test_preprocess_files_path, name)
286
279
  end
287
280
 
288
- def form_test_preprocess_files_full_expansion_path(name, context: nil)
281
+ def form_test_preprocess_files_full_expansion_path(name)
289
282
  form_named_path(@configurator.project_test_preprocess_files_path, name, subdir: PREPROCESS_FULL_EXPANSION_DIR)
290
283
  end
291
284
 
292
- def form_test_preprocess_files_directives_only_path(name, context: nil)
285
+ def form_test_preprocess_files_directives_only_path(name)
293
286
  form_named_path(@configurator.project_test_preprocess_files_path, name, subdir: PREPROCESS_DIRECTIVES_ONLY_DIR)
294
287
  end
295
288
 
296
- def form_test_preprocess_files_raw_directives_only_path(name, context: nil)
289
+ def form_test_preprocess_files_raw_directives_only_path(name)
297
290
  form_named_path(@configurator.project_test_preprocess_files_path, name, subdir: PREPROCESS_RAW_DIRECTIVES_ONLY_DIR)
298
291
  end
299
292
 
300
- def form_test_preprocess_build_directives_path(name, context: nil)
293
+ def form_test_preprocess_build_directives_path(name)
301
294
  form_named_path(@configurator.project_test_preprocess_build_directives_path, name)
302
295
  end
303
296
 
@@ -323,12 +316,10 @@ class FilePathUtils
323
316
  return File.join( build_output_path, File.basename(filepath).ext(@configurator.extension_testfail.primary) )
324
317
  end
325
318
 
326
- # A flat test's own identity (no mirrored subdirectory of its own) forms the shared flat
327
- # runners directory, untouched -- only a genuinely nested test's own mirrored
328
- # subdirectory (excluding its own basename) becomes an extra directory level here, so two
329
- # same-named tests in different directories land in distinct runner directories without
330
- # perturbing the overwhelmingly common flat, non-duplicate case.
331
- def form_test_runners_path(name, context: nil)
319
+ # A test's runner sits beneath the directories of its name, so same-named tests in
320
+ # different directories keep apart. Only a test at the project root uses the runners
321
+ # directory itself.
322
+ def form_test_runners_path(name)
332
323
  subdir = mirrored_test_subdir(name)
333
324
  return @configurator.project_test_runners_path if subdir.empty?
334
325
  return File.join(@configurator.project_test_runners_path, subdir)
@@ -373,13 +364,7 @@ class FilePathUtils
373
364
  # the same filepath, so all of them agree on where the artifact lands without
374
365
  # coordinating.
375
366
  def preprocessed_file_subpath(filepath, test = nil)
376
- roots = @configurator.paths_source +
377
- @configurator.paths_support +
378
- @configurator.paths_include +
379
- @configurator.paths_test +
380
- generated_roots( test )
381
-
382
- subdir = PathMirror.relative_subdir( filepath, roots )
367
+ subdir = form_build_subpath( filepath, generated_roots: generated_roots( test ) )
383
368
  basename = File.basename( filepath )
384
369
 
385
370
  return basename if subdir.empty?
@@ -450,12 +435,7 @@ class FilePathUtils
450
435
  # standard roots mirrors it. Without its root here, two modules sharing a basename
451
436
  # collapse onto one object filename.
452
437
  def form_test_build_objects_filelist(path, sources, extra_roots: [])
453
- return mirror_build_objects(
454
- sources,
455
- root: path,
456
- ext: @configurator.extension_object.primary,
457
- roots: @configurator.paths_source + @configurator.paths_support + extra_roots
458
- )
438
+ return mirror_build_objects( sources, root: path, ext: @configurator.extension_object.primary, generated_roots: extra_roots )
459
439
  end
460
440
 
461
441
  # Where one mock's generated files sit: its test's mock root, plus the subdirectory
@@ -546,13 +526,13 @@ class FilePathUtils
546
526
  return form_partial_filename( _module, PARTIAL_SUFFIX_IMPL, EXTENSION_CORE_SOURCE )
547
527
  end
548
528
 
529
+ # Each test's results file sits where `form_test_results_path` puts it, beneath its name
549
530
  def form_pass_results_filelist(path, files)
550
- return mirror_build_objects(
551
- files,
552
- root: path,
553
- ext: @configurator.extension_testpass.primary,
554
- roots: @configurator.paths_test
555
- )
531
+ results = @file_wrapper.instantiate_file_list_literal( files ).map do |file|
532
+ subdir = mirrored_test_subdir( form_test_name( file ) )
533
+ form_pass_results_filepath( subdir.empty? ? path : File.join( path, subdir ), file )
534
+ end
535
+ return @file_wrapper.instantiate_file_list_literal( results )
556
536
  end
557
537
 
558
538
  ### Private ###
@@ -577,9 +557,8 @@ class FilePathUtils
577
557
  return path
578
558
  end
579
559
 
580
- # The mirrored-subdirectory portion of a test's own identity, excluding its own basename
581
- # -- '' for a flat test (e.g. 'TestFoo', no subdirectory at all), or the leading segments
582
- # for a nested one (e.g. 'unit' for 'unit/test_foo').
560
+ # The directories of a test's name, excluding its basename: 'test/unit' for
561
+ # 'test/unit/test_foo', or '' for a test at the project root.
583
562
  def mirrored_test_subdir(name)
584
563
  dir = File.dirname(name)
585
564
  return '' if dir == '.'
@@ -602,15 +581,47 @@ class FilePathUtils
602
581
  # it contains a literal `[`/`]`. FileWrapper#instantiate_file_list_literal (`<<`, not
603
582
  # `.new`/`.include`) adds every entry as-is, with no glob interpretation at all -- the
604
583
  # correct construction for a list that's already the real, final answer.
605
- def mirror_build_objects(files, root:, ext:, roots:)
606
- clean_roots = PathMirror.clean_roots( roots )
584
+ def mirror_build_objects(files, root:, ext:, generated_roots: [])
585
+ sources = @file_wrapper.instantiate_file_list_literal( files ).to_a.uniq { |file| ProjectPaths.absolute( file ) }
586
+ objects = sources.map { |file| build_filepath( file, root: root, ext: ext, generated_roots: generated_roots ) }
607
587
 
608
- objects = @file_wrapper.instantiate_file_list_literal(files).map do |file|
609
- subdir = PathMirror.relative_subdir_from_clean_roots( file, clean_roots )
610
- basename = File.basename(file).ext(ext)
611
- subdir.empty? ? File.join(root, basename) : File.join(root, subdir, basename)
612
- end
588
+ validate_distinct_build_files!( sources, objects )
613
589
  return @file_wrapper.instantiate_file_list_literal( objects )
614
590
  end
615
591
 
592
+ def build_filepath(file, root:, ext:, generated_roots:)
593
+ subdir = form_build_subpath( file, generated_roots: generated_roots )
594
+ basename = File.basename( file ).ext( ext )
595
+ path = subdir.empty? ? File.join( root, basename ) : File.join( root, subdir, basename )
596
+
597
+ # A source far from the project gains a directory for each level its path climbs. Each
598
+ # path is checked once, however often it is formed.
599
+ @file_wrapper.check_path_length( path, origin: 'FilePathUtils#build_filepath' ) if first_formed?( path )
600
+ return path
601
+ end
602
+
603
+ def first_formed?(path)
604
+ return @formed_lock.synchronize { @formed_paths.add?( path ) }
605
+ end
606
+
607
+ # Where below a build output root the build file of `file` lands. A file below one of the
608
+ # caller's generated roots (a test's mocks or Partials) mirrors below that root; one
609
+ # below the build root otherwise (vendored frameworks, runners) lands flat; and any other
610
+ # lands by its own path from the project root, so two sources never share one.
611
+ def form_build_subpath(file, generated_roots: [])
612
+ generated = PathMirror.relative_subdir_if_rooted_in_clean_roots( file, generated_roots )
613
+ return generated unless generated.nil?
614
+ return '' if ProjectPaths.contains?( @configurator.project_build_root, file )
615
+
616
+ return BuildSubpath.of_directory( File.dirname( file ) )
617
+ end
618
+
619
+ # Two sources building to one file would leave one of them out of the build
620
+ def validate_distinct_build_files!(sources, built)
621
+ built_from = sources.zip( built ).to_h
622
+ DestinationCollision.find( sources ) { |source| built_from[source] }.each do |file, sharing|
623
+ DestinationCollision.raise_for( file, sharing, 'would build to the same file' )
624
+ end
625
+ end
626
+
616
627
  end
@@ -5,12 +5,13 @@
5
5
  # SPDX-License-Identifier: MIT
6
6
  # =========================================================================
7
7
 
8
+ require 'ceedling/filesystem/working_dir'
9
+
8
10
  class FileSystemWrapper
9
11
 
10
- def cd(path)
11
- FileUtils.cd path do
12
- yield
13
- end
12
+ # Through WorkingDir, so paths resolved inside the block resolve against `path`
13
+ def cd(path, &block)
14
+ return WorkingDir.chdir( path, &block )
14
15
  end
15
16
 
16
- end
17
+ end