NixOS VM tests: Difference between revisions

imported>Olafklingt
add examples
imported>Olafklingt
move haskellforall example from nix.dev here
Line 219: Line 219:
       '';
       '';
     }
     }
== Tests that need multiple virtual machines ==
Tests can involve multiple virtual machines.
This example uses the use-case of a [https://en.m.wikipedia.org/wiki/REST REST] interface to a [https://www.postgresql.org/ PostgreSQL] database.
The following example Nix expression is adapted from [https://www.haskellforall.com/2020/11/how-to-use-nixos-for-lightweight.html How to use NixOS for lightweight integration tests].
This tutorial follows [https://postgrest.org/en/stable/tutorials/tut0.html PostgREST tutorial], a generic [https://restfulapi.net/ RESTful API] for PostgreSQL.
If you skim over the official tutorial, you'll notice there's quite a bit of setup in order to test if all the steps work.
The setup includes:
- A virtual machine named `server` running PostgreSQL and PostgREST.
- A virtual machine named `client` running HTTP client queries using `curl`.
- A `testScript` orchestrating testing logic between `client` and `server`.
The complete `postgrest.nix` file looks like the following:
  let
    # Pin Nixpkgs, as some packages are broken in the 22.11 release
    nixpkgs = fetchTarball "https://github.com/NixOS/nixpkgs/archive/0f8f64b54ed07966b83db2f20c888d5e035012ef.tar.gz";
    pkgs = import nixpkgs { config = {}; overlays = []; };
 
    # Single source of truth for all tutorial constants
    database = "postgres";
    schema        = "api";
    table        = "todos";
    username      = "authenticator";
    password      = "mysecretpassword";
    webRole      = "web_anon";
    postgrestPort = 3000;
 
    # NixOS module shared between server and client
    sharedModule = {
      # Since it's common for CI not to have $DISPLAY available, explicitly disable graphics support
      virtualisation.graphics = false;
    };
 
  in
    pkgs.nixosTest {
      # NixOS tests are run inside a virtual machine, and here you specify its system type
      system = "x86_64-linux";
      name = "postgres-test";
      nodes = {
        server = { config, pkgs, ... }: {
          imports = [ sharedModule ];
 
          networking.firewall.allowedTCPPorts = [ postgrestPort ];
 
          services.postgresql = {
            enable = true;
 
            initialScript = pkgs.writeText "initialScript.sql" ''
              create schema ${schema};
 
              create table ${schema}.${table} (
                  id serial primary key,
                  done boolean not null default false,
                  task text not null,
                  due timestamptz
              );
 
              insert into ${schema}.${table} (task) values ('finish tutorial 0'), ('pat self on back');
 
              create role ${webRole} nologin;
              grant usage on schema ${schema} to ${webRole};
              grant select on ${schema}.${table} to ${webRole};
 
              create role ${username} inherit login password '${password}';
              grant ${webRole} to ${username};
            '';
          };
 
          users = {
            mutableUsers = false;
            users = {
              # For ease of debugging the VM as the `root` user
              root.password = "";
 
              # Create a system user that matches the database user so that you
              # can use peer authentication. The tutorial defines a password,
              # but it's not necessary.
              "${username}".isSystemUser = true;
            };
          };
 
          systemd.services.postgrest = {
            wantedBy = [ "multi-user.target" ];
            after = [ "postgresql.service" ];
            script =
              let
                configuration = pkgs.writeText "tutorial.conf" ''
                    db-uri = "postgres://${username}:${password}@localhost:${toString config.services.postgresql.port}/${database}"
                    db-schema = "${schema}"
                    db-anon-role = "${username}"
                '';
              in "${pkgs.haskellPackages.postgrest}/bin/postgrest ${configuration}";
            serviceConfig.User = username;
          };
        };
 
        client = {
          imports = [ sharedModule ];
        };
      };
 
      # Disable linting for simpler debugging of the testScript
      skipLint = true;
 
      testScript = ''
        import json
        import sys
 
        start_all()
 
        server.wait_for_open_port(${toString postgrestPort})
 
        expected = [
            {"id": 1, "done": False, "task": "finish tutorial 0", "due": None},
            {"id": 2, "done": False, "task": "pat self on back", "due": None},
        ]
 
        actual = json.loads(
            client.succeed(
                "${pkgs.curl}/bin/curl http://server:${toString postgrestPort}/${table}"
            )
        )
 
        assert expected == actual, "table query returns expected content"
      '';
  }
Unlike the previous example, the virtual machines need an expressive name to distinguish them.
For this example we choose `client` and `server`.
Set up all machines and run the test script:
  nix-build postgrest.nix
  ...
  test script finished in 10.96s
  cleaning up
  killing client (pid 10)
  killing server (pid 22)
  (0.00 seconds)
  /nix/store/bx7z3imvxxpwkkza10vb23czhw7873w2-vm-test-run-unnamed