Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#ifndef FOO_H
#define FOO_H

int foo_value(void);

#endif // FOO_H
6 changes: 6 additions & 0 deletions assets/fixtures/header_include_path_validation/dup.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#ifndef DUP_H
#define DUP_H

#define DUP_VALUE 222

#endif // DUP_H
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#ifndef DUP_H
#define DUP_H

#define DUP_VALUE 111

#endif // DUP_H
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#include "unity.h"
#include "mock_foo.h"

// foo.h exists identically in two configured :include directories
// (src/alt_drivers/foo.h and src/drivers/foo.h). This bare #include must
// resolve to exactly one of them, by search-path order, rather than erroring.

void setUp(void) {}
void tearDown(void) {}

void test_foo_value_ambiguous_mock(void)
{
foo_value_ExpectAndReturn(111);
TEST_ASSERT_EQUAL(111, foo_value());
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#include "unity.h"

// dup.h exists both via TEST_INCLUDE_PATH("other_inc") and via :paths -> :include
// (src/inc_dup/dup.h). TEST_INCLUDE_PATH() ranks ahead of :include by default, but
// naming enough trailing path here selects the :include copy specifically.
TEST_INCLUDE_PATH("other_inc")

#include "inc_dup/dup.h"

void setUp(void) {}
void tearDown(void) {}

void test_dup_value_resolves_via_include_path(void)
{
TEST_ASSERT_EQUAL(222, DUP_VALUE);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#include "unity.h"
#include "drivers/mock_foo.h"

// Disambiguates by path what test_ambiguous_mock.c leaves bare -- selects
// src/drivers/foo.h specifically, not src/alt_drivers/foo.h (the bare default).

void setUp(void) {}
void tearDown(void) {}

void test_foo_value_pathed_mock_ambiguous(void)
{
foo_value_ExpectAndReturn(111);
TEST_ASSERT_EQUAL(111, foo_value());
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#include "unity.h"

// dup.h exists both via TEST_INCLUDE_PATH("other_inc") and via :paths -> :include
// (src/dup.h, reached through src/**). TEST_INCLUDE_PATH() ranks ahead of :include
// in this test's own search paths, so the bare #include below must resolve to
// other_inc/dup.h, matching the real compiler's own -I order.
TEST_INCLUDE_PATH("other_inc")

#include "dup.h"

void setUp(void) {}
void tearDown(void) {}

void test_dup_value_resolves_via_test_include_path(void)
{
TEST_ASSERT_EQUAL(111, DUP_VALUE);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#include "dup.h"

int dup_value(void)
{
return 111;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#ifndef DUP_H
#define DUP_H

int dup_value(void);

#endif // DUP_H
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#include "dup.h"

int dup_value(void)
{
return 222;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#ifndef DUP_H
#define DUP_H

int dup_value(void);

#endif // DUP_H
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#include "unity.h"
#include "alpha/dup.h"

void setUp(void) {}
void tearDown(void) {}

void test_dup_value_alpha(void)
{
TEST_ASSERT_EQUAL(111, dup_value());
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#include "unity.h"
#include "dup.h"

void setUp(void) {}
void tearDown(void) {}

void test_dup_value_bare(void)
{
TEST_ASSERT_EQUAL(111, dup_value());
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#include "unity.h"
#include "beta/dup.h"

void setUp(void) {}
void tearDown(void) {}

void test_dup_value_beta(void)
{
TEST_ASSERT_EQUAL(222, dup_value());
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#include "unity.h"
#include "dup.h"

// A bare #include "dup.h" would, on its own, implicitly resolve to the first
// candidate by search-path order (alpha/dup.c, returning 111). This
// TEST_SOURCE_FILE() entry names beta/dup.c specifically -- the same basename,
// stem-matched against the header's own implied source -- so it should
// override that implicit resolution rather than merely adding a second,
// separately-compiled dup.c alongside it (which would fail to link: both
// alpha/dup.c and beta/dup.c define dup_value()).
TEST_SOURCE_FILE("beta/dup.c")

void setUp(void) {}
void tearDown(void) {}

void test_dup_value_overridden_by_test_source_file(void)
{
TEST_ASSERT_EQUAL(222, dup_value());
}
2 changes: 1 addition & 1 deletion docs/Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ A delta build is simply a build run that only performs regeneration, compilation
### `#include` relative paths & duplicate filename disambiguation
Added support for properly distinguishing all C files by filepath (addressing [#1167](https://github.com/ThrowTheSwitch/Ceedling/issues) specifically but also the fundamental problem generally).

In Ceedling’s early history simplicity won out with the assumption that every C file would be uniquely named. But, for example, this meant _dir1/foo.h_ and _dir2/foo.h_ were indistiguishable. Now Ceedling fully utilizes filepaths to distinguish all elements of a build. Relative paths are supported in `#include` directives. `test:` build tasks at the command line can optionally include a filepath to distinguish test files of the same name.
In Ceedling’s early history simplicity won out with the assumption that every C file would be uniquely named. But, for example, this meant _dir1/foo.h_ and _dir2/foo.h_ were indistiguishable and Ceedling “guessed” to disambiguate using the ordering of filepath collections. This worked, but there was no ability to use partial paths to explicitly select a specific file. Now Ceedling fully utilizes filepaths to distinguish all elements of a test build. Relative paths are supported in `#include` directives. `test:` build tasks at the command line can optionally include a filepath to distinguish test files of the same name. The previous convention still works where the ordering of paths is identical among compiler search paths and those used to find files for test builds, but now additional path information can be provided to target specific files in your source collection.

### Multiple file extensions per file type
Added support for multiple [file extensions](https://throwtheswitch.github.io/Ceedling/latest/configuration/reference/extension/) per type (e.g. `:extension` ↳ `:source` ⇒ `['.c', '.C']`) such as requested in [#947](https://github.com/ThrowTheSwitch/Ceedling/issues/947).
Expand Down
47 changes: 39 additions & 8 deletions docs/mkdocs/testing-guide/conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,27 @@ tree.

## Distinguishing same-named files

### Header / source files

Ceedling distinguishes files by their full relative path, not just their
filename. A project may have, for example, both `src/drivers/uart.c` and
`src/sensors/uart.c` without one hiding or colliding with the other.

Distinguishing files is mostly an issue in test builds. If two files share
a name and however you refer to them throughout your project does not
include enough path to distinguish them, Ceedling raises an error naming
every matching file rather than guessing which one you meant.
Distinguishing files is mostly an issue in test builds. If two files share
a name without enough path to distinguish them, Ceedling resolves the reference
to the first match among the file collection. File collections are ordered per
your project configuration (`:paths`). The ordering of collected files is
identical to the search path ordering provided to the compiler from the same
`:paths` configuration plus any use of
[`TEST_INCLUDE_PATH()`](build-directives.md#test_include_path) directives in
a test file. When Ceedling finds more than one reference, it uses the first
in the ordered file list but logs an informational notice naming multi-file
matches, so the choice is not silent.

If Ceedling uses the wrong match for lack of path information, your test build
fails downstream with an ordinary compilation error indirectly revealing the
issue. Add enough path at the point of reference to select the file you actually
meant.

* Use paths in `#include` directives in your test files to distinguish header
files of the same name (mocks are distiguished by the same filepath as the
Expand All @@ -35,10 +48,28 @@ every matching file rather than guessing which one you meant.
* Use the [`TEST_SOURCE_FILE()` build directive macro][build-directive-macros]
to provide a path to distinguish source files of the same name to be
compiled and linked with a test executable.
* Execute [`ceedling test:` tasks][ceedling-test] at the command line with an
optional partial path to distinguish test executables (e.g. `test:foo/bar.c`),
recalling that multiple conventions exist for finding/executing a test
executable via `test:` task.

`TEST_SOURCE_FILE()` also takes precedence over the header/source correspondence
convention above. If a test both `#include`s a header and separately names a
same-named source via `TEST_SOURCE_FILE()`, the directive's own path wins outright
— its source is the one compiled, and the header/source convention does not also
compile whatever it would otherwise have found on its own for that same name. This
holds even when the header's own `#include` was already unambiguous; a
`TEST_SOURCE_FILE()` entry sharing that basename is always authoritative. (The
header itself is still validated/resolved as usual — only which *source* file gets
compiled for a shared basename is affected.)

### Test files (CLI test tasks)

Execute [`ceedling test:` tasks][ceedling-test] at the command line with an
optional partial path to distinguish test executables (e.g. `test:foo/bar.c`),
recalling that multiple conventions exist for finding/executing a test
executable via `test:` task.

Unlike the cases above, a same-named test file given at the command line is
**not** auto-resolved. You must supply enough path yourself in the task
name. Ceedling will complain about colliding task names and provide a list
of matching candidates to help you provide path to disambiguate test files.

[build-directive-macros]: build-directives.md
[ceedling-test]: ../getting-started/command-line.md
Expand Down
93 changes: 67 additions & 26 deletions lib/ceedling/file_finder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,12 @@ class FileFinder
constructor :configurator, :file_finder_helper, :file_path_utils, :file_wrapper, :yaml_wrapper


def find_header_input_for_mock(mock)
# `collection`, when given, is a caller's own already-ordered, per-test header list
# (see IncludePathinator#ordered_header_files) reflecting that one test's real search-path
# priority -- TEST_INCLUDE_PATH() ranked ahead of :support/:include, as a real compile would
# see it. Absent that (a caller with no single test in view, e.g. Partializer resolving a
# module generically), the project-wide collection_all_headers is the only sensible fallback.
def find_header_input_for_mock(mock, collection: nil)
# Mock name/path => <mock prefix><header filename (.h)>, optionally preceded by however
# much path the #include itself carried (e.g. 'Mockfoo.h' or 'drivers/Mockfoo.h').
# Note: In some rare cases, a mock name may include a dot (ex. Sensor.44) because of versioning file naming convention
Expand All @@ -30,9 +35,9 @@ def find_header_input_for_mock(mock)
basename = File.basename(mock).delete_prefix(@configurator.cmock_mock_prefix)
header = dir == '.' ? basename : File.join(dir, basename)

found_path = @file_finder_helper.find_file_in_collection(
found_path = @file_finder_helper.resolve_file_in_collection(
header,
@configurator.collection_all_headers,
collection || @configurator.collection_all_headers,
:error
)

Expand All @@ -45,16 +50,22 @@ def find_header_input_for_mock(mock)
# also needs (or can reuse) the header path it was derived from, and every caller placing
# a mock's own files -- its search path, an early stand-in for it, or its real generated
# content -- must agree on exactly the same subdirectory for that mock to ever compile.
def resolve_mock(mock)
source = find_header_input_for_mock(mock)
def resolve_mock(mock, collection: nil)
source = find_header_input_for_mock(mock, collection: collection)
subdir = PathMirror.relative_subdir(source, @configurator.paths_test + @configurator.paths_support + @configurator.paths_include)
return [source, subdir]
end


# Find test filepath from only the base name of a test file (e.g. 'test_foo')
#
# Deliberately strict, unlike every other lookup this class performs: a human typing
# a `ceedling test:<name>` command needs to be told outright when their own name is
# ambiguous, not have Ceedling silently guess which test they meant -- there's no
# compilation step downstream to catch a wrong guess the way there is for a header
# or source file resolved during a build.
def find_test_file_from_name(name)
return find_first_candidate(name, @configurator.extension_source, @configurator.collection_all_tests, :error)
return find_first_candidate(name, @configurator.extension_source, @configurator.collection_all_tests, :error, strict: true)
end


Expand Down Expand Up @@ -193,8 +204,10 @@ def find_build_input_file(filepath:, complain: :error, context:, test: nil)
end


def find_header_file(filepath, complain = :error)
return find_first_candidate(filepath, @configurator.extension_header, @configurator.collection_all_headers, complain)
# `collection`, when given, overrides collection_all_headers with a caller's own
# already-ordered, per-test header list -- see find_header_input_for_mock's comment.
def find_header_file(filepath, complain = :error, collection: nil)
return find_first_candidate(filepath, @configurator.extension_header, collection || @configurator.collection_all_headers, complain)
end

def find_source_file(filepath, complain = :error)
Expand Down Expand Up @@ -284,41 +297,69 @@ def mirrored_query(filepath, release:, test:, context:)
# alone doesn't say which candidate filename actually exists. Every candidate but the
# last is searched for quietly by plain exact match -- a miss there just means trying
# the next spelling, not a real problem, and critically, must not go through
# find_file_in_collection at all: that helper's own case-insensitive "did you mean"
# fallback would otherwise fire on an early, expected miss (trying `.s` before `.S`, say)
# the moment ANY differently-cased candidate happens to exist on disk, well before every
# real candidate has had its turn. Only the true last candidate is searched under the
# caller's own complain-on-miss behavior, so a genuine failure still reports one sensible name.
def find_first_candidate(query, extension, collection, complain)
# find_file_in_collection/resolve_file_in_collection at all: that helper's own
# case-insensitive "did you mean" fallback would otherwise fire on an early, expected
# miss (trying `.s` before `.S`, say) the moment ANY differently-cased candidate
# happens to exist on disk, well before every real candidate has had its turn. Only
# the true last candidate is searched under the caller's own complain-on-miss
# behavior, so a genuine failure still reports one sensible name -- and, in non-strict
# mode, is the one place an ambiguity-resolution NOTICE gets logged (an ambiguity
# among the quietly-tried earlier spellings resolves silently; extension collections
# with only one configured spelling, the common case, always resolve via this last,
# logged attempt anyway).
#
# `strict:` selects which of the two ambiguity policies every attempt uses: raise
# (today's only behavior, still used by CLI test-name resolution) or resolve to the
# first match by collection order (everything else).
def find_first_candidate(query, extension, collection, complain, strict: false)
candidates = extension.candidates(query)

candidates[0...-1].each do |candidate|
found = match_candidate(candidate, collection)
found = match_candidate(candidate, collection, strict: strict)
return found unless found.nil?
end

return @file_finder_helper.find_file_in_collection(candidates.last, collection, complain)
if strict
return @file_finder_helper.find_file_in_collection(candidates.last, collection, complain)
else
return @file_finder_helper.resolve_file_in_collection(candidates.last, collection, complain)
end
end

# As `find_first_candidate`, but builds each candidate by plain string concatenation
# rather than `String#ext` -- some legacy filenames carry a dotted version segment
# (e.g. `foo.44`), and `.ext` would clobber that segment while re-adding the extension.
# Every candidate is searched for quietly; the caller decides how to react if none exist.
def try_extensions(basename, extension, collection)
extension.each do |ext|
found = match_candidate(basename + ext, collection)
# Mirrors find_first_candidate's quiet-until-the-last-spelling shape (see its own
# comment) -- the caller decides how to react if no spelling is ever found.
def try_extensions(basename, extension, collection, strict: false)
exts = extension.to_a

exts[0...-1].each do |ext|
found = match_candidate(basename + ext, collection, strict: strict)
return found unless found.nil?
end

return nil
last = basename + exts.last

if strict
return @file_finder_helper.find_file_in_collection(last, collection, :ignore)
else
return @file_finder_helper.resolve_file_in_collection(last, collection, :ignore)
end
end

# A single candidate query, matched against `collection` via the shared path-aware
# matcher, with no fuzzy fallback of any kind -- callers trying several candidate
# spellings in turn need each individual attempt to simply say yes or no (or raise on
# genuine ambiguity), not fall back to guessing partway through.
def match_candidate(candidate, collection)
return PathMatcher.match(candidate, collection)
# matcher. Strict mode raises on genuine ambiguity, exactly as today; non-strict mode
# silently takes the first match by collection order with no log of its own -- this
# quiet probe is for trying several candidate spellings in turn, not the caller's
# real resolution attempt (see find_first_candidate/try_extensions).
def match_candidate(candidate, collection, strict: false)
if strict
return PathMatcher.match(candidate, collection)
else
winner, _others = PathMatcher.resolve(candidate, collection)
return winner
end
end

end
Loading
Loading