summary refs log tree commit diff
path: root/checks.nix
blob: c20451a0ae7a2188a7c897c1678b5fc0e45e8929 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
{ pkgs, self, system, ... }:

let
    # The copy of `lib` from `pkgs` is not quite the same as the `lib` that
    # modules get. We're pretending this file is analogous to a package
    # definition, so this is the only one we have available. It's declared
    # explicitly here because doing this in `flake.nix` would have been harder
    # to explain.
    lib = pkgs.lib;

    # `mkNixEvalCheck` is a helper function that defines a check which
    # evaluates some nix code, and diffs the evaluation result against an
    # expected result.
    #
    # While you may occasionally call this directly, it is more likely that
    # you want `mkNixModuleCheck`. This one is presented first for ease of
    # understanding how they work.
    #
    # To do this, it sets up a nested instance of `nix` which runs within the
    # sandbox and can't touch the outer system. This is slightly involved, so
    # this helper is its own thing even though in practice we will also need
    # module handling logic for everything we're interested in testing.
    mkNixEvalCheck = name: input: expected:
        # Since this is a convenience wrapper, the implementation is trivial.
        mkNixEvalCheckWith { inherit name input expected; };

    # `mkNixEvalCheckWith` is the underlying implmentation of
    # `mkNixEvalCheck`. It has extra options, which are needed by
    # `mkNixModuleCheck` and likely not by anything else.
    mkNixEvalCheckWith =
          {
            # These parameters have the same meanings as in
            # `mkNixEvalCheck`.
            name, input, expected,

            # An attribute set of additional source files to link in so the
            # builder can use them, keyed by their filenames.
            extraSources ? { }
          }:

          let
              # Optionally, `input` can take a `sources` parameter to find
              # other files it relies on. We apply that parameter here.
              #
              # Making this work correctly is a lot of code, especially
              # compared to just using `builtins.readFile`. It's worth it,
              # because it makes any stack traces produced during testing
              # significantly more readable, by adding filenames to them.
              appliedInput = if builtins.isFunction input
                               then input sourceFiles
                               else input;

              # An attribute set of names and file contents in the store for
              # data that should be available to the build script via `src`
              # and to `appliedInput` via `sources`.
              #
              # This family of three variables exists so that `appliedInput`
              # doesn't need to have a cyclic dependency.
              sourceContents = extraSources // {
                                 "input.nix" = appliedInput;
                                 "expected.json" = "${expected}\n";
                               };

              # An attribute set of names and derivations that should be
              # available to the build script via `mkDerivation`'s `src`
              # parameter.
              #
              # This family of three variables exists so that `appliedInput`
              # doesn't need to have a cyclic dependency.
              sourceDerivations =
                  lib.mapAttrs (name: value: pkgs.writeTextDir name value)
                               sourceContents;

              # An attribute set of names and complete file paths in the store
              # that should be available to `appliedInput` via its `sources`
              # parameter.
              #
              # This family of three variables exists so that `appliedInput`
              # doesn't need to have a cyclic dependency.
              sourceFiles =
                  lib.mapAttrs (name: value: "${value}/${name}")
                               sourceDerivations;

          in pkgs.stdenv.mkDerivation {
            name = "secrets-nix-test-${name}";

            # The easiest way to get the test's input into a format the
            # build script can work with is to use it as the `src`.
            #
            # We construct a source directory with two files `input` and
            # `expected`. We also respect any additional files from
            # `extraSources`.
            src = pkgs.symlinkJoin {
              name = "secrets-nix-test-${name}-src";
              paths = lib.foldlAttrs
                          (result: name: value: result ++ [ value ])
                          [ ]
                          sourceDerivations;
            };

            nativeBuildInputs = with pkgs; [ diffutils nix ];

            # Recall that a nix flake check is a derivation; the check
            # succeeds if and only if the derivation builds successfully.
            # Sadly, `checkPhase` is skipped, so we do all the work in
            # `buildPhase`.
            buildPhase = ''
              # Because we potentially build some derivations, we need a
              # proper temporary directory for the store; we can't just use
              # the dummy store.
              mkdir nix-store
              ${pkgs.nix}/bin/nix \
                  --extra-experimental-features nix-command \
                  --store /build/nix-store \
                  --show-trace \
                  eval --json --file $src/input.nix > $out

              # The exit code of diff is what we want for this. Yay!
              if ! ${pkgs.diffutils}/bin/diff $src/expected.json $out; then
                # It's good to keep the user-programmer in the loop...
                echo
                echo "This is a nix evaluation test case. The expected eval"
                echo "output differed from the actual output. In an ideal"
                echo "world, the above diff would help you understand why."
                echo
                false
              fi
            '';
          };


    # `mkNixModuleCheck` builds on the behavior of `mkNixEvalCheck`,
    # attempting to call `evalModules` on a user-provided module, combining it
    # with the smalltech options module and with an appropriate version of
    # nixpkgs itself.
    #
    # This helper is intended to be useful for writing test cases that
    # simulate configuring a complete system. It is the helper we are likely
    # to use most frequently.
    #
    # It defers to `mkNixModuleCheckWith` for all the real work, so
    # as to be able to provide some sensible defaults while still being
    # configurable.
    mkNixModuleCheck =
        # This name will be appended to the derivation name. Set it to
        # something distinctive; it appears in error messages.
        name:

        # This should be a string (usually written as a multiline string
        # literal) giving the exact text of the input module.
        #
        # Unfortunately it needs to be a string, not an arbitrary nix
        # expression, because it needs to be serialized so the inner nix can
        # get a copy of it.
        input:

        # This should be a nix expression evaluating to an attribute set,
        # corresponding to the expected output of the test case.
        #
        # It will be serialized to JSON before comparing it against the actual
        # output.
        expected:

        # Since this is a convenience wrapper, the implementation is trivial.
        mkNixModuleCheckWith { inherit name input expected; };


      # `mkNixModuleCheckWith` is the underlying implementation of
      # `mkNixModuleCheck`. The only situation in which you'd use it directly
      # is if you need to set a custom `keyPath`.
      #
      # Please notice the somewhat subtle implementation; this is generating
      # a text file containing nix code, which is interpreted by the nested
      # nix. That's why we have to go to so much trouble to pass things around
      # as strings.
      #
      # Fortunately for passing `nixpkgs` itself, we can reference
      # the path to where it's already present in the filesystem as an input
      # to this flake in the outer nix; otherwise the test's input file would
      # be many megabytes in size.
      #
      # Less fortunately, for `options.nix` and the test input, we need to
      # include the contents textually. This leads to some awkwardness and
      # limitations, but it should be reliable.
      mkNixModuleCheckWith =
          {
            # These parameters have the same meanings as in
            # `mkNixModuleCheck`.
            name, input, expected,

            # Since the return value of a module potentially contains
            # functions and other things we can't serialize, we always extract
            # a sub-value from within it, and compare against that.
            #
            # The default is likely to be all we need for most cases.
            keyPath ? "config.secrets.export"
          }: mkNixEvalCheckWith {
            inherit name;

            extraSources = {
              "options.nix" = builtins.readFile ./options.nix;
              "test-input-module.nix" = input;
            };

            # We rely on the `sources` parameter; for details of how it works,
            # see the binding of `appliedInput` in the definition of
            # `mkNixEvalCheckWith`, above.
            #
            # It's important that the copy of `pkgs` available to the modules
            # in the inner nix be the same one this file is using for its own
            # needs, here in the outer nix. Otherwise, things like
            # `${pkgs.bash}` in expected-output values won't work correctly,
            # and since scripts are a critical part of the output, that's
            # mandatory.
            #
            # The way it's passed through here should work, but if it breaks,
            # well, hopefully this comment saved you some time.
            input = sources: ''
              let pkgs = import ${self.inputs.nixpkgs} { };
              in (pkgs.lib.evalModules {
                    modules = [
                      {
                        imports = [
                          ${sources."options.nix"}
                          ${sources."test-input-module.nix"}
                        ];
                      }
                    ];
                    specialArgs = { inherit pkgs; };
                 }).${keyPath}
            '';

           expected = builtins.toJSON expected;
         };

in {
  # Verify that the nix evaluator test harness works.
  trivial = mkNixEvalCheck "trivial" "1 + 2" "3";

  # Verify that when no secrets are configured, we correctly export a set
  # containing no secrets.
  empty = mkNixModuleCheck "empty" ''
    {
      config = { };
    }
  '' { };

  # Verify that when a single specific secret is configured, we correctly
  # export a set containing a description of it, and nothing else.
  single = mkNixModuleCheck "single" ''
    {
      config = {
        secrets.secrets.example = {
          filename = "example.key";
          script = '''
            touch example.key
          ''';
        };
      };
    }
  '' {
    example = {
      path = "/etc/nixos/secrets/example.key";
      script = ''
        #!${pkgs.bash}/bin/bash
        touch example.key
      '';
    };
  };

  ### ADD MORE NIX MODULE TEST CASES ABOVE THIS LINE! :)

  # Verify that the Rust command-line tool builds correctly.
  rust = self.packages.${system}.default;
}