Infinite recursion when modularizing flake runNixOSTest

I’m pretty sure I’m missing something fundamental here. Why does this standalone flake nix -L build properly

# flake.nix
{
  description = "example";

  inputs = {
    impermanence.url = "github:nix-community/impermanence";
    nixpkgs.url = "github:nixos/nixpkgs/nixos-24.11";
  };

  outputs = { self, nixpkgs, ... }@inputs: {
    packages.x86_64-linux.default = nixpkgs.legacyPackages.x86_64-linux.testers.runNixOSTest {
      name = "test";

      nodes."vm" = {
        imports = [
          inputs.impermanence.nixosModules.impermanence
        ];
      };

      testScript = ''
        # testing here
      '';
    };
  };
}

But if I try to modularize the runNixOSTest like so:

# flake.nix
{
  description = "example";

  inputs = {
    impermanence.url = "github:nix-community/impermanence";
    nixpkgs.url = "github:nixos/nixpkgs/nixos-24.11";
  };

  outputs = { self, nixpkgs, ... }@inputs: {
    packages.x86_64-linux.default = nixpkgs.legacyPackages.x86_64-linux.testers.runNixOSTest ./test.nix;
  };
}
# test.nix
{
  name = "test";

  nodes."vm" = { inputs, ...}: {
    imports = [
      inputs.impermanence.nixosModules.impermanence # infinite recursion
    ];
  };

  testScript = ''
    # testing here
  '';
}

I get an infinite recursion at inputs? Probably something basic on my end, appreciate the help.

First thing I see is you didn’t pass inputs into the test.nix file.
That would then cause {inputs, ...}: to not really make sense (because the module system doesn’t know where to get inputs from and it infrecs on _module.args.

I’m not familiar with the testers but does replacing ./test.nix with import ./test.nix { inherit inputs; }, and then putting {inputs}: at the top of test.nix work?

EDIT: and remove the {inputs, ...}: bit, since I don’t see a simple way to provide that module arg here. _module.args = { inherit inputs; }; wouldn’t work because that’s a different infrec because you’ll use inputs within imports.

1 Like

In addition to @waffle8946 recommendation, you can also inline everything in flake like so.

When I do that, nix -L build complains about a <<potential inifinite recursion>>. I’m not sure what to make of that but the short exception is:

error:
       … while evaluating the attribute 'config.result'
         at /nix/store/fz4h8yz3qr83p6cfhisgj02knjqg6nxs-source/lib/modules.nix:334:9:
          333|         options = checked options;
          334|         config = checked (removeAttrs config [ "_module" ]);
             |         ^
          335|         _module = checked (config._module);

       … while calling the 'seq' builtin
         at /nix/store/fz4h8yz3qr83p6cfhisgj02knjqg6nxs-source/lib/modules.nix:334:18:
          333|         options = checked options;
          334|         config = checked (removeAttrs config [ "_module" ]);
             |                  ^
          335|         _module = checked (config._module);

       (stack trace truncated; use '--show-trace' to show the full, detailed trace)

       error: cannot coerce a set to a string: { config = «potential infinite recursion»; lib = { __unfix__ = «lambda @ /nix/store/fz4h8yz3qr83p6cfhisgj02knjqg6nxs-source/lib/fixed-points.nix:447:7»; add = «thunk»; addContextFrom = «thunk»; addErrorContext = «thunk»; addMetaAttrs = «thunk»; all = «thunk»; allUnique = «thunk»; and = «thunk»; any = «thunk»; «438 attributes elided» }; «2 attributes elided» }

Is this maybe a round about way of saying the module system can’t find config?

Is in-lining runNixOSTest entirely inside flake.nix commonplace? If you have a large amount of tests, this would clutter the flake and seemingly conflict with the separation between flake boilerplate definitions and modules.

Is in-lining runNixOSTest entirely inside flake.nix commonplace?

That’s not the only way of doing it. It’s easy for a having a couple of checks or so, or for scratch purposes – you get inputs reference very quickly this way. Imports and separate files work well too.

These code examples work(e.g. nix run -L .\#checks.x86_64-linux.test.driverInteractive) and are equivalent:

{
  inputs = {
    impermanence.url = "github:nix-community/impermanence";
    nixpkgs.url = "github:nixos/nixpkgs/nixos-24.11";
  };

  outputs =
    inputs@{ nixpkgs, ... }:
    let
      system = "x86_64-linux";
      pkgs = import nixpkgs { inherit system; };
    in
    {
      # Runnable via
      # ❯ nix run -L .\#checks.x86_64-linux.test.driverInteractive
      checks.${system}.test = pkgs.testers.runNixOSTest {
        name = "test";
        nodes.machine1 =
          _: # { config, pkgs, ... }:
          {
            services.getty.autologinUser = "root";
            imports = [
              inputs.impermanence.nixosModules.impermanence
            ];
          };
        # If developing a proper test script, see
        # https://nixos.org/manual/nixos/stable/#ssec-machine-objects
        testScript = "start_all()";
      };
    };
}

or two files;

# flake.nix
{
  inputs = {
    impermanence.url = "github:nix-community/impermanence";
    nixpkgs.url = "github:nixos/nixpkgs/nixos-24.11";
  };

  outputs =
    inputs@{ nixpkgs, ... }:
    let
      system = "x86_64-linux";
      pkgs = import nixpkgs { inherit system; };
    in
    {
      # Runnable via
      # ❯ nix run -L .\#checks.x86_64-linux.test.driverInteractive
      checks.${system}.test = pkgs.testers.runNixOSTest (import ./test.nix { inherit inputs; });
    };
}

# test.nix

{ inputs, ... }:
{
  name = "test";
  nodes.machine1 =
    _: # { config, pkgs, ... }:
    {
      services.getty.autologinUser = "root";
      imports = [
        inputs.impermanence.nixosModules.impermanence
      ];
    };
  # If developing a proper test script, see
  # https://nixos.org/manual/nixos/stable/#ssec-machine-objects
  testScript = "start_all()";
}
1 Like

Gotcha, in my case I was missing the ( ) so the import ./test.nix { inherit inputs; } wasn’t working correctly. Putting the runNixOSTest under checks.${system}.<name> is really nice as well. Thank you for your help!

Yeah, import needs to be applied first – that’s what () does.

This could also be the case when using lib.modules.importApply would had produced a better error (though it still needs to be applied first):

{
  inputs = {
    impermanence.url = "github:nix-community/impermanence";
    nixpkgs.url = "github:nixos/nixpkgs/nixos-24.11";
  };

  outputs =
    inputs@{ nixpkgs, ... }:
    let
      system = "x86_64-linux";
      pkgs = import nixpkgs { inherit system; };
    in
    {
      # Runnable via
      # ❯ nix run -L .\#checks.x86_64-linux.test.driverInteractive
      checks.${system}.test = pkgs.testers.runNixOSTest pkgs.lib.modules.importApply ./test.nix {
        inherit inputs;
      };
    };
}

Produces:

error: module the argument that was passed to pkgs.runNixOSTest (:anon-12:anon-1:anon-1:anon-1) does not look like a module.