Skip to content

Commit 0448e3f

Browse files
authored
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.
1 parent d3ee0be commit 0448e3f

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+541
-157
lines changed

src/lake/Lake/CLI/Actions.lean

+46-7
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ Authors: Mac Malone
55
-/
66
import Lake.Build.Run
77
import Lake.Build.Targets
8+
import Lake.CLI.Build
89

910
namespace Lake
1011

@@ -28,14 +29,52 @@ def uploadRelease (pkg : Package) (tag : String) : LogIO Unit := do
2829
logInfo s!"uploading {tag}/{pkg.buildArchive}"
2930
proc {cmd := "gh", args}
3031

32+
def Package.resolveDriver
33+
(pkg : Package) (kind : String) (driver : String)
34+
: LakeT IO (Package × String) := do
35+
let pkgName := pkg.name.toString (escape := false)
36+
if driver.isEmpty then
37+
error s!"{pkgName}: no {kind} driver configured"
38+
else
39+
match driver.split (· == '/') with
40+
| [pkg, driver] =>
41+
let some pkg ← findPackage? pkg.toName
42+
| error s!"{pkgName}: unknown {kind} driver package '{pkg}'"
43+
return (pkg, driver)
44+
| [driver] =>
45+
return (pkg, driver)
46+
| _ =>
47+
error s!"{pkgName}: invalid {kind} driver '{driver}' (too many '/')"
48+
3149
def Package.test (pkg : Package) (args : List String := []) (buildConfig : BuildConfig := {}) : LakeT IO UInt32 := do
50+
let cfgArgs := pkg.testDriverArgs
51+
let (pkg, driver) ← pkg.resolveDriver "test" pkg.testDriver
3252
let pkgName := pkg.name.toString (escape := false)
33-
if pkg.testRunner.isAnonymous then
34-
error s!"{pkgName}: no test runner script or executable"
35-
else if let some script := pkg.scripts.find? pkg.testRunner then
36-
script.run args
37-
else if let some exe := pkg.findLeanExe? pkg.testRunner then
53+
if let some script := pkg.scripts.find? driver.toName then
54+
script.run (cfgArgs.toList ++ args)
55+
else if let some exe := pkg.findLeanExe? driver.toName then
56+
let exeFile ← runBuild exe.fetch buildConfig
57+
env exeFile.toString (cfgArgs ++ args.toArray)
58+
else if let some lib := pkg.findLeanLib? driver.toName then
59+
unless cfgArgs.isEmpty ∧ args.isEmpty do
60+
error s!"{pkgName}: arguments cannot be passed to a library test driver"
61+
match resolveLibTarget (← getWorkspace) lib with
62+
| .ok specs =>
63+
runBuild (buildSpecs specs) {buildConfig with out := .stdout}
64+
return 0
65+
| .error e =>
66+
error s!"{pkgName}: invalid test driver: {e}"
67+
else
68+
error s!"{pkgName}: invalid test driver: unknown script, executable, or library '{driver}'"
69+
70+
def Package.lint (pkg : Package) (args : List String := []) (buildConfig : BuildConfig := {}) : LakeT IO UInt32 := do
71+
let cfgArgs := pkg.lintDriverArgs
72+
let (pkg, driver) ← pkg.resolveDriver "lint" pkg.lintDriver
73+
if let some script := pkg.scripts.find? driver.toName then
74+
script.run (cfgArgs.data ++ args)
75+
else if let some exe := pkg.findLeanExe? driver.toName then
3876
let exeFile ← runBuild exe.fetch buildConfig
39-
env exeFile.toString args.toArray
77+
env exeFile.toString (cfgArgs ++ args.toArray)
4078
else
41-
error s!"{pkgName}: invalid test runner: unknown script or executable `{pkg.testRunner}`"
79+
let pkgName := pkg.name.toString (escape := false)
80+
error s!"{pkgName}: invalid lint driver: unknown script or executable '{driver}'"

src/lake/Lake/CLI/Build.lean

+14-7
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,18 @@ structure BuildSpec where
1515
getBuildJob : BuildData info.key → BuildJob Unit
1616

1717
@[inline] def BuildData.toBuildJob
18-
[FamilyOut BuildData k (BuildJob α)] (data : BuildData k) : BuildJob Unit :=
18+
[FamilyOut BuildData k (BuildJob α)] (data : BuildData k)
19+
: BuildJob Unit :=
1920
discard <| ofFamily data
2021

21-
@[inline] def mkBuildSpec (info : BuildInfo)
22-
[FamilyOut BuildData info.key (BuildJob α)] : BuildSpec :=
22+
@[inline] def mkBuildSpec
23+
(info : BuildInfo) [FamilyOut BuildData info.key (BuildJob α)]
24+
: BuildSpec :=
2325
{info, getBuildJob := BuildData.toBuildJob}
2426

25-
@[inline] def mkConfigBuildSpec (facetType : String)
26-
(info : BuildInfo) (config : FacetConfig Fam ι facet) (h : BuildData info.key = Fam facet)
27+
@[inline] def mkConfigBuildSpec
28+
(facetType : String) (info : BuildInfo)
29+
(config : FacetConfig Fam ι facet) (h : BuildData info.key = Fam facet)
2730
: Except CliError BuildSpec := do
2831
let some getJob := config.getJob?
2932
| throw <| CliError.nonCliFacet facetType facet
@@ -47,15 +50,19 @@ def parsePackageSpec (ws : Workspace) (spec : String) : Except CliError Package
4750
| none => throw <| CliError.unknownPackage spec
4851

4952
open Module in
50-
def resolveModuleTarget (ws : Workspace) (mod : Module) (facet : Name) : Except CliError BuildSpec :=
53+
def resolveModuleTarget
54+
(ws : Workspace) (mod : Module) (facet : Name := .anonymous)
55+
: Except CliError BuildSpec :=
5156
if facet.isAnonymous then
5257
return mkBuildSpec <| mod.facet leanArtsFacet
5358
else if let some config := ws.findModuleFacetConfig? facet then do
5459
mkConfigBuildSpec "module" (mod.facet facet) config rfl
5560
else
5661
throw <| CliError.unknownFacet "module" facet
5762

58-
def resolveLibTarget (ws : Workspace) (lib : LeanLib) (facet : Name) : Except CliError (Array BuildSpec) :=
63+
def resolveLibTarget
64+
(ws : Workspace) (lib : LeanLib) (facet : Name := .anonymous)
65+
: Except CliError (Array BuildSpec) :=
5966
if facet.isAnonymous then
6067
lib.defaultFacets.mapM (resolveFacet ·)
6168
else

src/lake/Lake/CLI/Help.lean

+59-4
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@ COMMANDS:
1818
init <name> <temp> create a Lean package in the current directory
1919
build <targets>... build targets
2020
exe <exe> <args>... build an exe and run it in Lake's environment
21-
test run the workspace's test script or executable
21+
test test the package using the configured test driver
22+
check-test check if there is a properly configured test driver
23+
lint lint the package using the configured lint driver
24+
check-lint check if there is a properly configured lint driver
2225
clean remove build outputs
2326
env <cmd> <args>... execute a command in Lake's environment
2427
lean <file> elaborate a Lean file in Lake's context
@@ -142,13 +145,62 @@ of the same package, the version materialized is undefined.
142145
A bare `lake update` will upgrade all dependencies."
143146

144147
def helpTest :=
145-
"Run the workspace's test script or executable
148+
"Test the workspace's root package using its configured test driver
146149
147150
USAGE:
148151
lake test [-- <args>...]
149152
150-
Looks for a script or executable tagged `@[test_runner]` in the workspace's
151-
root package and executes it with `args`. "
153+
A test driver can be configured by either setting the 'testDriver'
154+
package configuration option or by tagging a script, executable, or library
155+
`@[test_driver]`. A definition in a dependency can be used as a test driver
156+
by using the `<pkg>/<name>` syntax for the 'testDriver' configuration option.
157+
158+
A script test driver will be run with the package configuration's
159+
`testDriverArgs` plus the CLI `args`. An executable test driver will be
160+
built and then run like a script. A library test driver will just be built.
161+
"
162+
163+
def helpCheckTest :=
164+
"Check if there is a properly configured test driver
165+
166+
USAGE:
167+
lake check-test
168+
169+
Exits with code 0 if the workspace's root package has a properly
170+
configured lint driver. Errors (with code 1) otherwise.
171+
172+
Does NOT verify that the configured test driver actually exists in the
173+
package or its dependencies. It merely verifies that one is specified.
174+
"
175+
176+
def helpLint :=
177+
"Lint the workspace's root package using its configured lint driver
178+
179+
USAGE:
180+
lake lint [-- <args>...]
181+
182+
A lint driver can be configured by either setting the `lintDriver` package
183+
configuration option by tagging a script or executable `@[lint_driver]`.
184+
A definition in dependency can be used as a test driver by using the
185+
`<pkg>/<name>` syntax for the 'testDriver' configuration option.
186+
187+
A script lint driver will be run with the package configuration's
188+
`lintDriverArgs` plus the CLI `args`. An executable lint driver will be
189+
built and then run like a script.
190+
"
191+
192+
def helpCheckLint :=
193+
"Check if there is a properly configured lint driver
194+
195+
USAGE:
196+
lake check-lint
197+
198+
Exits with code 0 if the workspace's root package has a properly
199+
configured lint driver. Errors (with code 1) otherwise.
200+
201+
Does NOT verify that the configured lint driver actually exists in the
202+
package or its dependencies. It merely verifies that one is specified.
203+
"
152204

153205
def helpUpload :=
154206
"Upload build artifacts to a GitHub release
@@ -302,6 +354,9 @@ def help : (cmd : String) → String
302354
| "update" | "upgrade" => helpUpdate
303355
| "upload" => helpUpload
304356
| "test" => helpTest
357+
| "check-test" => helpCheckTest
358+
| "lint" => helpLint
359+
| "check-lint" => helpCheckLint
305360
| "clean" => helpClean
306361
| "script" => helpScriptCli
307362
| "scripts" => helpScriptList

src/lake/Lake/CLI/Main.lean

+19-6
Original file line numberDiff line numberDiff line change
@@ -346,16 +346,28 @@ protected def setupFile : CliM PUnit := do
346346
protected def test : CliM PUnit := do
347347
processOptions lakeOption
348348
let opts ← getThe LakeOptions
349-
let config ← mkLoadConfig opts
350-
let ws ← loadWorkspace config
349+
let ws ← loadWorkspace (← mkLoadConfig opts)
351350
noArgsRem do
352351
let x := ws.root.test opts.subArgs (mkBuildConfig opts)
353352
exit <| ← x.run (mkLakeContext ws)
354353

355354
protected def checkTest : CliM PUnit := do
356355
processOptions lakeOption
357-
let ws ← loadWorkspace ( ← mkLoadConfig (← getThe LakeOptions))
358-
noArgsRem do exit <| if ws.root.testRunner.isAnonymous then 1 else 0
356+
let pkg ← loadPackage (← mkLoadConfig (← getThe LakeOptions))
357+
noArgsRem do exit <| if pkg.testDriver.isEmpty then 1 else 0
358+
359+
protected def lint : CliM PUnit := do
360+
processOptions lakeOption
361+
let opts ← getThe LakeOptions
362+
let ws ← loadWorkspace (← mkLoadConfig opts)
363+
noArgsRem do
364+
let x := ws.root.lint opts.subArgs (mkBuildConfig opts)
365+
exit <| ← x.run (mkLakeContext ws)
366+
367+
protected def checkLint : CliM PUnit := do
368+
processOptions lakeOption
369+
let pkg ← loadPackage (← mkLoadConfig (← getThe LakeOptions))
370+
noArgsRem do exit <| if pkg.lintDriver.isEmpty then 1 else 0
359371

360372
protected def clean : CliM PUnit := do
361373
processOptions lakeOption
@@ -440,8 +452,7 @@ protected def translateConfig : CliM PUnit := do
440452
let lang ← parseLangSpec (← takeArg "configuration language")
441453
let outFile? := (← takeArg?).map FilePath.mk
442454
noArgsRem do
443-
Lean.searchPathRef.set cfg.lakeEnv.leanSearchPath
444-
let (pkg, _) ← loadPackage "[root]" cfg
455+
let pkg ← loadPackage cfg
445456
let outFile := outFile?.getD <| pkg.configFile.withExtension lang.fileExtension
446457
if (← outFile.pathExists) then
447458
throw (.outputConfigExists outFile)
@@ -468,6 +479,8 @@ def lakeCli : (cmd : String) → CliM PUnit
468479
| "setup-file" => lake.setupFile
469480
| "test" => lake.test
470481
| "check-test" => lake.checkTest
482+
| "lint" => lake.lint
483+
| "check-lint" => lake.checkLint
471484
| "clean" => lake.clean
472485
| "script" => lake.script
473486
| "scripts" => lake.script.list

src/lake/Lake/CLI/Translate/Lean.lean

+12-11
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,10 @@ def LeanConfig.addDeclFields (cfg : LeanConfig) (fs : Array DeclField) : Array D
8989
@[inline] def mkDeclValWhere? (fields : Array DeclField) : Option (TSyntax ``declValWhere) :=
9090
if fields.isEmpty then none else Unhygienic.run `(declValWhere|where $fields*)
9191

92-
def PackageConfig.mkSyntax (cfg : PackageConfig) : PackageDecl := Unhygienic.run do
93-
let declVal? := mkDeclValWhere? <|Array.empty
92+
def PackageConfig.mkSyntax (cfg : PackageConfig)
93+
(testDriver := cfg.testDriver) (lintDriver := cfg.lintDriver)
94+
: PackageDecl := Unhygienic.run do
95+
let declVal? := mkDeclValWhere? <| Array.empty
9496
|> addDeclFieldD `precompileModules cfg.precompileModules false
9597
|> addDeclFieldD `moreGlobalServerArgs cfg.moreGlobalServerArgs #[]
9698
|> addDeclFieldD `srcDir cfg.srcDir "."
@@ -102,6 +104,10 @@ def PackageConfig.mkSyntax (cfg : PackageConfig) : PackageDecl := Unhygienic.run
102104
|> addDeclField? `releaseRepo (cfg.releaseRepo <|> cfg.releaseRepo?)
103105
|> addDeclFieldD `buildArchive (cfg.buildArchive?.getD cfg.buildArchive) (defaultBuildArchive cfg.name)
104106
|> addDeclFieldD `preferReleaseBuild cfg.preferReleaseBuild false
107+
|> addDeclFieldD `testDriver testDriver ""
108+
|> addDeclFieldD `testDriverArgs cfg.testDriverArgs #[]
109+
|> addDeclFieldD `lintDriver lintDriver ""
110+
|> addDeclFieldD `lintDriverArgs cfg.lintDriverArgs #[]
105111
|> cfg.toWorkspaceConfig.addDeclFields
106112
|> cfg.toLeanConfig.addDeclFields
107113
`(packageDecl|package $(mkIdent cfg.name) $[$declVal?]?)
@@ -145,19 +151,15 @@ protected def LeanLibConfig.mkSyntax
145151
`(leanLibDecl|$[$attrs?:attributes]? lean_lib $(mkIdent cfg.name) $[$declVal?]?)
146152

147153
protected def LeanExeConfig.mkSyntax
148-
(cfg : LeanExeConfig) (defaultTarget := false) (testRunner := false)
154+
(cfg : LeanExeConfig) (defaultTarget := false)
149155
: LeanExeDecl := Unhygienic.run do
150156
let declVal? := mkDeclValWhere? <| Array.empty
151157
|> addDeclFieldD `srcDir cfg.srcDir "."
152158
|> addDeclFieldD `root cfg.root cfg.name
153159
|> addDeclFieldD `exeName cfg.exeName (cfg.name.toStringWithSep "-" (escape := false))
154160
|> addDeclFieldD `supportInterpreter cfg.supportInterpreter false
155161
|> cfg.toLeanConfig.addDeclFields
156-
let attrs? ← id do
157-
let mut attrs := #[]
158-
if testRunner then attrs := attrs.push <| ← `(Term.attrInstance|test_runner)
159-
if defaultTarget then attrs := attrs.push <| ← `(Term.attrInstance|default_target)
160-
if attrs.isEmpty then pure none else some <$> `(Term.attributes|@[$attrs,*])
162+
let attrs? ← if defaultTarget then some <$> `(Term.attributes|@[default_target]) else pure none
161163
`(leanExeDecl|$[$attrs?:attributes]? lean_exe $(mkIdent cfg.name) $[$declVal?]?)
162164

163165
protected def Dependency.mkSyntax (cfg : Dependency) : RequireDecl := Unhygienic.run do
@@ -175,14 +177,13 @@ protected def Dependency.mkSyntax (cfg : Dependency) : RequireDecl := Unhygienic
175177

176178
/-- Create a Lean module that encodes the declarative configuration of the package. -/
177179
def Package.mkLeanConfig (pkg : Package) : TSyntax ``module := Unhygienic.run do
178-
let testRunner := pkg.testRunner
179180
let defaultTargets := pkg.defaultTargets.foldl NameSet.insert NameSet.empty
180-
let pkgConfig := pkg.config.mkSyntax
181+
let pkgConfig := pkg.config.mkSyntax pkg.testDriver pkg.lintDriver
181182
let requires := pkg.depConfigs.map (·.mkSyntax)
182183
let leanLibs := pkg.leanLibConfigs.toArray.map fun cfg =>
183184
cfg.mkSyntax (defaultTargets.contains cfg.name)
184185
let leanExes := pkg.leanExeConfigs.toArray.map fun cfg =>
185-
cfg.mkSyntax (defaultTargets.contains cfg.name) (cfg.name == testRunner)
186+
cfg.mkSyntax (defaultTargets.contains cfg.name)
186187
`(module|
187188
import $(mkIdent `Lake)
188189
open $(mkIdent `System) $(mkIdent `Lake) $(mkIdent `DSL)

src/lake/Lake/CLI/Translate/Toml.lean

+4-1
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,10 @@ instance : ToToml Dependency := ⟨(toToml ·.toToml)⟩
122122
/-- Create a TOML table that encodes the declarative configuration of the package. -/
123123
def Package.mkTomlConfig (pkg : Package) (t : Table := {}) : Table :=
124124
pkg.config.toToml t
125-
|>.insertD `testRunner pkg.testRunner .anonymous
125+
|>.smartInsert `testDriver pkg.testDriver
126+
|>.smartInsert `testDriverArgs pkg.testDriverArgs
127+
|>.smartInsert `lintDriver pkg.lintDriver
128+
|>.smartInsert `lintDriverArgs pkg.lintDriverArgs
126129
|>.smartInsert `defaultTargets pkg.defaultTargets
127130
|>.smartInsert `require pkg.depConfigs
128131
|>.smartInsert `lean_lib pkg.leanLibConfigs.toArray

0 commit comments

Comments
 (0)