feat: lake test improvements & lake lint (#4261)

Extends the functionality of `lake test` and adds a parallel command in
`lake lint`.

* Rename `@[test_runner]` / `testRunner` to `@[test_driver]` /
`testDriver`. The old names are kept as deprecated aliases.
* Extend help page for `lake test` and adds one for `lake check-test`. 
* Add `lake lint` and its parallel tag `@[lint_driver]` , setting
`lintDriver`, and checker `lake check-lint`.
* Add support for specifying test / lint drivers from dependencies. 
* Add `testDriverArgs` / `lintDriverArgs` for fixing additional
arguments to the invocation of a driver script or executable.
* Add support for library test drivers (but not library lint drivers). 
* `lake check-test` / `lake check-lint` only load the package (without
dependencies), not the whole workspace.

Closes #4116. Closes #4121. Closes #4142.
This commit is contained in:
Mac Malone 2024-05-24 17:31:41 -04:00 committed by GitHub
parent d3ee0be908
commit 0448e3f4ea
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
44 changed files with 543 additions and 159 deletions

View file

@ -5,6 +5,7 @@ Authors: Mac Malone
-/
import Lake.Build.Run
import Lake.Build.Targets
import Lake.CLI.Build
namespace Lake
@ -28,14 +29,52 @@ def uploadRelease (pkg : Package) (tag : String) : LogIO Unit := do
logInfo s!"uploading {tag}/{pkg.buildArchive}"
proc {cmd := "gh", args}
def Package.test (pkg : Package) (args : List String := []) (buildConfig : BuildConfig := {}) : LakeT IO UInt32 := do
def Package.resolveDriver
(pkg : Package) (kind : String) (driver : String)
: LakeT IO (Package × String) := do
let pkgName := pkg.name.toString (escape := false)
if pkg.testRunner.isAnonymous then
error s!"{pkgName}: no test runner script or executable"
else if let some script := pkg.scripts.find? pkg.testRunner then
script.run args
else if let some exe := pkg.findLeanExe? pkg.testRunner then
let exeFile ← runBuild exe.fetch buildConfig
env exeFile.toString args.toArray
if driver.isEmpty then
error s!"{pkgName}: no {kind} driver configured"
else
error s!"{pkgName}: invalid test runner: unknown script or executable `{pkg.testRunner}`"
match driver.split (· == '/') with
| [pkg, driver] =>
let some pkg ← findPackage? pkg.toName
| error s!"{pkgName}: unknown {kind} driver package '{pkg}'"
return (pkg, driver)
| [driver] =>
return (pkg, driver)
| _ =>
error s!"{pkgName}: invalid {kind} driver '{driver}' (too many '/')"
def Package.test (pkg : Package) (args : List String := []) (buildConfig : BuildConfig := {}) : LakeT IO UInt32 := do
let cfgArgs := pkg.testDriverArgs
let (pkg, driver) ← pkg.resolveDriver "test" pkg.testDriver
let pkgName := pkg.name.toString (escape := false)
if let some script := pkg.scripts.find? driver.toName then
script.run (cfgArgs.toList ++ args)
else if let some exe := pkg.findLeanExe? driver.toName then
let exeFile ← runBuild exe.fetch buildConfig
env exeFile.toString (cfgArgs ++ args.toArray)
else if let some lib := pkg.findLeanLib? driver.toName then
unless cfgArgs.isEmpty ∧ args.isEmpty do
error s!"{pkgName}: arguments cannot be passed to a library test driver"
match resolveLibTarget (← getWorkspace) lib with
| .ok specs =>
runBuild (buildSpecs specs) {buildConfig with out := .stdout}
return 0
| .error e =>
error s!"{pkgName}: invalid test driver: {e}"
else
error s!"{pkgName}: invalid test driver: unknown script, executable, or library '{driver}'"
def Package.lint (pkg : Package) (args : List String := []) (buildConfig : BuildConfig := {}) : LakeT IO UInt32 := do
let cfgArgs := pkg.lintDriverArgs
let (pkg, driver) ← pkg.resolveDriver "lint" pkg.lintDriver
if let some script := pkg.scripts.find? driver.toName then
script.run (cfgArgs.data ++ args)
else if let some exe := pkg.findLeanExe? driver.toName then
let exeFile ← runBuild exe.fetch buildConfig
env exeFile.toString (cfgArgs ++ args.toArray)
else
let pkgName := pkg.name.toString (escape := false)
error s!"{pkgName}: invalid lint driver: unknown script or executable '{driver}'"

View file

@ -15,15 +15,18 @@ structure BuildSpec where
getBuildJob : BuildData info.key → BuildJob Unit
@[inline] def BuildData.toBuildJob
[FamilyOut BuildData k (BuildJob α)] (data : BuildData k) : BuildJob Unit :=
[FamilyOut BuildData k (BuildJob α)] (data : BuildData k)
: BuildJob Unit :=
discard <| ofFamily data
@[inline] def mkBuildSpec (info : BuildInfo)
[FamilyOut BuildData info.key (BuildJob α)] : BuildSpec :=
@[inline] def mkBuildSpec
(info : BuildInfo) [FamilyOut BuildData info.key (BuildJob α)]
: BuildSpec :=
{info, getBuildJob := BuildData.toBuildJob}
@[inline] def mkConfigBuildSpec (facetType : String)
(info : BuildInfo) (config : FacetConfig Fam ι facet) (h : BuildData info.key = Fam facet)
@[inline] def mkConfigBuildSpec
(facetType : String) (info : BuildInfo)
(config : FacetConfig Fam ι facet) (h : BuildData info.key = Fam facet)
: Except CliError BuildSpec := do
let some getJob := config.getJob?
| throw <| CliError.nonCliFacet facetType facet
@ -47,7 +50,9 @@ def parsePackageSpec (ws : Workspace) (spec : String) : Except CliError Package
| none => throw <| CliError.unknownPackage spec
open Module in
def resolveModuleTarget (ws : Workspace) (mod : Module) (facet : Name) : Except CliError BuildSpec :=
def resolveModuleTarget
(ws : Workspace) (mod : Module) (facet : Name := .anonymous)
: Except CliError BuildSpec :=
if facet.isAnonymous then
return mkBuildSpec <| mod.facet leanArtsFacet
else if let some config := ws.findModuleFacetConfig? facet then do
@ -55,7 +60,9 @@ def resolveModuleTarget (ws : Workspace) (mod : Module) (facet : Name) : Except
else
throw <| CliError.unknownFacet "module" facet
def resolveLibTarget (ws : Workspace) (lib : LeanLib) (facet : Name) : Except CliError (Array BuildSpec) :=
def resolveLibTarget
(ws : Workspace) (lib : LeanLib) (facet : Name := .anonymous)
: Except CliError (Array BuildSpec) :=
if facet.isAnonymous then
lib.defaultFacets.mapM (resolveFacet ·)
else

View file

@ -18,7 +18,10 @@ COMMANDS:
init <name> <temp> create a Lean package in the current directory
build <targets>... build targets
exe <exe> <args>... build an exe and run it in Lake's environment
test run the workspace's test script or executable
test test the package using the configured test driver
check-test check if there is a properly configured test driver
lint lint the package using the configured lint driver
check-lint check if there is a properly configured lint driver
clean remove build outputs
env <cmd> <args>... execute a command in Lake's environment
lean <file> elaborate a Lean file in Lake's context
@ -142,13 +145,62 @@ of the same package, the version materialized is undefined.
A bare `lake update` will upgrade all dependencies."
def helpTest :=
"Run the workspace's test script or executable
"Test the workspace's root package using its configured test driver
USAGE:
lake test [-- <args>...]
Looks for a script or executable tagged `@[test_runner]` in the workspace's
root package and executes it with `args`. "
A test driver can be configured by either setting the 'testDriver'
package configuration option or by tagging a script, executable, or library
`@[test_driver]`. A definition in a dependency can be used as a test driver
by using the `<pkg>/<name>` syntax for the 'testDriver' configuration option.
A script test driver will be run with the package configuration's
`testDriverArgs` plus the CLI `args`. An executable test driver will be
built and then run like a script. A library test driver will just be built.
"
def helpCheckTest :=
"Check if there is a properly configured test driver
USAGE:
lake check-test
Exits with code 0 if the workspace's root package has a properly
configured lint driver. Errors (with code 1) otherwise.
Does NOT verify that the configured test driver actually exists in the
package or its dependencies. It merely verifies that one is specified.
"
def helpLint :=
"Lint the workspace's root package using its configured lint driver
USAGE:
lake lint [-- <args>...]
A lint driver can be configured by either setting the `lintDriver` package
configuration option by tagging a script or executable `@[lint_driver]`.
A definition in dependency can be used as a test driver by using the
`<pkg>/<name>` syntax for the 'testDriver' configuration option.
A script lint driver will be run with the package configuration's
`lintDriverArgs` plus the CLI `args`. An executable lint driver will be
built and then run like a script.
"
def helpCheckLint :=
"Check if there is a properly configured lint driver
USAGE:
lake check-lint
Exits with code 0 if the workspace's root package has a properly
configured lint driver. Errors (with code 1) otherwise.
Does NOT verify that the configured lint driver actually exists in the
package or its dependencies. It merely verifies that one is specified.
"
def helpUpload :=
"Upload build artifacts to a GitHub release
@ -302,6 +354,9 @@ def help : (cmd : String) → String
| "update" | "upgrade" => helpUpdate
| "upload" => helpUpload
| "test" => helpTest
| "check-test" => helpCheckTest
| "lint" => helpLint
| "check-lint" => helpCheckLint
| "clean" => helpClean
| "script" => helpScriptCli
| "scripts" => helpScriptList

View file

@ -346,16 +346,28 @@ protected def setupFile : CliM PUnit := do
protected def test : CliM PUnit := do
processOptions lakeOption
let opts ← getThe LakeOptions
let config ← mkLoadConfig opts
let ws ← loadWorkspace config
let ws ← loadWorkspace (← mkLoadConfig opts)
noArgsRem do
let x := ws.root.test opts.subArgs (mkBuildConfig opts)
exit <| ← x.run (mkLakeContext ws)
protected def checkTest : CliM PUnit := do
processOptions lakeOption
let ws ← loadWorkspace ( ← mkLoadConfig (← getThe LakeOptions))
noArgsRem do exit <| if ws.root.testRunner.isAnonymous then 1 else 0
let pkg ← loadPackage (← mkLoadConfig (← getThe LakeOptions))
noArgsRem do exit <| if pkg.testDriver.isEmpty then 1 else 0
protected def lint : CliM PUnit := do
processOptions lakeOption
let opts ← getThe LakeOptions
let ws ← loadWorkspace (← mkLoadConfig opts)
noArgsRem do
let x := ws.root.lint opts.subArgs (mkBuildConfig opts)
exit <| ← x.run (mkLakeContext ws)
protected def checkLint : CliM PUnit := do
processOptions lakeOption
let pkg ← loadPackage (← mkLoadConfig (← getThe LakeOptions))
noArgsRem do exit <| if pkg.lintDriver.isEmpty then 1 else 0
protected def clean : CliM PUnit := do
processOptions lakeOption
@ -440,8 +452,7 @@ protected def translateConfig : CliM PUnit := do
let lang ← parseLangSpec (← takeArg "configuration language")
let outFile? := (← takeArg?).map FilePath.mk
noArgsRem do
Lean.searchPathRef.set cfg.lakeEnv.leanSearchPath
let (pkg, _) ← loadPackage "[root]" cfg
let pkg ← loadPackage cfg
let outFile := outFile?.getD <| pkg.configFile.withExtension lang.fileExtension
if (← outFile.pathExists) then
throw (.outputConfigExists outFile)
@ -468,6 +479,8 @@ def lakeCli : (cmd : String) → CliM PUnit
| "setup-file" => lake.setupFile
| "test" => lake.test
| "check-test" => lake.checkTest
| "lint" => lake.lint
| "check-lint" => lake.checkLint
| "clean" => lake.clean
| "script" => lake.script
| "scripts" => lake.script.list

View file

@ -89,8 +89,10 @@ def LeanConfig.addDeclFields (cfg : LeanConfig) (fs : Array DeclField) : Array D
@[inline] def mkDeclValWhere? (fields : Array DeclField) : Option (TSyntax ``declValWhere) :=
if fields.isEmpty then none else Unhygienic.run `(declValWhere|where $fields*)
def PackageConfig.mkSyntax (cfg : PackageConfig) : PackageDecl := Unhygienic.run do
let declVal? := mkDeclValWhere? <|Array.empty
def PackageConfig.mkSyntax (cfg : PackageConfig)
(testDriver := cfg.testDriver) (lintDriver := cfg.lintDriver)
: PackageDecl := Unhygienic.run do
let declVal? := mkDeclValWhere? <| Array.empty
|> addDeclFieldD `precompileModules cfg.precompileModules false
|> addDeclFieldD `moreGlobalServerArgs cfg.moreGlobalServerArgs #[]
|> addDeclFieldD `srcDir cfg.srcDir "."
@ -102,6 +104,10 @@ def PackageConfig.mkSyntax (cfg : PackageConfig) : PackageDecl := Unhygienic.run
|> addDeclField? `releaseRepo (cfg.releaseRepo <|> cfg.releaseRepo?)
|> addDeclFieldD `buildArchive (cfg.buildArchive?.getD cfg.buildArchive) (defaultBuildArchive cfg.name)
|> addDeclFieldD `preferReleaseBuild cfg.preferReleaseBuild false
|> addDeclFieldD `testDriver testDriver ""
|> addDeclFieldD `testDriverArgs cfg.testDriverArgs #[]
|> addDeclFieldD `lintDriver lintDriver ""
|> addDeclFieldD `lintDriverArgs cfg.lintDriverArgs #[]
|> cfg.toWorkspaceConfig.addDeclFields
|> cfg.toLeanConfig.addDeclFields
`(packageDecl|package $(mkIdent cfg.name) $[$declVal?]?)
@ -145,7 +151,7 @@ protected def LeanLibConfig.mkSyntax
`(leanLibDecl|$[$attrs?:attributes]? lean_lib $(mkIdent cfg.name) $[$declVal?]?)
protected def LeanExeConfig.mkSyntax
(cfg : LeanExeConfig) (defaultTarget := false) (testRunner := false)
(cfg : LeanExeConfig) (defaultTarget := false)
: LeanExeDecl := Unhygienic.run do
let declVal? := mkDeclValWhere? <| Array.empty
|> addDeclFieldD `srcDir cfg.srcDir "."
@ -153,11 +159,7 @@ protected def LeanExeConfig.mkSyntax
|> addDeclFieldD `exeName cfg.exeName (cfg.name.toStringWithSep "-" (escape := false))
|> addDeclFieldD `supportInterpreter cfg.supportInterpreter false
|> cfg.toLeanConfig.addDeclFields
let attrs? ← id do
let mut attrs := #[]
if testRunner then attrs := attrs.push <| ← `(Term.attrInstance|test_runner)
if defaultTarget then attrs := attrs.push <| ← `(Term.attrInstance|default_target)
if attrs.isEmpty then pure none else some <$> `(Term.attributes|@[$attrs,*])
let attrs? ← if defaultTarget then some <$> `(Term.attributes|@[default_target]) else pure none
`(leanExeDecl|$[$attrs?:attributes]? lean_exe $(mkIdent cfg.name) $[$declVal?]?)
protected def Dependency.mkSyntax (cfg : Dependency) : RequireDecl := Unhygienic.run do
@ -175,14 +177,13 @@ protected def Dependency.mkSyntax (cfg : Dependency) : RequireDecl := Unhygienic
/-- Create a Lean module that encodes the declarative configuration of the package. -/
def Package.mkLeanConfig (pkg : Package) : TSyntax ``module := Unhygienic.run do
let testRunner := pkg.testRunner
let defaultTargets := pkg.defaultTargets.foldl NameSet.insert NameSet.empty
let pkgConfig := pkg.config.mkSyntax
let pkgConfig := pkg.config.mkSyntax pkg.testDriver pkg.lintDriver
let requires := pkg.depConfigs.map (·.mkSyntax)
let leanLibs := pkg.leanLibConfigs.toArray.map fun cfg =>
cfg.mkSyntax (defaultTargets.contains cfg.name)
let leanExes := pkg.leanExeConfigs.toArray.map fun cfg =>
cfg.mkSyntax (defaultTargets.contains cfg.name) (cfg.name == testRunner)
cfg.mkSyntax (defaultTargets.contains cfg.name)
`(module|
import $(mkIdent `Lake)
open $(mkIdent `System) $(mkIdent `Lake) $(mkIdent `DSL)

View file

@ -122,7 +122,10 @@ instance : ToToml Dependency := ⟨(toToml ·.toToml)⟩
/-- Create a TOML table that encodes the declarative configuration of the package. -/
def Package.mkTomlConfig (pkg : Package) (t : Table := {}) : Table :=
pkg.config.toToml t
|>.insertD `testRunner pkg.testRunner .anonymous
|>.smartInsert `testDriver pkg.testDriver
|>.smartInsert `testDriverArgs pkg.testDriverArgs
|>.smartInsert `lintDriver pkg.lintDriver
|>.smartInsert `lintDriverArgs pkg.lintDriverArgs
|>.smartInsert `defaultTargets pkg.defaultTargets
|>.smartInsert `require pkg.depConfigs
|>.smartInsert `lean_lib pkg.leanLibConfigs.toArray

View file

@ -154,6 +154,44 @@ structure PackageConfig extends WorkspaceConfig, LeanConfig where
-/
preferReleaseBuild : Bool := false
/--
The name of the script, executable, or library by `lake test` when
this package is the workspace root. To point to a definition in another
package, use the syntax `<pkg>/<def>`.
A script driver will be run by `lake test` with the arguments
configured in `testDriverArgs` followed by any specified on the CLI
(e.g., via `lake lint -- <args>...`). An executable driver will be built
and then run like a script. A library will just be built.
-/
testDriver : String := ""
/--
Arguments to pass to the package's test driver.
These arguments will come before those passed on the command line via
`lake test -- <args>...`.
-/
testDriverArgs : Array String := #[]
/--
The name of the script or executable used by `lake lint` when this package
is the workspace root. To point to a definition in another package, use the
syntax `<pkg>/<def>`.
A script driver will be run by `lake lint` with the arguments
configured in `lintDriverArgs` followed by any specified on the CLI
(e.g., via `lake lint -- <args>...`). An executable driver will be built
and then run like a script.
-/
lintDriver : String := ""
/--
Arguments to pass to the package's linter.
These arguments will come before those passed on the command line via
`lake lint -- <args>...`.
-/
lintDriverArgs : Array String := #[]
deriving Inhabited
--------------------------------------------------------------------------------
@ -203,8 +241,11 @@ structure Package where
defaultScripts : Array Script := #[]
/-- Post-`lake update` hooks for the package. -/
postUpdateHooks : Array (OpaquePostUpdateHook config.name) := #[]
/-- Name of the package's test runner script or executable (if any). -/
testRunner : Name := .anonymous
/-- The driver used for `lake test` when this package is the workspace root. -/
testDriver : String := config.testDriver
/-- The driver used for `lake lint` when this package is the workspace root. -/
lintDriver : String := config.lintDriver
instance : Nonempty Package :=
have : Inhabited Environment := Classical.inhabited_of_nonempty inferInstance
@ -287,6 +328,14 @@ namespace Package
@[inline] def buildDir (self : Package) : FilePath :=
self.dir / self.config.buildDir
/-- The package's `testDriverArgs` configuration. -/
@[inline] def testDriverArgs (self : Package) : Array String :=
self.config.testDriverArgs
/-- The package's `lintDriverArgs` configuration. -/
@[inline] def lintDriverArgs (self : Package) : Array String :=
self.config.lintDriverArgs
/-- The package's `extraDepTargets` configuration. -/
@[inline] def extraDepTargets (self : Package) : Array Name :=
self.config.extraDepTargets

View file

@ -1,68 +1,22 @@
/-
Copyright (c) 2021 Mac Malone. All rights reserved.
Copyright (c) 2024 Mac Malone. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Mac Malone
-/
import Lake.Util.OrderedTagAttribute
import Lake.DSL.AttributesCore
open Lean
namespace Lake
initialize packageAttr : OrderedTagAttribute ←
registerOrderedTagAttribute `package "mark a definition as a Lake package configuration"
initialize packageDepAttr : OrderedTagAttribute ←
registerOrderedTagAttribute `package_dep "mark a definition as a Lake package dependency"
initialize postUpdateAttr : OrderedTagAttribute ←
registerOrderedTagAttribute `post_update "mark a definition as a Lake package post-update hook"
initialize scriptAttr : OrderedTagAttribute ←
registerOrderedTagAttribute `script "mark a definition as a Lake script"
initialize defaultScriptAttr : OrderedTagAttribute ←
registerOrderedTagAttribute `default_script "mark a Lake script as the package's default"
fun name => do
unless (← getEnv <&> (scriptAttr.hasTag · name)) do
throwError "attribute `default_script` can only be used on a `script`"
initialize leanLibAttr : OrderedTagAttribute ←
registerOrderedTagAttribute `lean_lib "mark a definition as a Lake Lean library target configuration"
initialize leanExeAttr : OrderedTagAttribute ←
registerOrderedTagAttribute `lean_exe "mark a definition as a Lake Lean executable target configuration"
initialize externLibAttr : OrderedTagAttribute ←
registerOrderedTagAttribute `extern_lib "mark a definition as a Lake external library target"
initialize targetAttr : OrderedTagAttribute ←
registerOrderedTagAttribute `target "mark a definition as a custom Lake target"
initialize defaultTargetAttr : OrderedTagAttribute ←
registerOrderedTagAttribute `default_target "mark a Lake target as the package's default"
fun name => do
let valid ← getEnv <&> fun env =>
leanLibAttr.hasTag env name ||
leanExeAttr.hasTag env name ||
externLibAttr.hasTag env name ||
targetAttr.hasTag env name
unless valid do
throwError "attribute `default_target` can only be used on a target (e.g., `lean_lib`, `lean_exe`)"
initialize testRunnerAttr : OrderedTagAttribute ←
registerOrderedTagAttribute `test_runner "mark a Lake script or executable as the package's test runner"
fun name => do
let valid ← getEnv <&> fun env =>
scriptAttr.hasTag env name ||
leanExeAttr.hasTag env name
unless valid do
throwError "attribute `test_runner` can only be used on a `script` or `lean_exe`"
initialize moduleFacetAttr : OrderedTagAttribute ←
registerOrderedTagAttribute `module_facet "mark a definition as a Lake module facet"
initialize packageFacetAttr : OrderedTagAttribute ←
registerOrderedTagAttribute `package_facet "mark a definition as a Lake package facet"
initialize libraryFacetAttr : OrderedTagAttribute ←
registerOrderedTagAttribute `library_facet "mark a definition as a Lake library facet"
initialize
registerBuiltinAttribute {
ref := by exact decl_name%
name := `test_runner
descr := testDriverAttr.attr.descr
add := fun decl stx attrKind => do
logWarningAt stx "@[test_runner] has been deprecated, use @[test_driver] instead"
testDriverAttr.attr.add decl stx attrKind
applicationTime := testDriverAttr.attr.applicationTime
erase := fun decl => testDriverAttr.attr.erase decl
}

View file

@ -0,0 +1,78 @@
/-
Copyright (c) 2021 Mac Malone. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Mac Malone
-/
import Lake.Util.OrderedTagAttribute
open Lean
namespace Lake
initialize packageAttr : OrderedTagAttribute ←
registerOrderedTagAttribute `package "mark a definition as a Lake package configuration"
initialize packageDepAttr : OrderedTagAttribute ←
registerOrderedTagAttribute `package_dep "mark a definition as a Lake package dependency"
initialize postUpdateAttr : OrderedTagAttribute ←
registerOrderedTagAttribute `post_update "mark a definition as a Lake package post-update hook"
initialize scriptAttr : OrderedTagAttribute ←
registerOrderedTagAttribute `script "mark a definition as a Lake script"
initialize defaultScriptAttr : OrderedTagAttribute ←
registerOrderedTagAttribute `default_script "mark a Lake script as the package's default"
fun name => do
unless (← getEnv <&> (scriptAttr.hasTag · name)) do
throwError "attribute `default_script` can only be used on a `script`"
initialize leanLibAttr : OrderedTagAttribute ←
registerOrderedTagAttribute `lean_lib "mark a definition as a Lake Lean library target configuration"
initialize leanExeAttr : OrderedTagAttribute ←
registerOrderedTagAttribute `lean_exe "mark a definition as a Lake Lean executable target configuration"
initialize externLibAttr : OrderedTagAttribute ←
registerOrderedTagAttribute `extern_lib "mark a definition as a Lake external library target"
initialize targetAttr : OrderedTagAttribute ←
registerOrderedTagAttribute `target "mark a definition as a custom Lake target"
initialize defaultTargetAttr : OrderedTagAttribute ←
registerOrderedTagAttribute `default_target "mark a Lake target as the package's default"
fun name => do
let valid ← getEnv <&> fun env =>
leanLibAttr.hasTag env name ||
leanExeAttr.hasTag env name ||
externLibAttr.hasTag env name ||
targetAttr.hasTag env name
unless valid do
throwError "attribute `default_target` can only be used on a target (e.g., `lean_lib`, `lean_exe`)"
initialize testDriverAttr : OrderedTagAttribute ←
registerOrderedTagAttribute `test_driver "mark a Lake script, executable, or library as package's test driver"
fun name => do
let valid ← getEnv <&> fun env =>
scriptAttr.hasTag env name ||
leanExeAttr.hasTag env name ||
leanLibAttr.hasTag env name
unless valid do
throwError "attribute `test_driver` can only be used on a `script`, `lean_exe`, or `lean_lib`"
initialize lintDriverAttr : OrderedTagAttribute ←
registerOrderedTagAttribute `lint_driver "mark a Lake script or executable as package's linter"
fun name => do
let valid ← getEnv <&> fun env =>
scriptAttr.hasTag env name ||
leanExeAttr.hasTag env name
unless valid do
throwError "attribute `lint_driver` can only be used on a `script` or `lean_exe`"
initialize moduleFacetAttr : OrderedTagAttribute ←
registerOrderedTagAttribute `module_facet "mark a definition as a Lake module facet"
initialize packageFacetAttr : OrderedTagAttribute ←
registerOrderedTagAttribute `package_facet "mark a definition as a Lake package facet"
initialize libraryFacetAttr : OrderedTagAttribute ←
registerOrderedTagAttribute `library_facet "mark a definition as a Lake library facet"

View file

@ -133,7 +133,8 @@ where
|>.insert ``externLibAttr
|>.insert ``targetAttr
|>.insert ``defaultTargetAttr
|>.insert ``testRunnerAttr
|>.insert ``testDriverAttr
|>.insert ``lintDriverAttr
|>.insert ``moduleFacetAttr
|>.insert ``packageFacetAttr
|>.insert ``libraryFacetAttr

View file

@ -53,7 +53,7 @@ def configFileExists (cfgFile : FilePath) : BaseIO Bool :=
leanFile.pathExists <||> tomlFile.pathExists
/-- Loads a Lake package configuration (either Lean or TOML). -/
def loadPackage
def loadPackageCore
(name : String) (cfg : LoadConfig)
: LogIO (Package × Option Environment) := do
if let some ext := cfg.relConfigFile.extension then
@ -88,7 +88,7 @@ def loadDepPackage
(dep : MaterializedDep) (leanOpts : Options) (reconfigure : Bool)
: StateT Workspace LogIO Package := fun ws => do
let name := dep.name.toString (escape := false)
let (pkg, env?) ← loadPackage name {
let (pkg, env?) ← loadPackageCore name {
lakeEnv := ws.lakeEnv
wsDir := ws.dir
relPkgDir := dep.relPkgDir
@ -110,7 +110,7 @@ Does not resolve dependencies.
-/
def loadWorkspaceRoot (config : LoadConfig) : LogIO Workspace := do
Lean.searchPathRef.set config.lakeEnv.leanSearchPath
let (root, env?) ← loadPackage "[root]" config
let (root, env?) ← loadPackageCore "[root]" config
let ws : Workspace := {
root, lakeEnv := config.lakeEnv
moduleFacetConfigs := initModuleFacetConfigs
@ -122,6 +122,11 @@ def loadWorkspaceRoot (config : LoadConfig) : LogIO Workspace := do
else
return ws
/-- Loads a Lake package as a single independent object (without dependencies). -/
def loadPackage (config : LoadConfig) : LogIO Package := do
Lean.searchPathRef.set config.lakeEnv.leanSearchPath
(·.1) <$> loadPackageCore "[root]" config
/-- Recursively visits a package dependency graph, avoiding cycles. -/
private def resolveDepsAcyclic
[Monad m] [MonadError m]

View file

@ -109,14 +109,28 @@ def Package.loadFromEnv
| .error e => error e
let depConfigs ← IO.ofExcept <| packageDepAttr.getAllEntries env |>.mapM fun name =>
evalConstCheck env opts Dependency ``Dependency name
let testRunners := testRunnerAttr.getAllEntries env
let testRunner ←
if testRunners.size > 1 then
error s!"{self.name}: only one script or executable can be tagged `@[test_runner]`"
else if h : testRunners.size > 0 then
pure (testRunners[0]'h)
let testDrivers := testDriverAttr.getAllEntries env
let testDriver ←
if testDrivers.size > 1 then
error s!"{self.name}: only one script, executable, or library can be tagged @[test_driver]"
else if h : testDrivers.size > 0 then
if self.config.testDriver.isEmpty then
pure (testDrivers[0]'h |>.toString)
else
error s!"{self.name}: cannot both set testDriver and use @[test_driver]"
else
pure .anonymous
pure self.config.testDriver
let lintDrivers := lintDriverAttr.getAllEntries env
let lintDriver ←
if lintDrivers.size > 1 then
error s!"{self.name}: only one script or executable can be tagged @[linter]"
else if h : lintDrivers.size > 0 then
if self.config.lintDriver.isEmpty then
pure (lintDrivers[0]'h |>.toString)
else
error s!"{self.name}: cannot both set linter and use @[linter]"
else
pure self.config.lintDriver
-- Deprecation warnings
unless self.config.manifestFile.isNone do
@ -127,8 +141,8 @@ def Package.loadFromEnv
-- Fill in the Package
return {self with
depConfigs, leanLibConfigs, leanExeConfigs, externLibConfigs
opaqueTargetConfigs, defaultTargets, scripts, defaultScripts, testRunner
postUpdateHooks
opaqueTargetConfigs, defaultTargets, scripts, defaultScripts
testDriver, lintDriver, postUpdateHooks
}
/--

View file

@ -175,12 +175,19 @@ protected def PackageConfig.decodeToml (t : Table) (ref := Syntax.missing) : Exc
let releaseRepo ← t.tryDecode? `releaseRepo
let buildArchive? ← t.tryDecode? `buildArchive
let preferReleaseBuild ← t.tryDecodeD `preferReleaseBuild false
let testRunner ← t.tryDecodeD `testRunner ""
let testDriver ← t.tryDecodeD `testDriver ""
let testDriver := if ¬testRunner.isEmpty ∧ testDriver.isEmpty then testRunner else testDriver
let testDriverArgs ← t.tryDecodeD `testDriverArgs #[]
let lintDriver ← t.tryDecodeD `lintDriver ""
let lintDriverArgs ← t.tryDecodeD `lintDriverArgs #[]
let toLeanConfig ← tryDecode <| LeanConfig.decodeToml t
let toWorkspaceConfig ← tryDecode <| WorkspaceConfig.decodeToml t
return {
name, precompileModules, moreGlobalServerArgs,
srcDir, buildDir, leanLibDir, nativeLibDir, binDir, irDir,
releaseRepo, buildArchive?, preferReleaseBuild
testDriver, testDriverArgs, lintDriver, lintDriverArgs
toLeanConfig, toWorkspaceConfig
}
@ -259,12 +266,11 @@ def loadTomlConfig (dir relDir relConfigFile : FilePath) : LogIO Package := do
let leanLibConfigs ← mkRBArray (·.name) <$> table.tryDecodeD `lean_lib #[]
let leanExeConfigs ← mkRBArray (·.name) <$> table.tryDecodeD `lean_exe #[]
let defaultTargets ← table.tryDecodeD `defaultTargets #[]
let testRunner ← table.tryDecodeD `testRunner .anonymous
let depConfigs ← table.tryDecodeD `require #[]
return {
dir, relDir, relConfigFile
config, depConfigs, leanLibConfigs, leanExeConfigs
defaultTargets, testRunner
defaultTargets
}
if errs.isEmpty then
return pkg

View file

@ -43,8 +43,6 @@ namespace Toml.Table
@[inline] nonrec def insert [enc : ToToml α] (k : Name) (v : α) (t : Table) : Table :=
t.insert k (enc.toToml v)
instance (priority := low) [ToToml α] : SmartInsert α := ⟨Table.insert⟩
/-- If the value is not `none`, inserts the encoded value into the table. -/
@[inline] nonrec def insertSome [enc : ToToml α] (k : Name) (v? : Option α) (t : Table) : Table :=
t.insertSome k (v?.map enc.toToml)
@ -61,6 +59,9 @@ instance : SmartInsert Table where
instance [ToToml (Array α)] : SmartInsert (Array α) where
smartInsert k v t := t.insertUnless v.isEmpty k (toToml v)
instance : SmartInsert String where
smartInsert k v t := t.insertUnless v.isEmpty k (toToml v)
/-- Insert a value into the table if `p` is `true`. -/
@[inline] nonrec def insertIf [enc : ToToml α] (p : Bool) (k : Name) (v : α) (t : Table) : Table :=
t.insertIf p k (enc.toToml v)

View file

@ -15,6 +15,7 @@ Each `lakefile.lean` includes a `package` declaration (akin to `main`) which def
* [Package Configuration Options](#package-configuration-options)
+ [Layout](#layout)
+ [Build & Run](#build--run)
+ [Test & Lint](#test--lint)
+ [Cloud Releases](#cloud-releases)
* [Defining Build Targets](#defining-build-targets)
+ [Lean Libraries](#lean-libraries)
@ -164,8 +165,10 @@ Lake provides a large assortment of configuration options for packages.
### Layout
These options control the top-level directory layout of the package and its build directory. Further paths specified by libraries, executables, and targets within the package are relative to these directories.
* `packagesDir`: The directory to which Lake should download remote dependencies. Defaults to `.lake/packages`.
* `srcDir`: The directory containing the package's Lean source files. Defaults to the package's directory. (This will be passed to `lean` as the `-R` option.)
* `srcDir`: The directory containing the package's Lean source files. Defaults to the package's directory.
* `buildDir`: The directory to which Lake should output the package's build results. Defaults to `build`.
* `leanLibDir`: The build subdirectory to which Lake should output the package's binary Lean libraries (e.g., `.olean`, `.ilean` files). Defaults to `lib`.
* `nativeLibDir`: The build subdirectory to which Lake should output the package's native libraries (e.g., `.a`, `.so`, `.dll` files). Defaults to `lib`.
@ -174,6 +177,8 @@ Lake provides a large assortment of configuration options for packages.
### Build & Run
These options configure how code is built and run in the package. Libraries, executables, and other targets within a package can further add to parts of this configuration.
* `platformIndependent`: Asserts whether Lake should assume Lean modules are platform-independent. That is, whether lake should include the platform and platform-dependent elements in a module's trace. See the docstring of `Lake.LeanConfig.platformIndependent` for more details. Defaults to `none`.
* `precompileModules`: Whether to compile each module into a native shared library that is loaded whenever the module is imported. This speeds up the evaluation of metaprograms and enables the interpreter to run functions marked `@[extern]`. Defaults to `false`.
* `moreServerOptions`: An `Array` of additional options to pass to the Lean language server (i.e., `lean --server`) launched by `lake serve`.
@ -188,8 +193,21 @@ Lake provides a large assortment of configuration options for packages.
* `weakLinkArgs`: An `Array` of additional arguments to pass to `leanc` when linking (e.g., binary executables or shared libraries) Unlike `moreLinkArgs`, these arguments do not affect the trace of the build result, so they can be changed without triggering a rebuild. They come *before* `moreLinkArgs`.
* `extraDepTargets`: An `Array` of [target](#custom-targets) names that the package should always build before anything else.
### Test & Lint
The CLI commands `lake test` and `lake lint` use definitions configured by the workspace's root package to perform testing and linting (this referred to as the test or lint *driver*). In Lean configuration files, these can be specified by applying the `@[test_driver]` or `@[lint_driver]` to a `script`, `lean_exe`, or `lean_lb`. They can also be configured (in Lean or TOML format) via the following options on the package.
* `testDriver`: The name of the script, executable, or library to drive `lake test`.
* `testDriverArgs`: An `Array` of arguments to pass to the package's test driver.
* `lintDriver`: The name of the script or executable used by `lake lint`. Libraries cannot be lint drivers.
* `lintDriverArgs`: An `Array` of arguments to pass to the package's lint driver.
You can specify definition from a dependency as a package's test or lint driver by using the syntax `<pkg>/<name>`. An executable driver will be built and then run, a script driver will just be run, and a library driver will just be built. A script or executable driver is run with any arguments configured by package (e.g., via `testDriverArgs`) followed by any specified on the CLI (e.g., via `lake lint -- <args>...`).
### Cloud Releases
These options define a cloud release for the package. See the section on [GitHub Release Builds](#github-release-builds) for more information.
* `releaseRepo`: The URL of the GitHub repository to upload and download releases of this package. If `none` (the default), for downloads, Lake uses the URL the package was download from (if it is a dependency) and for uploads, uses `gh`'s default.
* `buildArchive`: The name of the build archive for the GitHub cloud release.
Defaults to `{(pkg-)name}-{System.Platform.target}.tar.gz`.

View file

View file

@ -0,0 +1,6 @@
import Lake
open Lake DSL
package test where
testDriver := "dep/driver/foo"
lintDriver := "dep/driver/foo"

View file

@ -0,0 +1,3 @@
name = "test"
testDriver = "dep/driver/foo"
lintDriver = "dep/driver/foo"

View file

@ -0,0 +1,6 @@
import Lake
open Lake DSL
package test where
testDriver := "dep/driver"
lintDriver := "dep/driver"

View file

@ -0,0 +1,3 @@
name = "test"
testDriver = "dep/driver"
lintDriver = "dep/driver"

View file

@ -0,0 +1,8 @@
import Lake
open Lake DSL
package test where
testDriver := "dep/driver"
lintDriver := "dep/driver"
require dep from "dep"

View file

@ -0,0 +1,7 @@
name = "test"
testDriver = "dep/driver"
lintDriver = "dep/driver"
[[require]]
name = "dep"
path = "dep"

View file

@ -0,0 +1,9 @@
import Lake
open System Lake DSL
package dep
@[test_driver, lint_driver]
script driver args do
IO.println s!"dep: {args}"
return 0

View file

@ -0,0 +1,7 @@
import Lake
open System Lake DSL
package test
@[test_driver, lint_driver]
lean_exe driver

View file

@ -0,0 +1,6 @@
name = "test"
testDriver = "driver"
lintDriver = "driver"
[[lean_exe]]
name = "driver"

View file

@ -0,0 +1,7 @@
import Lake
open Lake DSL
package test
@[test_driver]
lean_lib Test

View file

@ -0,0 +1,5 @@
name = "test"
testDriver = "Test"
[[lean_lib]]
name = "Test"

View file

@ -4,4 +4,4 @@ open System Lake DSL
package test
@[test_runner]
lean_exe test
lean_exe driver

View file

@ -0,0 +1,5 @@
name = "test"
testRunner = "driver"
[[lean_exe]]
name = "driver"

View file

@ -3,7 +3,7 @@ open System Lake DSL
package test
@[test_runner]
script test args do
@[test_driver, lint_driver]
script driver args do
IO.println s!"script: {args}"
return 0

86
src/lake/tests/driver/test.sh Executable file
View file

@ -0,0 +1,86 @@
#!/usr/bin/env bash
set -euxo pipefail
LAKE=${LAKE:-../../.lake/build/bin/lake}
./clean.sh
# Script driver
$LAKE -f script.lean test | grep --color -F "script: []"
$LAKE -f script.lean lint | grep --color -F "script: []"
$LAKE -f script.lean test -- hello | grep --color "hello"
$LAKE -f script.lean lint -- hello | grep --color "hello"
# Executable driver
$LAKE -f exe.lean test | grep --color -F "exe: []"
$LAKE -f exe.toml test | grep --color -F "exe: []"
$LAKE -f exe.lean lint | grep --color -F "exe: []"
$LAKE -f exe.toml lint | grep --color -F "exe: []"
$LAKE -f exe.lean test -- hello | grep --color "hello"
$LAKE -f exe.toml test -- hello | grep --color "hello"
$LAKE -f exe.lean lint -- hello | grep --color "hello"
$LAKE -f exe.toml lint -- hello | grep --color "hello"
# Library test driver
rm -f .lake/build/lib/Test.olean
$LAKE -f lib.lean test | grep --color -F "Built Test"
rm -f .lake/build/lib/Test.olean
$LAKE -f lib.toml test | grep --color -F "Built Test"
($LAKE -f lib.lean test -- hello 2>&1 && exit 1 || true) | grep --color "arguments cannot be passed"
($LAKE -f lib.toml test -- hello 2>&1 && exit 1 || true) | grep --color "arguments cannot be passed"
# Upstream driver
rm -f lake-manifest.json
$LAKE -f dep.lean test | grep --color -F "dep: []"
$LAKE -f dep.toml test | grep --color -F "dep: []"
$LAKE -f dep.lean lint | grep --color -F "dep: []"
$LAKE -f dep.toml lint | grep --color -F "dep: []"
$LAKE -f dep.lean test -- hello | grep --color "hello"
$LAKE -f dep.toml test -- hello | grep --color "hello"
$LAKE -f dep.lean lint -- hello | grep --color "hello"
$LAKE -f dep.toml lint -- hello | grep --color "hello"
# Test runner
$LAKE -f runner.lean test 2>&1 | grep --color -F " @[test_runner] has been deprecated"
$LAKE -f runner.toml test
$LAKE -f runner.lean check-test
$LAKE -f runner.toml check-test
# Driver validation
($LAKE -f two.lean test 2>&1 && exit 1 || true) | grep --color "only one"
($LAKE -f none.lean test 2>&1 && exit 1 || true) | grep --color "no test driver"
($LAKE -f none.toml test 2>&1 && exit 1 || true) | grep --color "no test driver"
($LAKE -f unknown.lean test 2>&1 && exit 1 || true) | grep --color "invalid test driver: unknown"
($LAKE -f unknown.toml test 2>&1 && exit 1 || true) | grep --color "invalid test driver: unknown"
($LAKE -f dep-unknown.lean test 2>&1 && exit 1 || true) | grep --color "unknown test driver package"
($LAKE -f dep-unknown.toml test 2>&1 && exit 1 || true) | grep --color "unknown test driver package"
($LAKE -f dep-invalid.lean test 2>&1 && exit 1 || true) | grep --color "invalid test driver"
($LAKE -f dep-invalid.toml test 2>&1 && exit 1 || true) | grep --color "invalid test driver"
($LAKE -f two.lean lint 2>&1 && exit 1 || true) | grep --color "only one"
($LAKE -f none.lean lint 2>&1 && exit 1 || true) | grep --color "no lint driver"
($LAKE -f none.toml lint 2>&1 && exit 1 || true) | grep --color "no lint driver"
($LAKE -f unknown.lean lint 2>&1 && exit 1 || true) | grep --color "invalid lint driver: unknown"
($LAKE -f unknown.toml lint 2>&1 && exit 1 || true) | grep --color "invalid lint driver: unknown"
($LAKE -f dep-unknown.lean lint 2>&1 && exit 1 || true) | grep --color "unknown lint driver package"
($LAKE -f dep-unknown.toml lint 2>&1 && exit 1 || true) | grep --color "unknown lint driver package"
($LAKE -f dep-invalid.lean lint 2>&1 && exit 1 || true) | grep --color "invalid lint driver"
($LAKE -f dep-invalid.toml lint 2>&1 && exit 1 || true) | grep --color "invalid lint driver"
# Driver checker
$LAKE -f exe.lean check-test
$LAKE -f exe.toml check-test
$LAKE -f exe.lean check-lint
$LAKE -f exe.toml check-lint
$LAKE -f dep.lean check-test
$LAKE -f dep.toml check-test
$LAKE -f dep.lean check-lint
$LAKE -f dep.toml check-lint
$LAKE -f script.lean check-test
$LAKE -f script.lean check-lint
$LAKE -f lib.lean check-test
$LAKE -f two.lean check-test && exit 1 || true
$LAKE -f two.lean check-lint && exit 1 || true
$LAKE -f none.lean check-test && exit 1 || true
$LAKE -f none.toml check-test && exit 1 || true
$LAKE -f none.lean check-lint && exit 1 || true
$LAKE -f none.toml check-lint && exit 1 || true

View file

@ -3,8 +3,8 @@ open System Lake DSL
package test
@[test_runner]
@[test_driver, lint_driver]
lean_exe testExe
@[test_runner]
@[test_driver, lint_driver]
script testScript do return 1

View file

@ -0,0 +1,6 @@
import Lake
open System Lake DSL
package test where
testDriver := "invalid"
lintDriver := "invalid"

View file

@ -0,0 +1,3 @@
name = "test"
testDriver = "invalid"
lintDriver = "invalid"

View file

@ -1,5 +0,0 @@
name = "test"
testRunner = "test"
[[lean_exe]]
name = "test"

View file

@ -1,27 +0,0 @@
#!/usr/bin/env bash
set -euxo pipefail
LAKE=${LAKE:-../../.lake/build/bin/lake}
# Script test runner
$LAKE -f script.lean test | grep --color -F "script: []"
$LAKE -f script.lean test -- hello | grep --color "hello"
# Executable test runner
$LAKE -f exe.lean test | grep --color -F "exe: []"
$LAKE -f exe.lean test -- hello | grep --color "hello"
$LAKE -f exe.toml test | grep --color -F "exe: []"
$LAKE -f exe.toml test -- hello | grep --color "hello"
# Test runner validation
($LAKE -f two.lean test 2>&1 && exit 1 || true) | grep --color "only one"
($LAKE -f none.lean test 2>&1 && exit 1 || true) | grep --color "no test runner"
($LAKE -f none.toml test 2>&1 && exit 1 || true) | grep --color "no test runner"
# Test runner checker
$LAKE -f exe.lean check-test
$LAKE -f exe.toml check-test
$LAKE -f script.lean check-test
($LAKE -f two.lean check-test && exit 1 || true)
($LAKE -f none.lean check-test && exit 1 || true)
($LAKE -f none.toml check-test && exit 1 || true)

View file

@ -5,6 +5,8 @@ open System Lake DSL
package test where
releaseRepo := ""
buildArchive := ""
testDriver := "b"
lintDriver := "b"
platformIndependent := true
require foo from "-" with Lake.NameMap.empty |>.insert `foo "bar"
@ -13,4 +15,4 @@ require bar from git "https://example.com"@"abc"/"sub/dir"
@[default_target] lean_lib A
@[test_runner] lean_exe b
lean_exe b

View file

@ -1,5 +1,6 @@
name = "test"
testRunner = "b"
testDriver = "b"
lintDriver = "b"
defaultTargets = ["A"]
[leanOptions]

View file

@ -30,6 +30,7 @@ package test where
preferReleaseBuild := false
packagesDir := defaultPackagesDir
leanOptions := #[⟨`pp.unicode.fun, true⟩]
lintDriver := "b"
require foo from "dir" with NameMap.empty.insert `foo "bar"
require bar from git "https://example.com" @ "abc" / "sub" / "dir"
@ -45,7 +46,7 @@ lean_lib A where
nativeFacets := fun _ => #[]
toLeanConfig := testLeanConfig
@[test_runner]
@[test_driver]
lean_exe b where
srcDir := "."
root := `b

View file

@ -1,4 +1,5 @@
testRunner = "b"
testDriver = "b"
lintDriver = "b"
defaultTargets = ['A']
name = "test"