Unit test some worker protocol serializers

Continue with the characterization testing idioms begun in
c70484454f, but this time for unit tests.

Co-authored-by: Andreas Rammhold <andreas@rammhold.de>
This commit is contained in:
John Ericson 2023-09-04 18:15:32 -04:00
parent f878b422b0
commit 7ff43435f9
11 changed files with 228 additions and 8 deletions

View file

@ -22,6 +22,7 @@ makefiles = \
-include Makefile.config -include Makefile.config
ifeq ($(tests), yes) ifeq ($(tests), yes)
UNIT_TEST_ENV = _NIX_TEST_UNIT_DATA=unit-test-data
makefiles += \ makefiles += \
src/libutil/tests/local.mk \ src/libutil/tests/local.mk \
src/libstore/tests/local.mk \ src/libstore/tests/local.mk \

View file

@ -2,14 +2,70 @@
## Unit-tests ## Unit-tests
The unit-tests for each Nix library (`libexpr`, `libstore`, etc..) are defined The unit tests are defined using the [googletest] and [rapidcheck] frameworks.
under `src/{library_name}/tests` using the
[googletest](https://google.github.io/googletest/) and [googletest]: https://google.github.io/googletest/
[rapidcheck](https://github.com/emil-e/rapidcheck) frameworks. [rapidcheck]: https://github.com/emil-e/rapidcheck
### Source and header layout
> An example of some files, demonstrating much of what is described below
>
> ```
> src
> ├── libexpr
> │ ├── value/context.hh
> │ ├── value/context.cc
> │ │
> │ …
> └── tests
> │ ├── value/context.hh
> │ ├── value/context.cc
> │ │
> │ …
> │
> ├── unit-test-data
> │ ├── libstore
> │ │ ├── worker-protocol/content-address.bin
> │ │ …
> │ …
> …
> ```
The unit tests for each Nix library (`libnixexpr`, `libnixstore`, etc..) live inside a directory `src/${library_shortname}/tests` within the directory for the library (`src/${library_shortname}`).
The data is in `unit-test-data`, with one subdir per library, with the same name as where the code goes.
For example, `libnixstore` code is in `src/libstore`, and its test data is in `unit-test-data/libstore`.
The path to the `unit-test-data` directory is passed to the unit test executable with the environment variable `_NIX_TEST_UNIT_DATA`.
> **Note**
> Due to the way googletest works, downstream unit test executables will actually include and re-run upstream library tests.
> Therefore it is important that the same value for `_NIX_TEST_UNIT_DATA` be used with the tests for each library.
> That is why we have the test data nested within a single `unit-test-data` directory.
### Running tests
You can run the whole testsuite with `make check`, or the tests for a specific component with `make libfoo-tests_RUN`. You can run the whole testsuite with `make check`, or the tests for a specific component with `make libfoo-tests_RUN`.
Finer-grained filtering is also possible using the [--gtest_filter](https://google.github.io/googletest/advanced.html#running-a-subset-of-the-tests) command-line option, or the `GTEST_FILTER` environment variable. Finer-grained filtering is also possible using the [--gtest_filter](https://google.github.io/googletest/advanced.html#running-a-subset-of-the-tests) command-line option, or the `GTEST_FILTER` environment variable.
### Characterization testing
See [below](#characterization-testing-1) for a broader discussion of characterization testing.
Like with the functional characterization, `_NIX_TEST_ACCEPT=1` is also used.
For example:
```shell-session
$ _NIX_TEST_ACCEPT=1 make libstore-tests-exe_RUN
...
[ SKIPPED ] WorkerProtoTest.string_read
[ SKIPPED ] WorkerProtoTest.string_write
[ SKIPPED ] WorkerProtoTest.storePath_read
[ SKIPPED ] WorkerProtoTest.storePath_write
...
```
will regenerate the "golden master" expected result for the `libnixstore` characterization tests.
The characterization tests will mark themselves "skipped" since they regenerated the expected result instead of actually testing anything.
## Functional tests ## Functional tests
The functional tests reside under the `tests` directory and are listed in `tests/local.mk`. The functional tests reside under the `tests` directory and are listed in `tests/local.mk`.
@ -124,9 +180,12 @@ This technique is to include the exact output/behavior of a former version of Ni
For example, this technique is used for the language tests, to check both the printed final value if evaluation was successful, and any errors and warnings encountered. For example, this technique is used for the language tests, to check both the printed final value if evaluation was successful, and any errors and warnings encountered.
It is frequently useful to regenerate the expected output. It is frequently useful to regenerate the expected output.
To do that, rerun the failed test with `_NIX_TEST_ACCEPT=1`. To do that, rerun the failed test(s) with `_NIX_TEST_ACCEPT=1`.
(At least, this is the convention we've used for `tests/lang.sh`. For example:
If we add more characterization testing we should always strive to be consistent.) ```bash
_NIX_TEST_ACCEPT=1 make tests/lang.sh.test
```
This convention is shared with the [characterization unit tests](#characterization-testing-1) too.
An interesting situation to document is the case when these tests are "overfitted". An interesting situation to document is the case when these tests are "overfitted".
The language tests are, again, an example of this. The language tests are, again, an example of this.

View file

@ -75,6 +75,7 @@
./precompiled-headers.h ./precompiled-headers.h
./src ./src
./tests ./tests
./unit-test-data
./COPYING ./COPYING
./scripts/local.mk ./scripts/local.mk
(fileset.fileFilter (f: lib.strings.hasPrefix "nix-profile" f.name) ./scripts) (fileset.fileFilter (f: lib.strings.hasPrefix "nix-profile" f.name) ./scripts)

View file

@ -87,6 +87,6 @@ define build-program
# Phony target to run this program (typically as a dependency of 'check'). # Phony target to run this program (typically as a dependency of 'check').
.PHONY: $(1)_RUN .PHONY: $(1)_RUN
$(1)_RUN: $$($(1)_PATH) $(1)_RUN: $$($(1)_PATH)
$(trace-test) $$($(1)_PATH) $(trace-test) $$(UNIT_TEST_ENV) $$($(1)_PATH)
endef endef

View file

@ -0,0 +1,139 @@
#include <regex>
#include <nlohmann/json.hpp>
#include <gtest/gtest.h>
#include "worker-protocol.hh"
#include "worker-protocol-impl.hh"
#include "derived-path.hh"
#include "tests/libstore.hh"
namespace nix {
class WorkerProtoTest : public LibStoreTest
{
public:
Path unitTestData = getEnv("_NIX_TEST_UNIT_DATA").value() + "/libstore/worker-protocol";
bool testAccept() {
return getEnv("_NIX_TEST_ACCEPT") == "1";
}
Path goldenMaster(std::string_view testStem) {
return unitTestData + "/" + testStem + ".bin";
}
/**
* Golden test for `T` reading
*/
template<typename T>
void readTest(PathView testStem, T value)
{
if (testAccept())
{
GTEST_SKIP() << "Cannot read golden master because another test is also updating it";
}
else
{
auto expected = readFile(goldenMaster(testStem));
T got = ({
StringSource from { expected };
WorkerProto::Serialise<T>::read(
*store,
WorkerProto::ReadConn { .from = from });
});
ASSERT_EQ(got, value);
}
}
/**
* Golden test for `T` write
*/
template<typename T>
void writeTest(PathView testStem, const T & value)
{
auto file = goldenMaster(testStem);
StringSink to;
WorkerProto::write(
*store,
WorkerProto::WriteConn { .to = to },
value);
if (testAccept())
{
createDirs(dirOf(file));
writeFile(file, to.s);
GTEST_SKIP() << "Updating golden master";
}
else
{
auto expected = readFile(file);
ASSERT_EQ(to.s, expected);
}
}
};
#define CHARACTERIZATION_TEST(NAME, STEM, VALUE) \
TEST_F(WorkerProtoTest, NAME ## _read) { \
readTest(STEM, VALUE); \
} \
TEST_F(WorkerProtoTest, NAME ## _write) { \
writeTest(STEM, VALUE); \
}
CHARACTERIZATION_TEST(
string,
"string",
(std::tuple<std::string, std::string, std::string, std::string, std::string> {
"",
"hi",
"white rabbit",
"大白兔",
"oh no \0\0\0 what was that!",
}))
CHARACTERIZATION_TEST(
storePath,
"store-path",
(std::tuple<StorePath, StorePath> {
StorePath { "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-foo" },
StorePath { "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-foo-bar" },
}))
CHARACTERIZATION_TEST(
contentAddress,
"content-address",
(std::tuple<ContentAddress, ContentAddress, ContentAddress> {
ContentAddress {
.method = TextIngestionMethod {},
.hash = hashString(HashType::htSHA256, "Derive(...)"),
},
ContentAddress {
.method = FileIngestionMethod::Flat,
.hash = hashString(HashType::htSHA1, "blob blob..."),
},
ContentAddress {
.method = FileIngestionMethod::Recursive,
.hash = hashString(HashType::htSHA256, "(...)"),
},
}))
CHARACTERIZATION_TEST(
derivedPath,
"derived-path",
(std::tuple<DerivedPath, DerivedPath> {
DerivedPath::Opaque {
.path = StorePath { "g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-foo" },
},
DerivedPath::Built {
.drvPath = makeConstantStorePathRef(StorePath {
"g1w7hy3qg1w7hy3qg1w7hy3qg1w7hy3q-bar.drv",
}),
.outputs = OutputsSpec::Names { "x", "y" },
},
}))
}

View file

@ -75,4 +75,20 @@ void WorkerProto::Serialise<std::map<K, V>>::write(const Store & store, WorkerPr
} }
} }
template<typename... Ts>
std::tuple<Ts...> WorkerProto::Serialise<std::tuple<Ts...>>::read(const Store & store, WorkerProto::ReadConn conn)
{
return std::tuple<Ts...> {
WorkerProto::Serialise<Ts>::read(store, conn)...,
};
}
template<typename... Ts>
void WorkerProto::Serialise<std::tuple<Ts...>>::write(const Store & store, WorkerProto::WriteConn conn, const std::tuple<Ts...> & res)
{
std::apply([&]<typename... Us>(const Us &... args) {
(WorkerProto::Serialise<Us>::write(store, conn, args), ...);
}, res);
}
} }

View file

@ -28,6 +28,8 @@ class Store;
struct Source; struct Source;
// items being serialised // items being serialised
class StorePath;
struct ContentAddress;
struct DerivedPath; struct DerivedPath;
struct DrvOutput; struct DrvOutput;
struct Realisation; struct Realisation;
@ -220,6 +222,8 @@ template<typename T>
MAKE_WORKER_PROTO(std::vector<T>); MAKE_WORKER_PROTO(std::vector<T>);
template<typename T> template<typename T>
MAKE_WORKER_PROTO(std::set<T>); MAKE_WORKER_PROTO(std::set<T>);
template<typename... Ts>
MAKE_WORKER_PROTO(std::tuple<Ts...>);
template<typename K, typename V> template<typename K, typename V>
#define X_ std::map<K, V> #define X_ std::map<K, V>

Binary file not shown.

Binary file not shown.