Skip to content

Use parsec parser for project round trip prop tests - #12139

Open
philderbeast wants to merge 41 commits into
haskell:masterfrom
cabalism:test/parsec-parser-roundtrip-prop
Open

philderbeast wants to merge 41 commits into
haskell:masterfrom
cabalism:test/parsec-parser-roundtrip-prop

Conversation

@philderbeast

@philderbeast philderbeast commented Jul 22, 2026 •

Copy link
Copy Markdown
Collaborator

I've started working on #12138. This changes the property tests for round trip printing and parsing of projects to parse with the parsec parser from #8889 as well as with the legacy parser. The legacy printer is still the one doing the printing and the legacy parser is the oracle: both parsers have to read back the generated config.

Adds parseProjectConfig and exports this from Distribution.Client.ProjectConfig.Parsec. This is used for testing and is the parsec equivalent of parseLegacyProjectConfig. It could be defined in the test suite but then we'd have to export fieldsToConfig and readPreprocessFields instead.

I'll squash commits before applying the merge label if this pull request is approved.

Fixes

Running the round trip against the parsec parser found these, each fixed and tested:

  • debug-info: True and debug-info: False weren't parsed, only the numeric levels were. The same conversion backs --enable-debug-info= so that's fixed too.
  • max-backjumps: -1 wasn't parsed. The option and the docs both allow a negative number for unlimited backtracking.
  • Package locations with commas inside braces, like packages: ../{foo,bar}/, were split at the commas.
  • Legacy accepted project-file-parser as a field, an accident of lifting all the command line options into fields. The parser is chosen before the file is read so the value could never have an effect. Both parsers now warn about it, parsec saying why, and there's a package test showing each.
  • The path of a file+noindex: repository kept the URI's forward slashes where the legacy parser normalises it. Only visible on Windows. Both parsers now read it with fileNoIndexURIPath, the inverse of normaliseFileNoIndexURI, with haddocks and doctests on both saying which direction each is for.

Kept differences

Two differences are kept and documented rather than fixed, as the parsec parser has the better behaviour:

  • An empty value, test-log: say, is Flag "" for legacy and unset for parsec.
  • Legacy takes the raw rest of the line for single-value fields. Parsec reads one token, stopping at a comma or space unless quoted. The legacy printer never quotes.

The generators avoid these inputs, with comments saying why, and the parser tests assert the differing results explicitly.

Tests

  • The round trip parses with both parsers and shows a tree diff for whichever disagrees with the generated config.
  • Reading a parser test fixture now asserts both parsers produce the same skeleton, again with a tree diff, before the test looks at any one field.
  • Doctests for the project parsers go through the lexer (in their -- $setup), so they show what a project file does rather than what a hand-built field does.

Follow-ups, stacked on this PR, compare both parsers over every project file in cabal-testsuite and turn +legacy-comparison back on in validate now that the legacy pass no longer repeats the import warnings.

Note

I started down this path by hand, switching the round trip to the parsec parser and chasing the first failures, the ones shown below. The rest was done with the help of Claude Fable 5.1: working through the remaining failures one commit at a time, the Windows local repository path, the doctests, and the parser test oracle.

Differences

These are the failures I found by hand at the start, before the fixes above.

There are test failures with commas in the packages field:

$ cabal run cabal-install:unit-tests -- -p "round trip packages"
...
Unit Tests
  UnitTests.Distribution.Client.ProjectConfig
    ProjectConfig printing/parsing round trip
      round trip packages: FAIL
        *** Failed! Falsified (after 8 tests and 5 shrinks):
        [PackageLocationString {getPackageLocationString = "Z%{w=g,MZ}"}]
        []
        []
        []
        shown:
        packages: Z%{w=g,MZ}
        
        ProjectConfig {
          projectPackages = [-"Z%{w=g", -"MZ}", +"Z%{w=g,MZ}"],
...
        Use --quickcheck-replay="(SMGen 14836990702317481085 1208827380667102593,7)" to reproduce.

There are test failures with commas in the shared test:

$ cabal run cabal-install:unit-tests -- -p "round trip shared"
...
Unit Tests
  UnitTests.Distribution.Client.ProjectConfig
    ProjectConfig printing/parsing round trip
      round trip shared: FAIL
        *** Failed! Falsified (after 2 tests and 26 shrinks):
        ProjectConfigShared {projectConfigDistDir = Last {getLast = Nothing}, projectConfigConfigFile = Last {getLast = Nothing}, projectConfigProjectDir = Last {getLast = Nothing}, projectConfigProjectFile = Last {getLast = Nothing}, projectConfigProjectFileParser = Last {getLast = Nothing}, projectConfigIgnoreProject = Last {getLast = Nothing}, projectConfigHcFlavor = Last {getLast = Nothing}, projectConfigHcPath = Last {getLast = Nothing}, projectConfigHcPkg = Last {getLast = Nothing}, projectConfigHaddockIndex = Last {getLast = Nothing}, projectConfigInstallDirs = InstallDirs {prefix = Last {getLast = Just "y"}, bindir = Last {getLast = Nothing}, libdir = Last {getLast = Nothing}, libsubdir = Last {getLast = Just "6"}, dynlibdir = Last {getLast = Just "a"}, bytecodelibdir = Last {getLast = Just "G"}, flibdir = Last {getLast = Nothing}, libexecdir = Last {getLast = Just "f"}, libexecsubdir = Last {getLast = Just "/"}, includedir = Last {getLast = Nothing}, datadir = Last {getLast = Just "r"}, datasubdir = Last {getLast = Just "?"}, docdir = Last {getLast = Just ","}, mandir = Last {getLast = Nothing}, htmldir = Last {getLast = Just "1"}, haddockdir = Last {getLast = Nothing}, sysconfdir = Last {getLast = Just "T"}}, projectConfigPackageDBs = [], projectConfigRemoteRepos = [], projectConfigLocalNoIndexRepos = [], projectConfigActiveRepos = Last {getLast = Nothing}, projectConfigIndexState = Last {getLast = Nothing}, projectConfigStoreDir = Last {getLast = Nothing}, projectConfigConstraints = [], projectConfigPreferences = [], projectConfigCabalVersion = Last {getLast = Nothing}, projectConfigSolver = Last {getLast = Nothing}, projectConfigAllowOlder = Nothing, projectConfigAllowNewer = Nothing, projectConfigWriteGhcEnvironmentFilesPolicy = Last {getLast = Nothing}, projectConfigMaxBackjumps = Last {getLast = Nothing}, projectConfigReorderGoals = Last {getLast = Nothing}, projectConfigCountConflicts = Last {getLast = Nothing}, projectConfigFineGrainedConflicts = Last {getLast = Nothing}, projectConfigMinimizeConflictSet = Last {getLast = Nothing}, projectConfigStrongFlags = Last {getLast = Nothing}, projectConfigAllowBootLibInstalls = Last {getLast = Nothing}, projectConfigOnlyConstrained = Last {getLast = Nothing}, projectConfigPerComponent = Last {getLast = Nothing}, projectConfigIndependentGoals = Last {getLast = Nothing}, projectConfigPreferOldest = Last {getLast = Nothing}, projectConfigProgPathExtra = [], projectConfigMultiRepl = Last {getLast = Nothing}}
        shown:
        prefix: y
        libsubdir: 6
        dynlibdir: a
        bytecodelibdir: G
        libexecdir: f
        libexecsubdir: /
        datadir: r
        datasubdir: ?
        docdir: ,
        htmldir: 1
        sysconfdir: T
        
        ERROR: (Nothing,PErrorWithSource {perrorSource = PUnknownSource, perror = PError {perrorPosition = Position 9 9, perrorMessage = "\nunexpected ','\nexpecting white space, string, identifier or end of input\n"}} :| [])
        Use --quickcheck-replay="(SMGen 4102444156980047325 2848404851119429861,1)" to reproduce.

There's a test failure in the local test parsing the debug level:

$ cabal run cabal-install:unit-tests -- -p "round trip local"
...
Unit Tests
  UnitTests.Distribution.Client.ProjectConfig
    ProjectConfig printing/parsing round trip
      round trip local: FAIL (0.01s)
        *** Failed! Exception: 'Can't parse debug info level False' (after 2 tests and 41 shrinks):
        PackageConfig {packageConfigProgramPaths = MapLast {getMapLast = fromList []}, packageConfigProgramArgs = MapMappend {getMapMappend = fromList []}, packageConfigProgramPathExtra = [], packageConfigFlagAssignment = fromList [], packageConfigVanillaLib = Last {getLast = Nothing}, packageConfigSharedLib = Last {getLast = Nothing}, packageConfigStaticLib = Last {getLast = Nothing}, packageConfigBytecodeLib = Last {getLast = Nothing}, packageConfigDynExe = Last {getLast = Nothing}, packageConfigFullyStaticExe = Last {getLast = Nothing}, packageConfigProf = Last {getLast = Nothing}, packageConfigProfLib = Last {getLast = Nothing}, packageConfigProfShared = Last {getLast = Nothing}, packageConfigProfExe = Last {getLast = Nothing}, packageConfigProfDetail = Last {getLast = Nothing}, packageConfigProfLibDetail = Last {getLast = Nothing}, packageConfigConfigureArgs = [], packageConfigOptimization = Last {getLast = Nothing}, packageConfigProgPrefix = Last {getLast = Nothing}, packageConfigProgSuffix = Last {getLast = Nothing}, packageConfigExtraLibDirs = [], packageConfigExtraLibDirsStatic = [], packageConfigExtraFrameworkDirs = [], packageConfigExtraIncludeDirs = [], packageConfigGHCiLib = Last {getLast = Nothing}, packageConfigSplitSections = Last {getLast = Nothing}, packageConfigSplitObjs = Last {getLast = Nothing}, packageConfigStripExes = Last {getLast = Nothing}, packageConfigStripLibs = Last {getLast = Nothing}, packageConfigTests = Last {getLast = Nothing}, packageConfigBenchmarks = Last {getLast = Nothing}, packageConfigCoverage = Last {getLast = Nothing}, packageConfigRelocatable = Last {getLast = Nothing}, packageConfigDebugInfo = Last {getLast = Just NoDebugInfo}, packageConfigDumpBuildInfo = Last {getLast = Nothing}, packageConfigRunTests = Last {getLast = Nothing}, packageConfigDocumentation = Last {getLast = Nothing}, packageConfigHaddockHoogle = Last {getLast = Nothing}, packageConfigHaddockHtml = Last {getLast = Nothing}, packageConfigHaddockHtmlLocation = Last {getLast = Nothing}, packageConfigHaddockForeignLibs = Last {getLast = Nothing}, packageConfigHaddockExecutables = Last {getLast = Nothing}, packageConfigHaddockTestSuites = Last {getLast = Nothing}, packageConfigHaddockBenchmarks = Last {getLast = Nothing}, packageConfigHaddockInternal = Last {getLast = Nothing}, packageConfigHaddockCss = Last {getLast = Nothing}, packageConfigHaddockLinkedSource = Last {getLast = Nothing}, packageConfigHaddockQuickJump = Last {getLast = Nothing}, packageConfigHaddockHscolourCss = Last {getLast = Nothing}, packageConfigHaddockContents = Last {getLast = Nothing}, packageConfigHaddockIndex = Last {getLast = Nothing}, packageConfigHaddockBaseUrl = Last {getLast = Nothing}, packageConfigHaddockResourcesDir = Last {getLast = Nothing}, packageConfigHaddockOutputDir = Last {getLast = Nothing}, packageConfigHaddockUseUnicode = Last {getLast = Nothing}, packageConfigHaddockForHackage = Last {getLast = Nothing}, packageConfigTestHumanLog = Last {getLast = Nothing}, packageConfigTestMachineLog = Last {getLast = Nothing}, packageConfigTestShowDetails = Last {getLast = Nothing}, packageConfigTestKeepTix = Last {getLast = Nothing}, packageConfigTestWrapper = Last {getLast = Nothing}, packageConfigTestFailWhenNoTestSuites = Last {getLast = Nothing}, packageConfigTestTestOptions = [], packageConfigBenchmarkOptions = []}
        shown:
        debug-info: False
        
        Exception thrown while showing test case: 'Can't parse debug info level False'
        Use --quickcheck-replay="(SMGen 3113468884534292331 13010642303015172695,1)" to reproduce.

There's a test failure in the specific test parsing the debug level (same goes for the all test):

$ cabal run cabal-install:unit-tests -- -p "round trip specific"
Warning: this is a debug build of cabal-install with assertions enabled.
Unit Tests
  UnitTests.Distribution.Client.ProjectConfig
    ProjectConfig printing/parsing round trip
      round trip specific: FAIL (0.02s)
        *** Failed! Exception: 'Can't parse debug info level False' (after 3 tests and 43 shrinks):
        fromList [(PackageName "I",NonMEmpty {getNonMEmpty = PackageConfig {packageConfigProgramPaths = MapLast {getMapLast = fromList []}, packageConfigProgramArgs = MapMappend {getMapMappend = fromList []}, packageConfigProgramPathExtra = [], packageConfigFlagAssignment = fromList [], packageConfigVanillaLib = Last {getLast = Nothing}, packageConfigSharedLib = Last {getLast = Nothing}, packageConfigStaticLib = Last {getLast = Nothing}, packageConfigBytecodeLib = Last {getLast = Nothing}, packageConfigDynExe = Last {getLast = Nothing}, packageConfigFullyStaticExe = Last {getLast = Nothing}, packageConfigProf = Last {getLast = Nothing}, packageConfigProfLib = Last {getLast = Nothing}, packageConfigProfShared = Last {getLast = Nothing}, packageConfigProfExe = Last {getLast = Nothing}, packageConfigProfDetail = Last {getLast = Nothing}, packageConfigProfLibDetail = Last {getLast = Nothing}, packageConfigConfigureArgs = [], packageConfigOptimization = Last {getLast = Nothing}, packageConfigProgPrefix = Last {getLast = Nothing}, packageConfigProgSuffix = Last {getLast = Nothing}, packageConfigExtraLibDirs = [], packageConfigExtraLibDirsStatic = [], packageConfigExtraFrameworkDirs = [], packageConfigExtraIncludeDirs = [], packageConfigGHCiLib = Last {getLast = Nothing}, packageConfigSplitSections = Last {getLast = Nothing}, packageConfigSplitObjs = Last {getLast = Nothing}, packageConfigStripExes = Last {getLast = Nothing}, packageConfigStripLibs = Last {getLast = Nothing}, packageConfigTests = Last {getLast = Nothing}, packageConfigBenchmarks = Last {getLast = Nothing}, packageConfigCoverage = Last {getLast = Nothing}, packageConfigRelocatable = Last {getLast = Nothing}, packageConfigDebugInfo = Last {getLast = Just NoDebugInfo}, packageConfigDumpBuildInfo = Last {getLast = Nothing}, packageConfigRunTests = Last {getLast = Nothing}, packageConfigDocumentation = Last {getLast = Nothing}, packageConfigHaddockHoogle = Last {getLast = Nothing}, packageConfigHaddockHtml = Last {getLast = Nothing}, packageConfigHaddockHtmlLocation = Last {getLast = Nothing}, packageConfigHaddockForeignLibs = Last {getLast = Nothing}, packageConfigHaddockExecutables = Last {getLast = Nothing}, packageConfigHaddockTestSuites = Last {getLast = Nothing}, packageConfigHaddockBenchmarks = Last {getLast = Nothing}, packageConfigHaddockInternal = Last {getLast = Nothing}, packageConfigHaddockCss = Last {getLast = Nothing}, packageConfigHaddockLinkedSource = Last {getLast = Nothing}, packageConfigHaddockQuickJump = Last {getLast = Nothing}, packageConfigHaddockHscolourCss = Last {getLast = Nothing}, packageConfigHaddockContents = Last {getLast = Nothing}, packageConfigHaddockIndex = Last {getLast = Nothing}, packageConfigHaddockBaseUrl = Last {getLast = Nothing}, packageConfigHaddockResourcesDir = Last {getLast = Nothing}, packageConfigHaddockOutputDir = Last {getLast = Nothing}, packageConfigHaddockUseUnicode = Last {getLast = Nothing}, packageConfigHaddockForHackage = Last {getLast = Nothing}, packageConfigTestHumanLog = Last {getLast = Nothing}, packageConfigTestMachineLog = Last {getLast = Nothing}, packageConfigTestShowDetails = Last {getLast = Nothing}, packageConfigTestKeepTix = Last {getLast = Nothing}, packageConfigTestWrapper = Last {getLast = Nothing}, packageConfigTestFailWhenNoTestSuites = Last {getLast = Nothing}, packageConfigTestTestOptions = [], packageConfigBenchmarkOptions = []}})]
        shown:
        
        package I
          debug-info: False
        
        Exception thrown while showing test case: 'Can't parse debug info level False'
        Use --quickcheck-replay="(SMGen 18355447672091957642 9032507782252479951,2)" to reproduce.

QA notes

With a project file containing any of the lines in the snippet below, cabal build --dry-run --project-file-parser=parsec used to fail to parse and now doesn't. With the default fallback parser it used to fall back to the legacy parser and now doesn't need to, which -v2 shows.

debug-info: True
max-backjumps: -1
packages: ../{foo,bar}/

On Windows, a repository section with a file+noindex:C:/some/path url now gives the same C:\some\path under both parsers, so --project-file-parser=compare accepts it.


@philderbeast
philderbeast force-pushed the test/parsec-parser-roundtrip-prop branch 2 times, most recently from 34a1357 to e3e2ebc Compare July 22, 2026 13:23
@philderbeast
philderbeast marked this pull request as draft July 23, 2026 18:02
@philderbeast
philderbeast force-pushed the test/parsec-parser-roundtrip-prop branch 4 times, most recently from d37626a to fbe026e Compare August 1, 2026 13:22
@philderbeast
philderbeast force-pushed the test/parsec-parser-roundtrip-prop branch from fbe026e to 7e7f319 Compare August 25, 2026 14:42
@philderbeast
philderbeast force-pushed the test/parsec-parser-roundtrip-prop branch from 7e7f319 to 14b3e12 Compare September 23, 2026 17:27
@philderbeast
philderbeast marked this pull request as ready for review September 29, 2026 14:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant