packages feed

tilia-0.0.1.0: tests/Tilia/Corpus.hs

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}

-- | Corpora of Haskell to run the formatter over.
module Tilia.Corpus
  ( -- * Corpora
    Corpus (..),
    Source (..),
    Reference (..),
    Expectations (..),
    Lists (..),
    vendoredExamples,
    ormoluExamples,
    ghcTestSuite,
    hackagePackages,

    -- * Obtaining one
    Example (..),
    obtain,
  )
where

import Codec.Archive.Tar qualified as Tar
import Codec.Compression.GZip qualified as GZip
import Control.Exception (SomeException, try)
import Control.Monad (forM)
import Data.ByteString.Lazy qualified as BL
import Data.List (isPrefixOf, isSuffixOf, sort, stripPrefix)
import Data.Maybe (fromMaybe, listToMaybe, mapMaybe, maybeToList)
import Data.Set qualified as Set
import Data.Text (Text)
import Data.Text qualified as T
import GHC.LanguageExtensions.Type (Extension)
import Network.HTTP.Client qualified as HTTP
import Network.HTTP.Req
import System.Directory
  ( XdgDirectory (..),
    createDirectoryIfMissing,
    doesDirectoryExist,
    doesFileExist,
    getXdgDirectory,
    listDirectory,
    removePathForcibly,
    renameDirectory,
    renameFile,
  )
import System.Environment (lookupEnv)
import System.FilePath (splitDirectories, takeDirectory, (</>))
import Tilia.Package (newPackageReader)

----------------------------------------------------------------------------
-- Corpora

-- | Whether a corpus says what the formatted result should look like.
data Reference
  = -- | It does not, so only the properties that hold of any input can be
    -- checked.
    NoReference
  | -- | It does, in a file whose name is the input's with the given mark
    -- put before the extension.
    ReferenceMarked String
  deriving (Eq, Show)

-- | Where the examples of a corpus come from.
data Source
  = -- | Fetched from the network and unpacked into a cache, once per
    -- machine.
    Fetched (Url 'Https, Option 'Https) FilePath
  | -- | Hackage releases, each unpacked beside the others under one root.
    HackageReleases [String]
  | -- | Checked into this repository, so always at hand and never fetched.
    Vendored FilePath

-- | What a corpus says it expects of the formatter.
data Expectations
  = -- | Named by hand, here. Every example not named is expected to format.
    Listed Lists
  | -- | Recorded in a file, one line per example, holding what each of them
    -- does today.
    Recorded FilePath

-- | The exceptions a 'Listed' corpus makes, named in full.
data Lists = Lists
  { -- | Examples to leave alone, named relative to the root of the corpus.
    -- A name with no extension stands for a directory and takes everything
    -- under it.
    expectSkip :: [FilePath],
    -- | Examples the formatter is supposed to refuse.
    expectDeclined :: [FilePath]
  }

-- | Where a corpus comes from and what is in it.
data Corpus = Corpus
  { -- | Used for the cache directory and in test names.
    corpusName :: String,
    -- | Where its examples come from.
    corpusSource :: Source,
    -- | Whether the corpus says what the formatted result should look like.
    corpusReference :: Reference,
    -- | What it expects the formatter to make of them.
    corpusExpectations :: Expectations,
    -- | Are these modules of the package around them?
    --
    -- True for a corpus of releases, where a module is compiled with its
    -- package's @default-extensions@ and does not parse without them.
    corpusInPackages :: Bool
  }

-- | Our own examples.
vendoredExamples :: Corpus
vendoredExamples =
  Corpus
    { corpusName = "tilia",
      corpusSource = Vendored ("corpora" </> "vendored"),
      corpusReference = ReferenceMarked "-out",
      corpusExpectations =
        Listed
          Lists
            { expectSkip = [],
              expectDeclined =
                [ "other" </> "position-pragmas.hs",
                  "other" </> "cpp" </> "unbalanced.hs",
                  "other" </> "cpp" </> "define-in-a-quasiquote.hs"
                ]
            },
      corpusInPackages = False
    }

-- | Ormolu's examples.
ormoluExamples :: Corpus
ormoluExamples =
  Corpus
    { corpusName = "ormolu-0.9.0.0",
      corpusSource =
        Fetched
          ( https "hackage.haskell.org"
              /: "package"
              /: "ormolu-0.9.0.0"
              /: "ormolu-0.9.0.0.tar.gz",
            mempty
          )
          ("data" </> "examples"),
      corpusReference = ReferenceMarked "-out",
      corpusExpectations =
        Listed Lists {expectSkip = ormoluSkip, expectDeclined = []},
      corpusInPackages = False
    }

-- | GHC's test suite.
ghcTestSuite :: Corpus
ghcTestSuite =
  Corpus
    { corpusName = "ghc-9.10.1-testsuite",
      corpusSource =
        Fetched
          ( https "codeload.github.com"
              /: "ghc"
              /: "ghc"
              /: "tar.gz"
              /: "refs"
              /: "tags"
              /: "ghc-9.10.1-release",
            mempty
          )
          ("testsuite" </> "tests"),
      corpusReference = NoReference,
      corpusExpectations =
        Listed
          Lists
            { expectSkip =
                ["perf" </> "compiler" </> "parsing001.hs"] <> ghcUnreadable,
              expectDeclined = ghcDeclined
            },
      corpusInPackages = False
    }

-- | GHC test suite files the formatter is right to refuse.
ghcDeclined :: [FilePath]
ghcDeclined =
  [ "ghci.debugger" </> "HappyTest.hs",
    "parser" </> "should_compile" </> "ColumnPragma.hs",
    "parser" </> "should_compile" </> "T7118.hs",
    "perf" </> "compiler" </> "T20261.hs",
    "perf" </> "compiler" </> "T5631.hs",
    "programs" </> "joao-circular" </> "Funcs_Parser_Lazy.hs",
    "quasiquotation" </> "T4150.hs"
  ]

-- | Packages from Hackage.
hackagePackages :: Corpus
hackagePackages =
  Corpus
    { corpusName = "hackage",
      corpusSource = HackageReleases hackageReleases,
      corpusReference = NoReference,
      corpusExpectations = Recorded ("corpora" </> "hackage" </> "hackage.manifest"),
      corpusInPackages = True
    }

hackageReleases :: [String]
hackageReleases =
  [ "Agda-2.8.0",
    "HUnit-1.6.2.0",
    "QuickCheck-2.18.0.0",
    "ShellCheck-0.11.0",
    "adjunctions-4.4.4",
    "aeson-2.3.1.0",
    "ansi-terminal-1.1.5",
    "async-2.2.6",
    "attoparsec-0.14.4",
    "aws-0.25.3",
    "base64-bytestring-1.2.1.0",
    "bifunctors-5.6.3",
    "blaze-html-0.9.2.0",
    "blaze-markup-0.8.3.0",
    "brick-2.13",
    "brittany-0.14.0.2",
    "capability-0.5.0.1",
    "cassava-0.5.5.0",
    "comonad-5.0.10",
    "conduit-1.3.6.1",
    "contravariant-1.5.6",
    "criterion-1.6.5.0",
    "cryptonite-0.30",
    "diagrams-core-1.5.1.2",
    "distributed-process-0.7.8",
    "dlist-1.0",
    "esqueleto-3.6.0.3",
    "exceptions-0.10.12",
    "fay-0.24.2.0",
    "free-5.2",
    "hakyll-4.17.0.0",
    "hashable-1.5.1.0",
    "haxl-2.5.1.1",
    "hedgehog-1.7",
    "hledger-1.52.1",
    "hlint-3.10",
    "hspec-core-2.11.17",
    "http-client-0.7.19",
    "http-types-0.12.6",
    "idris-1.3.4",
    "intero-0.1.40",
    "leksah-0.16.2.2",
    "lens-5.3.6",
    "megaparsec-9.8.1",
    "microlens-0.5.0.0",
    "mtl-2.3.2",
    "optics-0.4.2.1",
    "optparse-applicative-0.19.0.0",
    "pandoc-3.10.2",
    "pandoc-types-1.23.1.2",
    "parsec3-1.0.1.8",
    "parser-combinators-1.3.1",
    "persistent-2.18.1.0",
    "pipes-4.3.16",
    "postgrest-9.0.1",
    "profunctors-5.6.3",
    "purescript-0.15.15",
    "raaz-0.3.11",
    "random-1.3.1",
    "recursion-schemes-5.2.3",
    "resourcet-1.3.0",
    "retry-0.9.3.1",
    "safe-exceptions-0.1.7.4",
    "scientific-0.3.8.1",
    "scotty-0.30",
    "semigroupoids-6.0.2",
    "servant-0.20.3.0",
    "servant-server-0.20.3.0",
    "shake-0.19.9",
    "split-0.2.5",
    "stack-9.9.9",
    "statistics-0.16.5.0",
    "stm-2.5.3.1",
    "swagger2-2.9.1",
    "tasty-1.5.4",
    "tensorflow-0.2.0.1",
    "text-2.1.4",
    "th-abstraction-0.7.2.0",
    "time-1.16.0.1",
    "tls-2.4.3",
    "transformers-0.6.3.0",
    "typed-process-0.2.13.0",
    "unliftio-0.2.25.1",
    "unordered-containers-0.2.21",
    "unpacked-containers-0",
    "uuid-types-1.0.6.1",
    "vector-0.13.2.0",
    "vector-algorithms-0.9.1.0",
    "wai-3.2.5",
    "warp-3.4.15",
    "xmonad-0.18.1",
    "yesod-core-1.7.0.0"
  ]

-- | Ormolu examples we do not format the way Ormolu does.
ormoluSkip :: [FilePath]
ormoluSkip =
  [ "other" </> "disabling",
    "declaration" </> "value" </> "function" </> "required-type-arguments-2.hs",
    "declaration" </> "data" </> "comment-in-empty-record.hs",
    "import" </> "comment-inside-empty-import-list.hs",
    "other" </> "comment-two-blocks.hs",
    "other" </> "comment-glued-together.hs",
    "other" </> "multiple-blank-line-comment.hs",
    "declaration" </> "type" </> "parens-comments.hs",
    "declaration" </> "value" </> "function" </> "parens-comments.hs",
    "import" </> "comments-inside-imports.hs",
    "import" </> "comment-between-merged-imports.hs",
    "declaration" </> "data" </> "with-comment.hs",
    "declaration" </> "data" </> "record-empty-haddock.hs",
    "other" </> "empty-haddock.hs",
    "declaration" </> "value" </> "function" </> "arrow" </> "proc-do-complex.hs",
    "declaration" </> "value" </> "function" </> "comprehension" </> "transform-multi-line2.hs",
    "declaration" </> "value" </> "function" </> "if-with-comment-next-to-keyword.hs",
    "declaration" </> "value" </> "function" </> "operator-comments-2.hs",
    "declaration" </> "value" </> "function" </> "record" </> "wildcard-comments-0.hs",
    "declaration" </> "value" </> "function" </> "record" </> "wildcard-comments-1.hs",
    "other" </> "pragma-comments-after.hs",
    "declaration" </> "value" </> "function" </> "infix" </> "esqueleto-0.hs",
    "declaration" </> "value" </> "function" </> "infix" </> "esqueleto-1.hs",
    "declaration" </> "class" </> "default-signatures.hs",
    "declaration" </> "type-families" </> "closed-type-family" </> "with-comments.hs",
    "declaration" </> "deriving" </> "singleline.hs",
    "declaration" </> "deriving" </> "multiline.hs",
    "declaration" </> "deriving" </> "overlapping.hs",
    "declaration" </> "warning" </> "warning-single-line.hs"
  ]
    <> ormoluUnreadable

-- | Ormolu examples GHC's own parser cannot read.
ormoluUnreadable :: [FilePath]
ormoluUnreadable =
  [ "declaration" </> "class" </> "type-operators3.hs",
    "declaration" </> "data" </> "datatype-contexts.hs",
    "declaration" </> "foreign" </> "foreign-import-multiline.hs",
    "declaration" </> "value" </> "function" </> "application-1.hs",
    "declaration" </> "value" </> "function" </> "application-2.hs",
    "declaration" </> "value" </> "function" </> "arrow" </> "proc-cases.hs",
    "declaration" </> "value" </> "function" </> "arrow" </> "proc-do-simple1.hs",
    "declaration" </> "value" </> "function" </> "block-arguments.hs",
    "declaration" </> "value" </> "function" </> "case-empty.hs",
    "declaration" </> "value" </> "function" </> "do-single-line-lambda-case.hs",
    "declaration" </> "value" </> "function" </> "if-multi-line.hs",
    "declaration" </> "value" </> "function" </> "infix" </> "hanging.hs",
    "declaration" </> "value" </> "function" </> "let-multi-line.hs",
    "declaration" </> "value" </> "function" </> "let-single-line.hs",
    "declaration" </> "value" </> "function" </> "negation.hs",
    "declaration" </> "value" </> "function" </> "negative-literals.hs",
    "declaration" </> "value" </> "function" </> "pattern" </> "or-patterns.hs",
    "declaration" </> "value" </> "function" </> "type-applications.hs",
    "other" </> "comment-before-hanging.hs",
    "other" </> "cpp" </> "continuation.hs",
    "other" </> "cpp" </> "cpp-and-imports.hs",
    "other" </> "cpp" </> "lonely-hash.hs",
    "other" </> "cpp" </> "separation-0a.hs",
    "other" </> "cpp" </> "separation-0b.hs",
    "other" </> "cpp" </> "separation-1a.hs",
    "other" </> "cpp" </> "separation-1b.hs",
    "other" </> "cpp" </> "separation-2a.hs",
    "other" </> "cpp" </> "separation-2b.hs",
    "other" </> "cpp" </> "shifted.hs",
    "other" </> "cpp" </> "simple-import.hs",
    "other" </> "necessary-brackets.hs"
  ]

-- | GHC test suite files GHC's own parser cannot read.
ghcUnreadable :: [FilePath]
ghcUnreadable =
  [ "cabal" </> "sigcabal01" </> "p" </> "Map.hsig",
    "driver" </> "dynamicToo" </> "dynamicToo005" </> "A005.hsig",
    "annotations" </> "should_fail" </> "T19374b.hs",
    "annotations" </> "should_fail" </> "T19374c.hs",
    "annotations" </> "should_fail" </> "annfail13.hs",
    "arrows" </> "should_fail" </> "T2111.hs",
    "arrows" </> "should_fail" </> "arrowfail003.hs",
    "cabal" </> "fileStatus.hs",
    "codeGen" </> "should_run" </> "CheckBoundsOK.hs",
    "codeGen" </> "should_run" </> "T10245.hs",
    "codeGen" </> "should_run" </> "T12855.hs",
    "codeGen" </> "should_run" </> "T2080.hs",
    "codeGen" </> "should_run" </> "T7600.hs",
    "codeGen" </> "should_run" </> "cas_int.hs",
    "codeGen" </> "should_run" </> "cgrun044.hs",
    "codeGen" </> "should_run" </> "cgrun071.hs",
    "codeGen" </> "should_run" </> "cgrun072.hs",
    "codeGen" </> "should_run" </> "cgrun075.hs",
    "codeGen" </> "should_run" </> "cgrun076.hs",
    "codeGen" </> "should_run" </> "cgrun077.hs",
    "codeGen" </> "should_run" </> "cgrun079.hs",
    "codeGen" </> "should_run" </> "cgrun080.hs",
    "concurrent" </> "should_run" </> "T5611.hs",
    "concurrent" </> "should_run" </> "T5611a.hs",
    "concurrent" </> "should_run" </> "conc036.hs",
    "concurrent" </> "should_run" </> "conc037.hs",
    "concurrent" </> "should_run" </> "conc038.hs",
    "concurrent" </> "should_run" </> "foreignInterruptible.hs",
    "corelint" </> "T21115.hs",
    "deSugar" </> "should_run" </> "T5742.hs",
    "dependent" </> "should_fail" </> "RenamingStar.hs",
    "dmdanal" </> "should_compile" </> "T9208.hs",
    "driver" </> "FullGHCVersion.hs",
    "driver" </> "T10869.hs",
    "driver" </> "T10869A.hs",
    "driver" </> "T10970.hs",
    "driver" </> "T11763.hs",
    "driver" </> "T12135.hs",
    "driver" </> "T12674" </> "-T12674.hs",
    "driver" </> "T12752pass.hs",
    "driver" </> "T16167.hs",
    "driver" </> "T16476a.hs",
    "driver" </> "T16476b.hs",
    "driver" </> "T16521" </> "A.hs",
    "driver" </> "T17786.hs",
    "driver" </> "T2464.hs",
    "driver" </> "T3389.hs",
    "driver" </> "T8526" </> "A.hs",
    "driver" </> "bug1677" </> "Foo.hs",
    "driver" </> "multipleHomeUnits" </> "c-file" </> "C.hs",
    "driver" </> "multipleHomeUnits" </> "cpp-includes" </> "CPPIncludes.hs",
    "driver" </> "multipleHomeUnits" </> "cpp-includes" </> "CPPIncludes_Down.hs",
    "driver" </> "recomp011" </> "Main.hs",
    "driver" </> "recomp021" </> "A.hs",
    "driver" </> "should_fail" </> "T12752.hs",
    "eyeball" </> "inline2.hs",
    "ffi" </> "should_fail" </> "capi_wrapper.hs",
    "ffi" </> "should_fail" </> "ccall_value.hs",
    "ffi" </> "should_run" </> "T22159.hs",
    "gadt" </> "records-fail1.hs",
    "generics" </> "Uniplate" </> "GUniplate.hs",
    "ghci.debugger" </> "mdo.hs",
    "ghci.debugger" </> "scripts" </> "TupleN.hs",
    "ghci.debugger" </> "scripts" </> "break015.hs",
    "ghci.debugger" </> "scripts" </> "dynbrk005.hs",
    "ghci" </> "prog009" </> "A3.hs",
    "ghci" </> "prog013" </> "Bad.hs",
    "ghci" </> "scripts" </> "ghci022.hs",
    "ghci" </> "scripts" </> "ghci044a.hs",
    "ghci" </> "should_run" </> "PackedDataCon" </> "ByteCode.hs",
    "ghci" </> "should_run" </> "PackedDataCon" </> "Obj.hs",
    "ghci" </> "should_run" </> "UnboxedTuples" </> "ByteCode.hs",
    "ghci" </> "should_run" </> "UnboxedTuples" </> "Obj.hs",
    "ghci" </> "should_run" </> "UnliftedDataTypeInterp" </> "ByteCode.hs",
    "ghci" </> "should_run" </> "UnliftedDataTypeInterp" </> "Obj.hs",
    "haddock" </> "should_compile_flag_haddock" </> "haddockA004.hs",
    "haddock" </> "should_compile_flag_haddock" </> "haddockA011.hs",
    "haddock" </> "should_compile_flag_haddock" </> "haddockA041.hs",
    "haddock" </> "should_compile_noflag_haddock" </> "haddockC004.hs",
    "haddock" </> "should_compile_noflag_haddock" </> "haddockC011.hs",
    "haddock" </> "should_fail_flag_haddock" </> "haddockE003.hs",
    "hiefile" </> "should_compile" </> "CPP.hs",
    "hiefile" </> "should_compile" </> "T22416.hs",
    "hiefile" </> "should_compile" </> "hie002.hs",
    "indexed-types" </> "should_compile" </> "T12538.hs",
    "javascript" </> "T23346.hs",
    "lib" </> "integer" </> "IntegerConversionRules.hs",
    "linear" </> "should_fail" </> "LinearNoExt.hs",
    "linear" </> "should_fail" </> "LinearNoExtU.hs",
    "linear" </> "should_fail" </> "T20083.hs",
    "mdo" </> "should_compile" </> "mdo001.hs",
    "mdo" </> "should_compile" </> "mdo002.hs",
    "mdo" </> "should_compile" </> "mdo003.hs",
    "mdo" </> "should_compile" </> "mdo004.hs",
    "mdo" </> "should_compile" </> "mdo005.hs",
    "mdo" </> "should_compile" </> "mdo006.hs",
    "mdo" </> "should_fail" </> "mdofail001.hs",
    "mdo" </> "should_fail" </> "mdofail002.hs",
    "mdo" </> "should_fail" </> "mdofail003.hs",
    "mdo" </> "should_fail" </> "mdofail004.hs",
    "mdo" </> "should_fail" </> "mdofail005.hs",
    "mdo" </> "should_fail" </> "mdofail006.hs",
    "mdo" </> "should_run" </> "mdorun001.hs",
    "mdo" </> "should_run" </> "mdorun002.hs",
    "mdo" </> "should_run" </> "mdorun003.hs",
    "mdo" </> "should_run" </> "mdorun005.hs",
    "module" </> "Mod178_2.hs",
    "module" </> "T11432.hs",
    "module" </> "T11432a.hs",
    "module" </> "T12026.hs",
    "module" </> "mod183.hs",
    "module" </> "mod69.hs",
    "module" </> "mod70.hs",
    "module" </> "mod76.hs",
    "module" </> "mod89.hs",
    "module" </> "mod98.hs",
    "numeric" </> "should_run" </> "T12136.hs",
    "numeric" </> "should_run" </> "T20291.hs",
    "numeric" </> "should_run" </> "foundation.hs",
    "parser" </> "should_compile" </> "T10582.hs",
    "parser" </> "should_compile" </> "T15279.hs",
    "parser" </> "should_compile" </> "read023.hs",
    "parser" </> "should_compile" </> "read039.hs",
    "parser" </> "should_compile" </> "read046.hs",
    "parser" </> "should_compile" </> "read058.hs",
    "parser" </> "should_fail" </> "ExportCommaComma.hs",
    "parser" </> "should_fail" </> "InfixAppPatErr.hs",
    "parser" </> "should_fail" </> "NoBlockArgumentsFail.hs",
    "parser" </> "should_fail" </> "NoBlockArgumentsFail2.hs",
    "parser" </> "should_fail" </> "NoBlockArgumentsFail3.hs",
    "parser" </> "should_fail" </> "NoBlockArgumentsFailArrowCmds.hs",
    "parser" </> "should_fail" </> "NoDoAndIfThenElse.hs",
    "parser" </> "should_fail" </> "NoNumericUnderscores0.hs",
    "parser" </> "should_fail" </> "NoNumericUnderscores1.hs",
    "parser" </> "should_fail" </> "NoPatternSynonyms.hs",
    "parser" </> "should_fail" </> "OpaqueParseFail1.hs",
    "parser" </> "should_fail" </> "OpaqueParseFail2.hs",
    "parser" </> "should_fail" </> "OpaqueParseFail3.hs",
    "parser" </> "should_fail" </> "ParserNoLambdaCase.hs",
    "parser" </> "should_fail" </> "ParserNoMultiWayIf.hs",
    "parser" </> "should_fail" </> "ParserNoTH1.hs",
    "parser" </> "should_fail" </> "ParserNoTH2.hs",
    "parser" </> "should_fail" </> "RecordDotSyntaxFail0.hs",
    "parser" </> "should_fail" </> "RecordDotSyntaxFail1.hs",
    "parser" </> "should_fail" </> "RecordDotSyntaxFail2.hs",
    "parser" </> "should_fail" </> "RecordDotSyntaxFail3.hs",
    "parser" </> "should_fail" </> "RecordDotSyntaxFail4.hs",
    "parser" </> "should_fail" </> "RecordDotSyntaxFail6.hs",
    "parser" </> "should_fail" </> "RecordDotSyntaxFail7.hs",
    "parser" </> "should_fail" </> "SuffixAtFail.hs",
    "parser" </> "should_fail" </> "T10196Fail1.hs",
    "parser" </> "should_fail" </> "T10196Fail2.hs",
    "parser" </> "should_fail" </> "T10498a.hs",
    "parser" </> "should_fail" </> "T10498b.hs",
    "parser" </> "should_fail" </> "T12045d.hs",
    "parser" </> "should_fail" </> "T12051.hs",
    "parser" </> "should_fail" </> "T12429.hs",
    "parser" </> "should_fail" </> "T12610.hs",
    "parser" </> "should_fail" </> "T13260.hs",
    "parser" </> "should_fail" </> "T1344a.hs",
    "parser" </> "should_fail" </> "T1344b.hs",
    "parser" </> "should_fail" </> "T1344c.hs",
    "parser" </> "should_fail" </> "T13450.hs",
    "parser" </> "should_fail" </> "T13450TH.hs",
    "parser" </> "should_fail" </> "T15730.hs",
    "parser" </> "should_fail" </> "T15730b.hs",
    "parser" </> "should_fail" </> "T15849.hs",
    "parser" </> "should_fail" </> "T16270.hs",
    "parser" </> "should_fail" </> "T16270h.hs",
    "parser" </> "should_fail" </> "T16999.hs",
    "parser" </> "should_fail" </> "T17865.hs",
    "parser" </> "should_fail" </> "T17879a.hs",
    "parser" </> "should_fail" </> "T17879b.hs",
    "parser" </> "should_fail" </> "T18251a.hs",
    "parser" </> "should_fail" </> "T18251b.hs",
    "parser" </> "should_fail" </> "T18251f.hs",
    "parser" </> "should_fail" </> "T19504.hs",
    "parser" </> "should_fail" </> "T19928.hs",
    "parser" </> "should_fail" </> "T20609.hs",
    "parser" </> "should_fail" </> "T20609a.hs",
    "parser" </> "should_fail" </> "T20609b.hs",
    "parser" </> "should_fail" </> "T20609c.hs",
    "parser" </> "should_fail" </> "T20609d.hs",
    "parser" </> "should_fail" </> "T21843a.hs",
    "parser" </> "should_fail" </> "T21843b.hs",
    "parser" </> "should_fail" </> "T21843c.hs",
    "parser" </> "should_fail" </> "T21843d.hs",
    "parser" </> "should_fail" </> "T21843e.hs",
    "parser" </> "should_fail" </> "T21843f.hs",
    "parser" </> "should_fail" </> "T22070.hs",
    "parser" </> "should_fail" </> "T3095.hs",
    "parser" </> "should_fail" </> "T3153.hs",
    "parser" </> "should_fail" </> "T3751.hs",
    "parser" </> "should_fail" </> "T3811.hs",
    "parser" </> "should_fail" </> "T3811b.hs",
    "parser" </> "should_fail" </> "T3811d.hs",
    "parser" </> "should_fail" </> "T3811e.hs",
    "parser" </> "should_fail" </> "T3811f.hs",
    "parser" </> "should_fail" </> "T5425.hs",
    "parser" </> "should_fail" </> "T8431.hs",
    "parser" </> "should_fail" </> "T8501a.hs",
    "parser" </> "should_fail" </> "T8501b.hs",
    "parser" </> "should_fail" </> "T8506.hs",
    "parser" </> "should_fail" </> "T9225.hs",
    "parser" </> "should_fail" </> "T984.hs",
    "parser" </> "should_fail" </> "cmdFail001.hs",
    "parser" </> "should_fail" </> "cmdFail002.hs",
    "parser" </> "should_fail" </> "cmdFail003.hs",
    "parser" </> "should_fail" </> "cmdFail004.hs",
    "parser" </> "should_fail" </> "cmdFail005.hs",
    "parser" </> "should_fail" </> "cmdFail006.hs",
    "parser" </> "should_fail" </> "cmdFail007.hs",
    "parser" </> "should_fail" </> "cmdFail008.hs",
    "parser" </> "should_fail" </> "cmdFail009.hs",
    "parser" </> "should_fail" </> "patFail001.hs",
    "parser" </> "should_fail" </> "patFail002.hs",
    "parser" </> "should_fail" </> "patFail003.hs",
    "parser" </> "should_fail" </> "patFail004.hs",
    "parser" </> "should_fail" </> "patFail005.hs",
    "parser" </> "should_fail" </> "patFail006.hs",
    "parser" </> "should_fail" </> "patFail007.hs",
    "parser" </> "should_fail" </> "patFail008.hs",
    "parser" </> "should_fail" </> "patFail009.hs",
    "parser" </> "should_fail" </> "position001.hs",
    "parser" </> "should_fail" </> "position002.hs",
    "parser" </> "should_fail" </> "proposal-229c.hs",
    "parser" </> "should_fail" </> "readFail002.hs",
    "parser" </> "should_fail" </> "readFail004.hs",
    "parser" </> "should_fail" </> "readFail005.hs",
    "parser" </> "should_fail" </> "readFail006.hs",
    "parser" </> "should_fail" </> "readFail007.hs",
    "parser" </> "should_fail" </> "readFail009.hs",
    "parser" </> "should_fail" </> "readFail011.hs",
    "parser" </> "should_fail" </> "readFail012.hs",
    "parser" </> "should_fail" </> "readFail013.hs",
    "parser" </> "should_fail" </> "readFail014.hs",
    "parser" </> "should_fail" </> "readFail015.hs",
    "parser" </> "should_fail" </> "readFail017.hs",
    "parser" </> "should_fail" </> "readFail018.hs",
    "parser" </> "should_fail" </> "readFail019.hs",
    "parser" </> "should_fail" </> "readFail020.hs",
    "parser" </> "should_fail" </> "readFail022.hs",
    "parser" </> "should_fail" </> "readFail024.hs",
    "parser" </> "should_fail" </> "readFail025.hs",
    "parser" </> "should_fail" </> "readFail026.hs",
    "parser" </> "should_fail" </> "readFail027.hs",
    "parser" </> "should_fail" </> "readFail031.hs",
    "parser" </> "should_fail" </> "readFail033.hs",
    "parser" </> "should_fail" </> "readFail034.hs",
    "parser" </> "should_fail" </> "readFail040.hs",
    "parser" </> "should_fail" </> "readFail047.hs",
    "parser" </> "should_fail" </> "readFailTraditionalRecords1.hs",
    "parser" </> "should_fail" </> "readFailTraditionalRecords2.hs",
    "parser" </> "should_fail" </> "readFailTraditionalRecords3.hs",
    "parser" </> "should_fail" </> "strictnessDataCon_A.hs",
    "parser" </> "should_fail" </> "strictnessDataCon_B.hs",
    "parser" </> "should_fail" </> "typeopsDataCon_A.hs",
    "parser" </> "should_fail" </> "typeopsDataCon_B.hs",
    "parser" </> "should_fail" </> "typeops_A.hs",
    "parser" </> "should_fail" </> "typeops_B.hs",
    "parser" </> "should_fail" </> "typeops_C.hs",
    "parser" </> "should_fail" </> "typeops_D.hs",
    "parser" </> "should_fail" </> "unpack_before_opr.hs",
    "parser" </> "should_fail" </> "unpack_empty_type.hs",
    "parser" </> "unicode" </> "T10907.hs",
    "parser" </> "unicode" </> "T1744.hs",
    "parser" </> "unicode" </> "T18158b.hs",
    "parser" </> "unicode" </> "T18225B.hs",
    "parser" </> "unicode" </> "utf8_001.hs",
    "parser" </> "unicode" </> "utf8_002.hs",
    "parser" </> "unicode" </> "utf8_003.hs",
    "parser" </> "unicode" </> "utf8_004.hs",
    "parser" </> "unicode" </> "utf8_005.hs",
    "parser" </> "unicode" </> "utf8_010.hs",
    "parser" </> "unicode" </> "utf8_011.hs",
    "parser" </> "unicode" </> "utf8_020.hs",
    "parser" </> "unicode" </> "utf8_021.hs",
    "parser" </> "unicode" </> "utf8_022.hs",
    "parser" </> "unicode" </> "utf8_023.hs",
    "partial-sigs" </> "should_compile" </> "T14217.hs",
    "patsyn" </> "should_fail" </> "T10426.hs",
    "patsyn" </> "should_fail" </> "export-syntax.hs",
    "patsyn" </> "should_fail" </> "import-syntax.hs",
    "perf" </> "compiler" </> "T12234.hs",
    "perf" </> "compiler" </> "T14683.hs",
    "perf" </> "compiler" </> "T18698" </> "T18698.hs",
    "perf" </> "should_run" </> "T13623.hs",
    "plugins" </> "T20803a.hs",
    "plugins" </> "plugin-recomp" </> "Common.hs",
    "primops" </> "should_run" </> "T4442.hs",
    "primops" </> "should_run" </> "UnalignedAddrPrimOps.hs",
    "printer" </> "Ppr010.hs",
    "printer" </> "Ppr027.hs",
    "profiling" </> "should_compile" </> "T19894" </> "Fold.hs",
    "profiling" </> "should_compile" </> "T19894" </> "Operations.hs",
    "profiling" </> "should_compile" </> "T19894" </> "Step.hs",
    "profiling" </> "should_compile" </> "T19894" </> "StreamD.hs",
    "profiling" </> "should_compile" </> "T19894" </> "StreamK.hs",
    "profiling" </> "should_compile" </> "T19894" </> "Unfold.hs",
    "profiling" </> "should_compile" </> "T19894" </> "inline.hs",
    "profiling" </> "should_fail" </> "T17916.hs",
    "profiling" </> "should_fail" </> "proffail001.hs",
    "programs" </> "barton-mangler-bug" </> "Bug.hs",
    "programs" </> "joao-circular" </> "Funcs_Lexer.hs",
    "programs" </> "joao-circular" </> "LrcPrelude.hs",
    "qualifieddo" </> "should_fail" </> "qdofail002.hs",
    "qualifieddo" </> "should_fail" </> "qdofail005.hs",
    "quasiquotation" </> "T5204.hs",
    "quotes" </> "T20893.hs",
    "quotes" </> "T3572.hs",
    "quotes" </> "T4056.hs",
    "quotes" </> "T4169.hs",
    "quotes" </> "T4170.hs",
    "quotes" </> "T8455.hs",
    "quotes" </> "T8759a.hs",
    "quotes" </> "T9824.hs",
    "quotes" </> "TH_abstractFamily.hs",
    "quotes" </> "TH_bracket1.hs",
    "quotes" </> "TH_bracket2.hs",
    "quotes" </> "TH_bracket3.hs",
    "quotes" </> "TH_ppr1.hs",
    "quotes" </> "TH_scope.hs",
    "quotes" </> "TH_spliceViewPat" </> "A.hs",
    "rename" </> "should_fail" </> "T12879.hs",
    "rename" </> "should_fail" </> "T14907a.hs",
    "rename" </> "should_fail" </> "T9032.hs",
    "rename" </> "should_fail" </> "T9437.hs",
    "rename" </> "should_fail" </> "rnfail016.hs",
    "rename" </> "should_fail" </> "rnfail016a.hs",
    "roles" </> "should_fail" </> "Roles7.hs",
    "rts" </> "T12497.hs",
    "rts" </> "linker" </> "T20494.hs",
    "rts" </> "linker" </> "T5435.hs",
    "rts" </> "stack002.hs",
    "runghc" </> "T6132.hs",
    "safeHaskell" </> "flags" </> "Flags01.hs",
    "safeHaskell" </> "safeLanguage" </> "SafeLang18.hs",
    "saks" </> "should_fail" </> "saks_fail007.hs",
    "saks" </> "should_fail" </> "saks_fail024.hs",
    "saks" </> "should_fail" </> "saks_fail025.hs",
    "simplCore" </> "T9646" </> "Main.hs",
    "simplCore" </> "T9646" </> "StrictPrim.hs",
    "simplCore" </> "T9646" </> "Type.hs",
    "simplCore" </> "should_compile" </> "T13658.hs",
    "simplCore" </> "should_compile" </> "T21694.hs",
    "simplCore" </> "should_compile" </> "T8832.hs",
    "simplCore" </> "should_run" </> "T21575.hs",
    "stage1" </> "T2632.hs",
    "th" </> "T10279.hs",
    "th" </> "T10638.hs",
    "th" </> "T10819.hs",
    "th" </> "T10891.hs",
    "th" </> "T11484.hs",
    "th" </> "T16180.hs",
    "th" </> "T16326_TH.hs",
    "th" </> "T16980a.hs",
    "th" </> "T23309A.hs",
    "th" </> "T23378A.hs",
    "th" </> "T2817.hs",
    "th" </> "T3177.hs",
    "th" </> "T3177a.hs",
    "th" </> "T4436.hs",
    "th" </> "T5217.hs",
    "th" </> "T6018th.hs",
    "th" </> "T8807.hs",
    "th" </> "T9209.hs",
    "th" </> "TH_ExplicitForAllRules_a.hs",
    "th" </> "TH_class1.hs",
    "th" </> "TH_dataD1.hs",
    "th" </> "TH_foreignCallingConventions.hs",
    "th" </> "TH_implicitParams.hs",
    "th" </> "TH_lookupName.hs",
    "th" </> "TH_raiseErr1.hs",
    "th" </> "TH_recover.hs",
    "th" </> "TH_recursiveDo.hs",
    "th" </> "TH_recursiveDoImport.hs",
    "th" </> "TH_reifyDecl1.hs",
    "th" </> "TH_reifyDecl2.hs",
    "th" </> "TH_reifyExplicitForAllFams.hs",
    "th" </> "TH_reifyInstances.hs",
    "th" </> "TH_reifyLinear.hs",
    "th" </> "TH_reifyLocalDefs.hs",
    "th" </> "TH_reifyMkName.hs",
    "th" </> "TH_repE2.hs",
    "th" </> "TH_repGuard.hs",
    "th" </> "TH_repGuardOutput.hs",
    "th" </> "TH_repPatSig.hs",
    "th" </> "TH_repPatSigTVar.hs",
    "th" </> "TH_repPrim.hs",
    "th" </> "TH_repPrim2.hs",
    "th" </> "TH_repPrimOutput.hs",
    "th" </> "TH_repPrimOutput2.hs",
    "th" </> "TH_sections.hs",
    "th" </> "TH_spliceD2.hs",
    "th" </> "TH_spliceDecl1.hs",
    "th" </> "TH_spliceDecl2.hs",
    "th" </> "TH_spliceDecl3.hs",
    "th" </> "TH_spliceE1.hs",
    "th" </> "TH_spliceE3.hs",
    "th" </> "TH_spliceE4.hs",
    "th" </> "TH_spliceExpr1.hs",
    "th" </> "TH_spliceGuard.hs",
    "th" </> "TH_tf1.hs",
    "th" </> "TH_tf3.hs",
    "th" </> "TH_unresolvedInfix.hs",
    "th" </> "TH_unresolvedInfix2.hs",
    "typecheck" </> "should_compile" </> "FloatFDs.hs",
    "typecheck" </> "should_compile" </> "tc134.hs",
    "typecheck" </> "should_fail" </> "ExplicitSpecificity3.hs",
    "typecheck" </> "should_fail" </> "ExplicitSpecificity8.hs",
    "typecheck" </> "should_fail" </> "T13446.hs",
    "typecheck" </> "should_fail" </> "T14761b.hs",
    "typecheck" </> "should_fail" </> "T2126.hs",
    "typecheck" </> "should_fail" </> "T3102.hs",
    "typecheck" </> "should_fail" </> "T9634.hs",
    "typecheck" </> "should_fail" </> "tcfail089.hs",
    "typecheck" </> "should_run" </> "T1735.hs",
    "typecheck" </> "should_run" </> "T1735_Help" </> "Main.hs",
    "typecheck" </> "should_run" </> "T4809.hs",
    "unboxedsums" </> "UnboxedSumsTH_Fail.hs",
    "unboxedsums" </> "unboxedsums4.hs",
    "warnings" </> "should_fail" </> "CaretDiagnostics2.hs",
    "wcompat-warnings" </> "WCompatWarningsOff.hs",
    "wcompat-warnings" </> "WCompatWarningsOn.hs",
    "wcompat-warnings" </> "WCompatWarningsOnOff.hs"
  ]

----------------------------------------------------------------------------
-- Obtaining one

-- | One thing to format, and what it should come out as if that is known.
data Example = Example
  { -- | Where the corpus puts it, relative to the corpus root, which is
    -- what names the test: the absolute path runs through a cache directory
    -- that differs on every machine.
    exampleName :: FilePath,
    -- | The file to format, as an absolute path on this machine.
    exampleInput :: FilePath,
    -- | The file holding what the corpus says formatting should produce, if
    -- it says. 'Nothing' for a corpus that ships no expected outputs, and
    -- for an example within one that happens to have none.
    exampleReference :: Maybe FilePath,
    -- | What the package around it puts in force, already resolved from its
    -- @.cabal@ file. Empty for a corpus whose examples are not modules of a
    -- package; see 'corpusInPackages'.
    exampleExtensions :: [Extension]
  }
  deriving (Eq, Show)

-- | Get a corpus, fetching and unpacking it if this machine does not have
-- it yet.
--
-- Fetching happens once: an unpacked corpus is left in place and found
-- again, and a download interrupted half way leaves nothing behind to be
-- mistaken for a complete one. 'Left' is for the machine that cannot reach
-- the network rather than for a defect, and callers are expected to say so
-- and carry on rather than fail. A vendored corpus is already here and can
-- never fail this way.
obtain :: Corpus -> IO (Either Text [Example])
obtain corpus = case corpusSource corpus of
  Vendored dir -> Right <$> examplesIn corpus dir
  Fetched url root -> do
    home <- corpusCache
    let unpacked = home </> corpusName corpus
    createDirectoryIfMissing True home
    fetch url (home </> corpusName corpus <> ".tar.gz") unpacked >>= \case
      Left problem -> pure (Left problem)
      Right () -> Right <$> examplesIn corpus (unpacked </> root)
  HackageReleases releases -> do
    home <- corpusCache
    let root = home </> corpusName corpus
    createDirectoryIfMissing True root
    inTurn root releases >>= \case
      Left problem -> pure (Left problem)
      Right () -> Right <$> examplesIn corpus root
  where
    inTurn _ [] = pure (Right ())
    inTurn root (name : rest) =
      fetch (hackage name) (root </> name <> ".tar.gz") (root </> name) >>= \case
        Left problem -> pure (Left problem)
        Right () -> inTurn root rest

-- | Put an archive's contents where they are wanted, if they are not there.
--
-- Fetching happens once and unpacking happens once, and either step already
-- done is skipped.
fetch :: (Url 'Https, Option 'Https) -> FilePath -> FilePath -> IO (Either Text ())
fetch url archive unpacked =
  doesDirectoryExist unpacked >>= \case
    True -> pure (Right ())
    False -> do
      have <- doesFileExist archive
      got <- if have then pure (Right ()) else download url archive
      either (pure . Left) (const (unpackTo archive unpacked)) got

-- | Where Hackage keeps a release's sources.
hackage :: String -> (Url 'Https, Option 'Https)
hackage name =
  ( https "hackage.haskell.org"
      /: "package"
      /: T.pack name
      /: T.pack (name <> ".tar.gz"),
    mempty
  )

-- | Where corpora are kept.
--
-- Beside the fixity cache, and for the same reason: it is data about the
-- outside world that is expensive to obtain and cheap to keep.
corpusCache :: IO FilePath
corpusCache =
  lookupEnv "TILIA_CORPUS_DIR" >>= \case
    Just dir -> pure dir
    Nothing -> (</> "corpus") <$> getXdgDirectory XdgCache "tilia"

----------------------------------------------------------------------------
-- Fetching

-- | Fetch an archive.
--
-- Written to a temporary name and moved into place. Anything that leaves a
-- partial file under the real name would be taken for a complete download
-- on the next run and never fetched again.
--
-- 'Left' is for the machine that cannot reach the network rather than for a
-- defect, and callers are expected to say so and carry on. Only the fetch
-- is caught: a file that cannot be written is a fault worth hearing about.
download :: (Url 'Https, Option 'Https) -> FilePath -> IO (Either Text ())
download (url, query) dest =
  try get >>= \case
    Left (e :: HttpException) -> pure (Left (explain e))
    Right bytes
      | not (gzipped bytes) -> pure (Left "the answer was not an archive")
      | otherwise -> do
          BL.writeFile partial bytes
          Right <$> renameFile partial dest
  where
    partial = dest <> ".part"
    -- The two bytes every gzip stream opens with.
    gzipped = (== [0x1f, 0x8b]) . BL.unpack . BL.take 2
    get =
      runReq defaultHttpConfig $
        responseBody <$> req GET url NoReqBody lbsResponse query
    explain = \case
      VanillaHttpException (HTTP.HttpExceptionRequest _ reason) -> flatten reason
      other -> flatten other
    flatten :: (Show a) => a -> Text
    flatten = T.take 200 . T.unwords . T.words . T.pack . show

-- | Unpack the Haskell files of an archive, dropping its top-level
-- directory.
unpackTo :: FilePath -> FilePath -> IO (Either Text ())
unpackTo archive dest = do
  removePathForcibly staging
  outcome <- quietly (Left "could not unpack") $ do
    bytes <- BL.readFile archive
    Tar.foldEntries write (pure ()) (const (pure ())) (Tar.read (GZip.decompress bytes))
    pure (Right ())
  case outcome of
    Left problem -> do
      removePathForcibly staging
      pure (Left (problem <> " " <> T.pack archive))
    Right () -> do
      there <- doesDirectoryExist staging
      if there
        then Right <$> renameDirectory staging dest
        else pure (Left ("nothing to unpack in " <> T.pack archive))
  where
    staging = dest <> ".part"

    write entry rest = do
      case Tar.entryContent entry of
        Tar.NormalFile content _
          | Just path <- beneathTop (Tar.entryPath entry),
            any (`isSuffixOf` path) (".cabal" : haskellExtensions) -> do
              createDirectoryIfMissing True (takeDirectory (staging </> path))
              BL.writeFile (staging </> path) content
        _ -> pure ()
      rest
    beneathTop path = case splitDirectories path of
      (_ : rest@(_ : _)) | all safe rest -> Just (foldr1 (</>) rest)
      _ -> Nothing
    safe part = part /= ".." && not ("/" `isPrefixOf` part)

----------------------------------------------------------------------------
-- Enumerating

-- | Every example in an unpacked corpus, in a settled order.
examplesIn :: Corpus -> FilePath -> IO [Example]
examplesIn corpus root = do
  found <- sort <$> haskellFilesIn root
  reader <- packageReaderFor corpus
  let present = Set.fromList found
      files = filter (not . skipped) found
      example f reference = Example (nameOf f) f reference <$> reader f
  case corpusReference corpus of
    NoReference -> traverse (`example` Nothing) files
    ReferenceMarked mark ->
      forM (filter (not . answerTo mark present) files) $ \f ->
        if mark `isSuffixOf` stemOf f
          then example f (Just f)
          else do
            let reference = stemOf f <> mark <> extensionOf f
            there <- doesFileExist reference
            example f (if there then Just reference else Nothing)
  where
    nameOf f = fromMaybe f (stripPrefix (root <> "/") f)
    answerTo mark present f = case withoutSuffix mark (stemOf f) of
      Just stem -> Set.member (stem <> extensionOf f) present
      Nothing -> False
    skipped f = any listed (nameOf f : maybeToList (inputFor (nameOf f)))
    listed name = any covers skips
      where
        covers entry = entry == name || (entry <> "/") `isPrefixOf` name
    skips = case corpusExpectations corpus of
      Listed lists -> expectSkip lists
      Recorded _ -> []
    inputFor name = case corpusReference corpus of
      ReferenceMarked mark
        | Just stem <- withoutSuffix mark (stemOf name) ->
            Just (stem <> extensionOf name)
      _ -> Nothing
    withoutSuffix suffix name
      | suffix `isSuffixOf` name = Just (take (length name - length suffix) name)
      | otherwise = Nothing
    stemOf f =
      fromMaybe f (listToMaybe (mapMaybe (`withoutSuffix` f) haskellExtensions))
    extensionOf f =
      fromMaybe "" (listToMaybe (filter (`isSuffixOf` f) haskellExtensions))

-- | What each of a corpus's examples has in force before its own pragmas.
packageReaderFor :: Corpus -> IO (FilePath -> IO [Extension])
packageReaderFor corpus
  | not (corpusInPackages corpus) = pure (const (pure []))
  | otherwise = do
      reader <- newPackageReader
      pure (fmap (either (const []) id) . reader)

-- | The extensions an example may be written with.
haskellExtensions :: [String]
haskellExtensions = [".hs", ".hs-boot", ".hsig"]

haskellFilesIn :: FilePath -> IO [FilePath]
haskellFilesIn dir = do
  isDir <- doesDirectoryExist dir
  if not isDir
    then pure [dir | any (`isSuffixOf` dir) haskellExtensions]
    else do
      entries <- quietly [] (listDirectory dir)
      concat <$> traverse (haskellFilesIn . (dir </>)) entries

----------------------------------------------------------------------------
-- Helpers

quietly :: a -> IO a -> IO a
quietly fallback action =
  try action >>= \case
    Left (_ :: SomeException) -> pure fallback
    Right a -> pure a