summaryrefslogtreecommitdiff
path: root/pkgs/development/interpreters/python/hooks/python-catch-conflicts-hook-tests.nix
blob: d1ae10648a17b092a7092bc1afbd47093c0dbd06 (plain)
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
{
  lib,
  pythonOnBuildForHost,
  runCommand,
  writeShellScript,
  coreutils,
  gnugrep,
}:
let

  pythonPkgs = pythonOnBuildForHost.pkgs;

  ### UTILITIES

  # customize a package so that its store paths differs
  customize = pkg: pkg.overrideAttrs { some_modification = true; };

  # generates minimal pyproject.toml
  pyprojectToml =
    pname:
    builtins.toFile "pyproject.toml" ''
      [project]
      name = "${pname}"
      version = "1.0.0"
    '';

  # generates source for a python project
  projectSource =
    pname:
    runCommand "my-project-source" { } ''
      mkdir -p $out/src
      cp ${pyprojectToml pname} $out/pyproject.toml
      touch $out/src/__init__.py
    '';

  # helper to reduce boilerplate
  generatePythonPackage =
    args:
    pythonPkgs.buildPythonPackage (
      {
        version = "1.0.0";
        src = runCommand "my-project-source" { } ''
          mkdir -p $out/src
          cp ${pyprojectToml args.pname} $out/pyproject.toml
          touch $out/src/__init__.py
        '';
        pyproject = true;
        catchConflicts = true;
        buildInputs = [ pythonPkgs.setuptools ];
      }
      // args
    );

  # in order to test for a failing build, wrap it in a shell script
  expectFailure =
    build: errorMsg:
    lib.overrideDerivation build (old: {
      builder = writeShellScript "test-for-failure" ''
        export PATH=${coreutils}/bin:${gnugrep}/bin:$PATH
        ${old.builder} "$@" > ./log 2>&1
        status=$?
        cat ./log
        if [ $status -eq 0 ] || ! grep -q "${errorMsg}" ./log; then
          echo "The build should have failed with '${errorMsg}', but it didn't"
          exit 1
        else
          echo "The build failed as expected with: ${errorMsg}"
          mkdir -p $out
        fi
      '';
    });
in
{

  ### TEST CASES

  # Test case which must not trigger any conflicts.
  # This derivation has runtime dependencies on custom versions of multiple build tools.
  # This scenario is relevant for lang2nix tools which do not override the nixpkgs fix-point.
  # see https://github.com/NixOS/nixpkgs/issues/283695
  ignores-build-time-deps = generatePythonPackage {
    pname = "ignores-build-time-deps";
    buildInputs = [
      pythonPkgs.build
      pythonPkgs.packaging
      pythonPkgs.setuptools
      pythonPkgs.wheel
    ];
    propagatedBuildInputs = [
      # Add customized versions of build tools as runtime deps
      (customize pythonPkgs.packaging)
      (customize pythonPkgs.setuptools)
      (customize pythonPkgs.wheel)
    ];
  };

  # multi-output derivation with dependency on itself must not crash
  cyclic-dependencies = generatePythonPackage {
    pname = "cyclic-dependencies";
    preFixup = ''
      appendToVar propagatedBuildInputs "$out"
    '';
  };

  # Simplest test case that should trigger a conflict
  catches-simple-conflict =
    let
      # this build must fail due to conflicts
      package = pythonPkgs.buildPythonPackage rec {
        pname = "catches-simple-conflict";
        version = "0.0.0";
        src = projectSource pname;
        pyproject = true;
        catchConflicts = true;
        buildInputs = [
          pythonPkgs.setuptools
        ];
        # depend on two different versions of packaging
        # (an actual runtime dependency conflict)
        propagatedBuildInputs = [
          pythonPkgs.packaging
          (customize pythonPkgs.packaging)
        ];
      };
    in
    expectFailure package "Found duplicated packages in closure for dependency 'packaging'";

  /*
    More complex test case with a transitive conflict

    Test sets up this dependency tree:

      toplevel
      ├── dep1
      │   └── leaf
      └── dep2
          └── leaf (customized version -> conflicting)
  */
  catches-transitive-conflict =
    let
      # package depending on both dependency1 and dependency2
      toplevel = generatePythonPackage {
        pname = "catches-transitive-conflict";
        propagatedBuildInputs = [
          dep1
          dep2
        ];
      };
      # dep1 package depending on leaf
      dep1 = generatePythonPackage {
        pname = "dependency1";
        propagatedBuildInputs = [ leaf ];
      };
      # dep2 package depending on conflicting version of leaf
      dep2 = generatePythonPackage {
        pname = "dependency2";
        propagatedBuildInputs = [ (customize leaf) ];
      };
      # some leaf package
      leaf = generatePythonPackage {
        pname = "leaf";
      };
    in
    expectFailure toplevel "Found duplicated packages in closure for dependency 'leaf'";

  /*
    Transitive conflict with multiple dependency chains leading to the
    conflicting package.

    Test sets up this dependency tree:

      toplevel
      ├── dep1
      │   └── leaf
      ├── dep2
      │   └── leaf
      └── dep3
          └── leaf (customized version -> conflicting)
  */
  catches-conflict-multiple-chains =
    let
      # package depending on dependency1, dependency2 and dependency3
      toplevel = generatePythonPackage {
        pname = "catches-conflict-multiple-chains";
        propagatedBuildInputs = [
          dep1
          dep2
          dep3
        ];
      };
      # dep1 package depending on leaf
      dep1 = generatePythonPackage {
        pname = "dependency1";
        propagatedBuildInputs = [ leaf ];
      };
      # dep2 package depending on leaf
      dep2 = generatePythonPackage {
        pname = "dependency2";
        propagatedBuildInputs = [ leaf ];
      };
      # dep3 package depending on conflicting version of leaf
      dep3 = generatePythonPackage {
        pname = "dependency3";
        propagatedBuildInputs = [ (customize leaf) ];
      };
      # some leaf package
      leaf = generatePythonPackage {
        pname = "leaf";
      };
    in
    expectFailure toplevel "Found duplicated packages in closure for dependency 'leaf'";
}