Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
7e728ea
Add parseProjectConfig
philderbeast Jul 21, 2026
869b9f1
Update test to use parseProjectConfig
philderbeast Jul 21, 2026
ef0be71
Add more specific test names
philderbeast Jul 21, 2026
955ecf2
Clear provenance before comparison
philderbeast Jul 21, 2026
cf99865
Add boolean parsing for debug info
philderbeast Jul 22, 2026
188ade3
Add haddocks to default and enabled levels
philderbeast Jul 22, 2026
09f7bbf
Add parsing debug-info test
philderbeast Jul 22, 2026
1882dd7
WIP
philderbeast Jul 28, 2026
5196838
Add doctests for parsing legacy packages
philderbeast Jul 28, 2026
b200721
Add packages glob parser test
philderbeast Jul 28, 2026
52ae340
Add a glob test for optional packages
philderbeast Jul 28, 2026
fd302d9
Add a glob parser for package location
philderbeast Jul 28, 2026
f1c43e6
Avoid the use of Newtype
philderbeast Jul 28, 2026
8321eb9
For roundtripping, show delimited lines too
philderbeast Jul 30, 2026
0702a8e
Add doctests for project parsers
philderbeast Jul 30, 2026
f9e157a
Add empty string local parser test
philderbeast Jul 30, 2026
87004aa
Satisfy fix-whitespace
philderbeast Sep 23, 2026
4a64a3d
Satisfy fourmolu
philderbeast Sep 23, 2026
9c5e7b3
Use alternative for removed unpack'
philderbeast Sep 23, 2026
3d1713c
Grammar disallows parser setting in project
philderbeast Sep 23, 2026
740cf31
Expect negative for max-backjumps
philderbeast Sep 23, 2026
9ab3e34
Avoid empty and whitespace tokens
philderbeast Sep 23, 2026
6e6e520
Don't generate commas for single-value fields
philderbeast Sep 23, 2026
f339a2e
Document & test that legacy sets empty fields to ""
philderbeast Sep 23, 2026
b0c00de
Get rid of top level function only used by doctest
philderbeast Sep 23, 2026
dd421ea
Add a changelog entry
philderbeast Sep 23, 2026
823d16c
debug-info now passes
philderbeast Sep 24, 2026
2e3a0f6
Use normalise when reading
philderbeast Sep 24, 2026
65108a9
Add fileNoIndexURIPath, haddocks and doctests
philderbeast Sep 24, 2026
5cb58d6
Expect native local repo paths in the parser test
philderbeast Sep 24, 2026
89c7b8c
Say why local repo paths are stored native
philderbeast Sep 29, 2026
3c75feb
Spell out the uses of paths
philderbeast Sep 29, 2026
a277019
Assert both parsers agree in readConfig
philderbeast Sep 29, 2026
597d367
Show a tree diff when the parsers disagree
philderbeast Sep 29, 2026
94c31a1
Parse the round trip text with both parsers
philderbeast Sep 29, 2026
c5d2b9b
Allow max-backjumps=-1 in docs
philderbeast Sep 29, 2026
7bbe2f5
Add a test for parser selection as project field
philderbeast Sep 29, 2026
dea9543
Separate the .out files
philderbeast Sep 29, 2026
4e548ff
Add a warning about project-file-parser as field
philderbeast Sep 29, 2026
531cbb1
Mention the warning in the docs
philderbeast Sep 29, 2026
1769cc9
Follow hlint suggestion: redundant $
philderbeast Sep 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion Cabal-QuickCheck/src/Test/QuickCheck/Instances/Cabal.hs
Original file line number Diff line number Diff line change
Expand Up @@ -480,8 +480,10 @@ shortListOf1 bound gen = sized $ \n -> do
k <- choose (1, 1 `max` ((n `div` 2) `min` bound))
vectorOf k gen

-- | The parsec parser reads a single-value field as one token, stopping at a
-- comma, whereas the legacy parser took the rest of the line.
arbitraryShortToken :: Gen String
arbitraryShortToken = arbitraryShortStringWithout "{}[]"
arbitraryShortToken = arbitraryShortStringWithout "{}[],"

arbitraryShortPath :: Gen String
arbitraryShortPath = arbitraryShortStringWithout "{}[],<>:|*?" `suchThat` (not . winDevice)
Expand Down
6 changes: 6 additions & 0 deletions Cabal-syntax/src/Distribution/FieldGrammar/Newtypes.hs
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,12 @@ newtype List sep b a = List {_getList :: [a]}
--
-- >>> :t alaList' FSep Token
-- alaList' FSep Token :: [String] -> List FSep Token String
--
-- >>> _getList <$> (eitherParsec "foo bar foo" :: Either String (List FSep Token String))
-- Right ["foo","bar","foo"]
--
-- >>> _getList <$> (eitherParsec "xL{4,IE-,eK<}fE?e" :: Either String (List FSep Token String))
-- Right ["xL{4","IE-","eK<}fE?e"]
alaList :: sep -> [a] -> List sep (Identity a) a
alaList _ = List

Expand Down
23 changes: 21 additions & 2 deletions Cabal/src/Distribution/Simple/Compiler.hs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE ViewPatterns #-}

-- |
-- Module : Distribution.Simple.Compiler
Expand Down Expand Up @@ -357,9 +358,11 @@ intToOptimisationLevel i
-- levels. For compilers that do not the level is just capped to the
-- level they do support.
data DebugInfoLevel
= NoDebugInfo
= -- | The default and disabled level. Disabled by @--disable-debug-info@ or @debug-info: False@.
NoDebugInfo
| MinimalDebugInfo
| NormalDebugInfo
| -- | The enabled level when enabled by @--enable-debug-info@ or @debug-info: True@.
NormalDebugInfo
| MaximalDebugInfo
deriving (Bounded, Enum, Eq, Generic, Read, Show)

Expand All @@ -373,8 +376,24 @@ instance Parsec DebugInfoLevel where
parsecDebugInfoLevel :: CabalParsing m => m DebugInfoLevel
parsecDebugInfoLevel = flagToDebugInfoLevel . pure <$> parsecToken

-- | Converts a string to a 'DebugInfoLevel'. The string can be either a boolean
-- or 0-based integer for that enum. Evaluates to the default of 'NoDebugInfo'
-- when there's no string or when the string is "False". When the string is
-- "True", the level is 'NormalDebugInfo'. These string comparisons are
-- case-insensitive.
--
-- >>> [flagToDebugInfoLevel (Just $ show n) | n <- [0 .. 3]]
-- [NoDebugInfo,MinimalDebugInfo,NormalDebugInfo,MaximalDebugInfo]
--
-- >>> nub $ flagToDebugInfoLevel . Just <$> ["0", "False", "false"]
-- [NoDebugInfo]
--
-- >>> nub $ flagToDebugInfoLevel <$> Nothing : (Just <$> ["2", "True", "true"])
-- [NormalDebugInfo]
flagToDebugInfoLevel :: Maybe String -> DebugInfoLevel
flagToDebugInfoLevel Nothing = NormalDebugInfo
flagToDebugInfoLevel (Just (fmap toLower -> "false")) = NoDebugInfo
flagToDebugInfoLevel (Just (fmap toLower -> "true")) = NormalDebugInfo
flagToDebugInfoLevel (Just s) = case reads s of
[(i, "")]
| i >= fromEnum (minBound :: DebugInfoLevel)
Expand Down
5 changes: 4 additions & 1 deletion cabal-install/cabal-install.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -398,17 +398,20 @@ test-suite parser-tests

type: exitcode-stdio-1.0
main-is: Tests.hs
hs-source-dirs: parser-tests
hs-source-dirs: parser-tests tests
build-depends:
, cabal-install
, Cabal-tree-diff
, containers
, directory
, filepath
, network-uri >= 2.6.2.0 && <2.7
, tasty >= 1.2.3 && <1.6
, tasty-hunit >= 0.10
, tree-diff
other-modules:
Tests.ParserTests
UnitTests.Distribution.Client.TreeDiffInstances

-- Tests to run with a limited stack and heap size
-- The test suite name must be keep short cause a longer one
Expand Down
85 changes: 73 additions & 12 deletions cabal-install/parser-tests/Tests/ParserTests.hs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import Distribution.Client.Targets (readUserConstraint)
import Distribution.Client.Types.AllowNewer (AllowNewer (..), AllowOlder (..), RelaxDepMod (..), RelaxDepScope (..), RelaxDepSubject (..), RelaxDeps (..), RelaxedDep (..))
import Distribution.Client.Types.InstallMethod (InstallMethod (..))
import Distribution.Client.Types.OverwritePolicy (OverwritePolicy (..))
import Distribution.Client.Types.Repo (LocalRepo (..), RemoteRepo (..), asPosixPath)
import Distribution.Client.Types.Repo (LocalRepo (..), RemoteRepo (..))
import Distribution.Client.Types.RepoName (RepoName (..))
import Distribution.Client.Types.SourceRepo
import Distribution.Client.Types.WriteGhcEnvironmentFilesPolicy (WriteGhcEnvironmentFilesPolicy (..))
Expand All @@ -48,7 +48,6 @@ import Distribution.Solver.Types.Settings
, ReorderGoals (..)
, StrongFlags (..)
)
import Distribution.System (OS (..), buildOS)
import Distribution.Types.CondTree (CondTree (..))
import Distribution.Types.Flag (mkFlagAssignment)
import Distribution.Types.PackageId (PackageIdentifier (..))
Expand All @@ -62,27 +61,35 @@ import Distribution.Verbosity
import GHC.Stack (HasCallStack)
import Network.URI (parseURI)
import System.Directory (canonicalizePath, doesFileExist)
import System.FilePath ((</>))
import System.FilePath (normalise, (</>))
import Prelude ()

import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (Assertion, assertBool, assertEqual, testCase)
import Test.Tasty.HUnit (Assertion, assertBool, assertEqual, assertFailure, testCase)

import Data.TreeDiff (ToExpr, ansiWlEditExpr, ediff)
import UnitTests.Distribution.Client.TreeDiffInstances ()

parserTests :: TestTree
parserTests =
testGroup
"project files parsec tests"
[ testCase "read packages" testPackages
, testCase "read packages glob" testPackagesGlob
, testCase "read packages comma separated" testPackagesCommaSeparated
, testCase "read optional-packages" testOptionalPackages
, testCase "read optional-packages glob" testOptionalPackagesGlob
, testCase "read extra-packages" testExtraPackages
, testCase "read source-repository-package" testSourceRepoList
, testCase "read project-config-build-only" testProjectConfigBuildOnly
, testCase "read project-config-shared" testProjectConfigShared
, testCase "read install-dirs" testInstallDirs
, testCase "read remote-repos" testRemoteRepos
, testCase "read local-no-index-repos" testLocalNoIndexRepos
, testCase "read project-file-parser" testProjectFileParser
, testCase "set explicit provenance" testProjectConfigProvenance
, testCase "read project-config-local-packages" testProjectConfigLocalPackages
, testCase "read project-config-local-packages-empty-string" testProjectConfigLocalPackagesEmptyString
, testCase "read project-config-all-packages" testProjectConfigAllPackages
, testCase "read project-config-specific-packages" testProjectConfigSpecificPackages
, testCase "test projectConfigAllPackages concatenation" testAllPackagesConcat
Expand All @@ -98,16 +105,35 @@ parserTests =

testPackages :: Assertion
testPackages = do
let expected = [".", "packages/packages.cabal"]
let expected = [".", "packages/packages.cabal", "a", "b"]
(config, legacy) <- readConfigDefault "packages"
assertConfigEquals expected config legacy (projectPackages . snd . condTreeData)

testPackagesGlob :: Assertion
testPackagesGlob = do
let expected = ["*/*.cabal", "../{foo,bar}/"]
(config, legacy) <- readConfig "packages" "cabal.glob.project"
assertConfigEquals expected config legacy (projectPackages . snd . condTreeData)

testPackagesCommaSeparated :: Assertion
testPackagesCommaSeparated = do
let expected = ["xL{4,IE-,eK<}fE?e"]
-- let expected = ["xL{4","IE-","eK<}fE?e"]
(config, legacy) <- readConfig "packages" "cabal.comma-separated.project"
assertConfigEquals expected config legacy (projectPackages . snd . condTreeData)

testOptionalPackages :: Assertion
testOptionalPackages = do
let expected = [".", "packages/packages.cabal"]
(config, legacy) <- readConfigDefault "optional-packages"
assertConfigEquals expected config legacy (projectPackagesOptional . snd . condTreeData)

testOptionalPackagesGlob :: Assertion
testOptionalPackagesGlob = do
let expected = ["*/*.cabal", "../{foo,bar}/"]
(config, legacy) <- readConfig "optional-packages" "cabal.glob.project"
assertConfigEquals expected config legacy (projectPackagesOptional . snd . condTreeData)

testSourceRepoList :: Assertion
testSourceRepoList = do
(config, legacy) <- readConfigDefault "source-repository-packages"
Expand Down Expand Up @@ -298,26 +324,31 @@ testRemoteRepos = do
testLocalNoIndexRepos :: Assertion
testLocalNoIndexRepos = do
(config, legacy) <- readConfigDefault "local-no-index-repos"
let actualLocalRepos = (fromNubList . projectConfigLocalNoIndexRepos . projectConfigShared . snd . condTreeData) config
assertBool "Expected LocalNoIndexRepos do not match parsed values" $ compareLists expected actualLocalRepos compareLocalRepos
let localRepos = fromNubList . projectConfigLocalNoIndexRepos . projectConfigShared . snd . condTreeData
assertBool "Expected LocalNoIndexRepos do not match parsed values" $ compareLists expected (localRepos config) compareLocalRepos
assertBool "Expected LocalNoIndexRepos do not match legacy parsed values" $ compareLists expected (localRepos legacy) compareLocalRepos
assertConfigEquals mempty config legacy (projectConfigRemoteRepos . projectConfigShared . snd . condTreeData)
where
expected = [myRepository, mySecureRepository]
myRepository =
LocalRepo
{ localRepoName = RepoName "my-repository"
, localRepoPath = normalisePath "/absolute/path/to/directory"
, localRepoPath = normalise "/absolute/path/to/directory"
, localRepoSharedCache = False
}
mySecureRepository =
LocalRepo
{ localRepoName = RepoName "my-other-repository"
, localRepoPath = normalisePath "/another/path/to/repository"
, localRepoPath = normalise "/another/path/to/repository"
, localRepoSharedCache = False
}
normalisePath path = case buildOS of
Windows -> asPosixPath path
_ -> path

-- | The parser is chosen before the project file is read, so neither parser
-- takes this field from the file.
testProjectFileParser :: Assertion
testProjectFileParser = do
(config, legacy) <- readConfigDefault "project-file-parser"
assertConfigEquals NoFlag config legacy (projectConfigProjectFileParser . projectConfigShared . snd . condTreeData)

testProjectConfigProvenance :: Assertion
testProjectConfigProvenance = do
Expand Down Expand Up @@ -397,6 +428,18 @@ testProjectConfigLocalPackages = do
packageConfigTestTestOptions = [toPathTemplate "--some-option", toPathTemplate "42"]
packageConfigBenchmarkOptions = [toPathTemplate "--some-benchmark-option", toPathTemplate "--another-option"]

-- | The parsers differ on a field with an empty value. The legacy parser
-- passes the empty rest of the line to the option's reader and so sets the
-- field to the empty string. The parsec parser sees a field with no lines and
-- leaves it unset, which is the better behaviour.
testProjectConfigLocalPackagesEmptyString :: Assertion
testProjectConfigLocalPackagesEmptyString = do
(config, legacy) <- readConfigDiverging "project-config-local-packages" "cabal.empty-string.project"
assertEqual "Legacy parser sets the empty string" (toFlag (toPathTemplate "")) (field legacy)
assertEqual "Parsec parser leaves the field unset" NoFlag (field config)
where
field = packageConfigTestHumanLog . projectConfigLocalPackages . snd . condTreeData

testProjectConfigAllPackages :: Assertion
testProjectConfigAllPackages = do
(config, legacy) <- readConfigDefault "project-config-all-packages"
Expand Down Expand Up @@ -563,8 +606,26 @@ verbosity = mkVerbosity defaultVerbosityHandles normal
readConfigDefault :: FilePath -> IO (ProjectConfigSkeleton, ProjectConfigSkeleton)
readConfigDefault testSubDir = readConfig testSubDir "cabal.project"

-- | Reads a project file with both parsers and, with the legacy parser as the
-- oracle, checks that the parsec parser agrees with it on the whole config
-- before the caller looks at any one field.
readConfig :: FilePath -> FilePath -> IO (ProjectConfigSkeleton, ProjectConfigSkeleton)
readConfig testSubDir projectFileName = do
(parsec, legacy) <- readConfigDiverging testSubDir projectFileName
assertEdiffEqual "Parsec parser disagrees with the legacy parser" legacy parsec
return (parsec, legacy)

-- | Like 'assertEqual' but the failure shows a tree diff of the two values
-- instead of two 'show' dumps.
assertEdiffEqual :: (Eq a, ToExpr a, HasCallStack) => String -> a -> a -> Assertion
assertEdiffEqual msg expected actual =
unless (expected == actual) . assertFailure $
unlines [msg, show (ansiWlEditExpr (ediff expected actual))]

-- | Reads a project file with both parsers without checking that they agree,
-- for the fixtures where they are known to differ.
readConfigDiverging :: FilePath -> FilePath -> IO (ProjectConfigSkeleton, ProjectConfigSkeleton)
readConfigDiverging testSubDir projectFileName = do
(TestDir testRootFp projectConfigFp distDirLayout) <- testDirInfo testSubDir projectFileName
exists <- liftIO $ doesFileExist projectConfigFp
assertBool ("projectConfig does not exist: " <> projectConfigFp) exists
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-- The glob pattern syntax example from the users guide.
-- https://github.com/haskell/cabal/blob/d8c1b6f68df0a736c32a15ae85db9ffaf002ecb8/doc/cabal-project-description-file.rst?plain=1#L146
optional-packages: */*.cabal ../{foo,bar}/
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-- This example was generated for the property test "round trip packages" and
-- resulted in this reported difference:
-- projectPackages = [-"xL{4", -"IE-", -"eK<}fE?e", +"xL{4,IE-,eK<}fE?e"]
-- projectPackages = [-"7{u", -"{h", -"{=n}}}", +"7{u,{h,{=n}}}"]
packages: xL{4,IE-,eK<}fE?e
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-- The glob pattern syntax example from the users guide.
-- https://github.com/haskell/cabal/blob/d8c1b6f68df0a736c32a15ae85db9ffaf002ecb8/doc/cabal-project-description-file.rst?plain=1#L146
packages: */*.cabal ../{foo,bar}/
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
packages: . packages/packages.cabal
packages: a,b
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
test-log:
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
project-file-parser: legacy
9 changes: 6 additions & 3 deletions cabal-install/src/Distribution/Client/Config.hs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ import Distribution.Client.Types
, RemoteRepo (..)
, RepoName (..)
, emptyRemoteRepo
, fileNoIndexURIPath
, isRelaxDeps
, unRepoName
)
Expand Down Expand Up @@ -207,6 +208,7 @@ import Distribution.Simple.Utils
, writeFileAtomic
)
import Distribution.Solver.Types.ConstraintSource
import Distribution.System (buildOS)
import Distribution.Utils.Path (getSymbolicPath, unsafeMakeSymbolicPath)
import Distribution.Verbosity
( normal
Expand All @@ -228,8 +230,7 @@ import System.Directory
, renameFile
)
import System.FilePath
( normalise
, takeDirectory
( takeDirectory
, (</>)
)
import System.IO.Error
Expand Down Expand Up @@ -1689,7 +1690,9 @@ postProcessRepo lineno reponameStr repo0 = do
Left $
LocalRepo
reponame
(normalise (uriPath uri))
-- Native path, so a repository named here and in a project
-- file compares equal and shares one cache key.
(fileNoIndexURIPath buildOS uri)
(uriFragment uri == "#shared-cache")
_ -> do
let repo = repo0{remoteRepoName = reponame}
Expand Down
Loading
Loading