@@ -5,11 +5,13 @@ import Distribution.Client.IndexUtils.ActiveRepos
55import qualified Distribution.Compat.NonEmptySet as NES
66import Distribution.Package
77import Distribution.Simple.Utils (toUTF8LBS )
8+ import Distribution.Solver.Types.PackageIndex (OverrideOrMerge (.. ))
89import qualified Distribution.Solver.Types.PackageIndex as PackageIndex
910import Distribution.Types.LibraryName
1011import Distribution.Version
1112
1213import qualified Data.List as List
14+ import qualified Data.Map.Strict as Map
1315
1416import Test.Tasty
1517import Test.Tasty.HUnit
@@ -18,6 +20,9 @@ tests :: [TestTree]
1820tests =
1921 [ simpleVersionsParserTests
2022 , indexCombiningTests
23+ , overrideOrMergeTests
24+ , deprecationAwareStrategyTests
25+ , addIndexTests
2126 ]
2227
2328-- ---------------------------------------------------------------------------
@@ -193,3 +198,183 @@ repoBar1 = PackageIndex.fromList [bar1]
193198repoFoo12 , repoFoo1bar1 :: PackageIndex. PackageIndex PackageIdentifier
194199repoFoo12 = PackageIndex. fromList [foo1, foo2]
195200repoFoo1bar1 = PackageIndex. fromList [foo1, bar1]
201+
202+ -- ---------------------------------------------------------------------------
203+ -- overrideOrMerge tests
204+ --
205+ -- These test PackageIndex.overrideOrMerge directly, which is the building
206+ -- block for conditionally falling back to merge when all versions of a
207+ -- package in the override repo are deprecated (issue #8502).
208+ -- ---------------------------------------------------------------------------
209+
210+ overrideOrMergeTests :: TestTree
211+ overrideOrMergeTests =
212+ testGroup
213+ " overrideOrMerge"
214+ [ testCase " all-Override strategy matches plain override" $
215+ -- When strategy always returns Override, result equals PackageIndex.override
216+ let result = PackageIndex. overrideOrMerge (const Override ) repoFoo12 repoFoo2
217+ expected = PackageIndex. override repoFoo12 repoFoo2
218+ in allPkgs result @?= allPkgs expected
219+ , testCase " all-Merge strategy matches plain merge" $
220+ -- When strategy always returns Merge, result equals PackageIndex.merge
221+ let result = PackageIndex. overrideOrMerge (const Merge ) repoFoo12 repoFoo2
222+ expected = PackageIndex. merge repoFoo12 repoFoo2
223+ in allPkgs result @?= allPkgs expected
224+ , testCase " Override: second index wins entire package bucket" $
225+ -- repoFoo12 has foo-1.0 and foo-2.0; repoFoo2 has only foo-2.0.
226+ -- Override means repoFoo2 wins the 'foo' bucket, so foo-1.0 is hidden.
227+ allPkgs (PackageIndex. overrideOrMerge (const Override ) repoFoo12 repoFoo2)
228+ @?= [foo2]
229+ , testCase " Merge: both buckets combined, duplicates removed" $
230+ -- repoFoo12 has foo-1.0 and foo-2.0; repoFoo2 has only foo-2.0.
231+ -- Merge keeps foo-1.0 and foo-2.0 (foo-2.0 deduplicated).
232+ allPkgs (PackageIndex. overrideOrMerge (const Merge ) repoFoo12 repoFoo2)
233+ @?= List. sort [foo1, foo2]
234+ , testCase " mixed strategy: Override for foo, Merge for bar" $
235+ -- repoFoo1bar1 has foo-1.0 and bar-1.0; second index has foo-2.0 and bar-1.0.
236+ -- foo is overridden (foo-1.0 hidden), bar is merged (bar-1.0 deduplicated).
237+ let i2 = PackageIndex. fromList [foo2, bar1]
238+ strategy name
239+ | name == mkPackageName " foo" = Override
240+ | otherwise = Merge
241+ in allPkgs (PackageIndex. overrideOrMerge strategy repoFoo1bar1 i2)
242+ @?= List. sort [foo2, bar1]
243+ , testCase " deprecated fallback: Merge when override repo has only deprecated versions" $
244+ -- Simulates the issue-8502 scenario: repo-a has foo-1.0; repo-b (override)
245+ -- has foo-2.0 but all its versions are deprecated. The caller detects this
246+ -- and passes Merge for 'foo', so foo-1.0 remains visible alongside foo-2.0.
247+ let repoA = PackageIndex. fromList [foo1]
248+ repoB = PackageIndex. fromList [foo2] -- pretend all deprecated
249+ strategy name
250+ | name == mkPackageName " foo" = Merge -- fall back because all deprecated
251+ | otherwise = Override
252+ in allPkgs (PackageIndex. overrideOrMerge strategy repoA repoB)
253+ @?= List. sort [foo1, foo2]
254+ , testCase " package absent from second index: first index versions kept" $
255+ -- bar is only in repoFoo1bar1, not in repoFoo2; it survives regardless of strategy.
256+ allPkgs (PackageIndex. overrideOrMerge (const Override ) repoFoo1bar1 repoFoo2)
257+ @?= List. sort [foo2, bar1]
258+ , testCase " package absent from first index: second index versions appear" $
259+ allPkgs (PackageIndex. overrideOrMerge (const Override ) repoFoo1 repoBar1)
260+ @?= List. sort [foo1, bar1]
261+ , testCase " empty first index: second index fully visible" $
262+ allPkgs (PackageIndex. overrideOrMerge (const Override ) mempty repoFoo12)
263+ @?= List. sort [foo1, foo2]
264+ , testCase " empty second index: first index unchanged" $
265+ allPkgs (PackageIndex. overrideOrMerge (const Override ) repoFoo12 mempty )
266+ @?= List. sort [foo1, foo2]
267+ ]
268+
269+ allPkgs :: PackageIndex. PackageIndex PackageIdentifier -> [PackageIdentifier ]
270+ allPkgs = List. sort . PackageIndex. allPackages
271+
272+ -- ---------------------------------------------------------------------------
273+ -- deprecationAwareStrategy tests
274+ --
275+ -- Tests for the per-package Override/Merge decision used when applying a
276+ -- CombineStrategyOverride repo. The three cases are:
277+ -- 1. Package absent from preferred-versions -> Override
278+ -- 2. Package present, some versions preferred -> Override
279+ -- 3. Package present, no versions preferred -> Merge (all deprecated)
280+ -- ---------------------------------------------------------------------------
281+
282+ deprecationAwareStrategyTests :: TestTree
283+ deprecationAwareStrategyTests =
284+ testGroup
285+ " deprecationAwareStrategy"
286+ [ testCase " package absent from preferred-versions gives Override" $
287+ -- No entry for 'foo' in prefs, so the repo is not restricting it.
288+ strat repoFoo1 Map. empty fooName @?= Override
289+ , testCase " package present with matching versions gives Override" $
290+ -- foo-1.0 is in the index and satisfies ">= 1.0", so not all deprecated.
291+ strat repoFoo1 (prefs fooName (orLaterVersion v1)) fooName @?= Override
292+ , testCase " package present but no versions match gives Merge" $
293+ -- foo-1.0 is in the index but the pref ">= 2.0" excludes it: all deprecated.
294+ strat repoFoo1 (prefs fooName (orLaterVersion v2)) fooName @?= Merge
295+ , testCase " unrelated package in prefs does not affect result" $
296+ -- Prefs only mention 'bar'; 'foo' has no pref entry, so Override.
297+ strat repoFoo1 (prefs barName (orLaterVersion v1)) fooName @?= Override
298+ , testCase " package absent from index but in prefs gives Merge" $
299+ -- The pref entry exists but the index is empty, so lookupDependency
300+ -- returns [], meaning no preferred version exists.
301+ strat mempty (prefs fooName (orLaterVersion v2)) fooName @?= Merge
302+ , testCase " multiple packages decided independently" $
303+ -- foo is deprecated (pref excludes foo-1.0), bar is not (bar-1.0 satisfies >= 1.0).
304+ let p =
305+ Map. unionWith
306+ intersectVersionRanges
307+ (prefs fooName (orLaterVersion v2))
308+ (prefs barName (orLaterVersion v1))
309+ in do
310+ strat repoFoo1bar1 p fooName @?= Merge
311+ strat repoFoo1bar1 p barName @?= Override
312+ ]
313+ where
314+ strat = deprecationAwareStrategy
315+ fooName = mkPackageName " foo"
316+ barName = mkPackageName " bar"
317+ v1 = mkVersion [1 , 0 ]
318+ v2 = mkVersion [2 , 0 ]
319+ prefs name vr = Map. singleton name vr
320+
321+ -- ---------------------------------------------------------------------------
322+ -- addIndex tests
323+ --
324+ -- Tests for the top-level addIndex, which is the function used by
325+ -- getSourcePackagesAtIndexState to fold each repository's index into the
326+ -- accumulator. Unlike applyStrategy, addIndex consults preferred-versions
327+ -- for CombineStrategyOverride, falling back to merge when all versions of a
328+ -- package are deprecated. A regression to plain override would cause the
329+ -- "all deprecated" test to fail.
330+ -- ---------------------------------------------------------------------------
331+
332+ addIndexTests :: TestTree
333+ addIndexTests =
334+ testGroup
335+ " addIndex"
336+ [ testCase " Skip: index not added" $
337+ run [(repoFoo1, [] , CombineStrategySkip )]
338+ @?= []
339+ , testCase " Merge: index added" $
340+ run [(repoFoo1, [] , CombineStrategyMerge )]
341+ @?= [foo1]
342+ , testCase " Override with no prefs: behaves like plain override" $
343+ run
344+ [ (repoFoo12, [] , CombineStrategyMerge )
345+ , (repoFoo2, [] , CombineStrategyOverride )
346+ ]
347+ @?= [foo2]
348+ , testCase " Override with prefs matching some versions: still overrides" $
349+ -- foo-2.0 satisfies ">= 2.0", so not all deprecated; override applies.
350+ run
351+ [ (repoFoo12, [] , CombineStrategyMerge )
352+ , (repoFoo2, [dep fooName (orLaterVersion v2)], CombineStrategyOverride )
353+ ]
354+ @?= [foo2]
355+ , testCase " Override with all versions deprecated: falls back to merge" $
356+ -- foo-2.0 does not satisfy ">= 3.0", so all versions deprecated;
357+ -- override falls back to merge, keeping foo-1.0 from the first repo.
358+ run
359+ [ (repoFoo1, [] , CombineStrategyMerge )
360+ , (repoFoo2, [dep fooName (orLaterVersion v3)], CombineStrategyOverride )
361+ ]
362+ @?= List. sort [foo1, foo2]
363+ , testCase " Override: only the deprecated package falls back, others still override" $
364+ -- foo is all-deprecated in override repo → merge; bar has no prefs → override.
365+ run
366+ [ (repoFoo1bar1, [] , CombineStrategyMerge )
367+ ,
368+ ( PackageIndex. fromList [foo2, bar1]
369+ , [dep fooName (orLaterVersion v3)]
370+ , CombineStrategyOverride
371+ )
372+ ]
373+ @?= List. sort [foo1, foo2, bar1]
374+ ]
375+ where
376+ run = allPkgs . List. foldl' (\ acc (idx, ps, s) -> addIndex acc (idx, ps, s)) mempty
377+ fooName = mkPackageName " foo"
378+ v2 = mkVersion [2 , 0 ]
379+ v3 = mkVersion [3 , 0 ]
380+ dep name vr = Dependency name vr (NES. singleton LMainLibName )
0 commit comments