forked from lix-project/lix
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:
parent
f878b422b0
commit
7ff43435f9
1
Makefile
1
Makefile
|
@ -22,6 +22,7 @@ makefiles = \
|
|||
-include Makefile.config
|
||||
|
||||
ifeq ($(tests), yes)
|
||||
UNIT_TEST_ENV = _NIX_TEST_UNIT_DATA=unit-test-data
|
||||
makefiles += \
|
||||
src/libutil/tests/local.mk \
|
||||
src/libstore/tests/local.mk \
|
||||
|
|
|
@ -2,14 +2,70 @@
|
|||
|
||||
## Unit-tests
|
||||
|
||||
The unit-tests for each Nix library (`libexpr`, `libstore`, etc..) are defined
|
||||
under `src/{library_name}/tests` using the
|
||||
[googletest](https://google.github.io/googletest/) and
|
||||
[rapidcheck](https://github.com/emil-e/rapidcheck) frameworks.
|
||||
The unit tests are defined using the [googletest] and [rapidcheck] frameworks.
|
||||
|
||||
[googletest]: https://google.github.io/googletest/
|
||||
[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`.
|
||||
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
|
||||
|
||||
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.
|
||||
|
||||
It is frequently useful to regenerate the expected output.
|
||||
To do that, rerun the failed test with `_NIX_TEST_ACCEPT=1`.
|
||||
(At least, this is the convention we've used for `tests/lang.sh`.
|
||||
If we add more characterization testing we should always strive to be consistent.)
|
||||
To do that, rerun the failed test(s) with `_NIX_TEST_ACCEPT=1`.
|
||||
For example:
|
||||
```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".
|
||||
The language tests are, again, an example of this.
|
||||
|
|
|
@ -75,6 +75,7 @@
|
|||
./precompiled-headers.h
|
||||
./src
|
||||
./tests
|
||||
./unit-test-data
|
||||
./COPYING
|
||||
./scripts/local.mk
|
||||
(fileset.fileFilter (f: lib.strings.hasPrefix "nix-profile" f.name) ./scripts)
|
||||
|
|
|
@ -87,6 +87,6 @@ define build-program
|
|||
# Phony target to run this program (typically as a dependency of 'check').
|
||||
.PHONY: $(1)_RUN
|
||||
$(1)_RUN: $$($(1)_PATH)
|
||||
$(trace-test) $$($(1)_PATH)
|
||||
$(trace-test) $$(UNIT_TEST_ENV) $$($(1)_PATH)
|
||||
|
||||
endef
|
||||
|
|
139
src/libstore/tests/worker-protocol.cc
Normal file
139
src/libstore/tests/worker-protocol.cc
Normal 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" },
|
||||
},
|
||||
}))
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -28,6 +28,8 @@ class Store;
|
|||
struct Source;
|
||||
|
||||
// items being serialised
|
||||
class StorePath;
|
||||
struct ContentAddress;
|
||||
struct DerivedPath;
|
||||
struct DrvOutput;
|
||||
struct Realisation;
|
||||
|
@ -220,6 +222,8 @@ template<typename T>
|
|||
MAKE_WORKER_PROTO(std::vector<T>);
|
||||
template<typename T>
|
||||
MAKE_WORKER_PROTO(std::set<T>);
|
||||
template<typename... Ts>
|
||||
MAKE_WORKER_PROTO(std::tuple<Ts...>);
|
||||
|
||||
template<typename K, typename V>
|
||||
#define X_ std::map<K, V>
|
||||
|
|
BIN
unit-test-data/libstore/worker-protocol/content-address.bin
Normal file
BIN
unit-test-data/libstore/worker-protocol/content-address.bin
Normal file
Binary file not shown.
BIN
unit-test-data/libstore/worker-protocol/derived-path.bin
Normal file
BIN
unit-test-data/libstore/worker-protocol/derived-path.bin
Normal file
Binary file not shown.
BIN
unit-test-data/libstore/worker-protocol/store-path.bin
Normal file
BIN
unit-test-data/libstore/worker-protocol/store-path.bin
Normal file
Binary file not shown.
BIN
unit-test-data/libstore/worker-protocol/string.bin
Normal file
BIN
unit-test-data/libstore/worker-protocol/string.bin
Normal file
Binary file not shown.
Loading…
Reference in a new issue