@@ -34,6 +34,7 @@ type ComponentTestOptions struct {
3434 Tests []string
3535 WorkDir string
3636 Provision string
37+ FromSpec bool
3738}
3839
3940type tmtSource struct {
@@ -58,6 +59,10 @@ type tmtRunSettings struct {
5859 WorkDir string
5960 TMTProgramPath string
6061 Provision string
62+ // FromSpec runs the plan from the component's rendered spec directory
63+ // instead of cloning the catalog 'source'. SpecDir is that directory.
64+ FromSpec bool
65+ SpecDir string
6166}
6267
6368const (
@@ -142,8 +147,9 @@ AZURE LINUX 4 PREREQUISITES:
142147 supplied candidate RPMs and execute the plan, so it modifies the host.
143148
144149azldev creates or reuses an isolated Python environment under --work-dir and
145- installs TMT with virtual-provisioner support there. python3 and git must be
146- available on the host.` ,
150+ installs TMT with virtual-provisioner support there. python3 must be available
151+ on the host; git is also required unless '--from-spec' is used (which runs the
152+ plan from the rendered spec directory instead of cloning).` ,
147153 Example : ` # Build the component first
148154 azldev component build buildah
149155
@@ -161,6 +167,12 @@ available on the host.`,
161167 }),
162168 }
163169
170+ registerComponentTestFlags (cmd , options )
171+
172+ return cmd
173+ }
174+
175+ func registerComponentTestFlags (cmd * cobra.Command , options * ComponentTestOptions ) {
164176 cmd .Flags ().StringVarP (& options .ImagePath , "image-path" , "i" , "" , "Path to the qcow2 image under test" )
165177 _ = cmd .MarkFlagFilename ("image-path" )
166178 cmd .Flags ().StringSliceVarP (
@@ -177,9 +189,12 @@ available on the host.`,
177189 _ = cmd .MarkFlagDirname ("work-dir" )
178190 cmd .Flags ().StringVar (& options .Provision , "provision" , tmtProvisionVirtual ,
179191 "TMT provisioner mode: 'virtual' (default) runs tests in QEMU; 'local' runs on this machine (must be Azure Linux 4)" )
192+ cmd .Flags ().BoolVar (& options .FromSpec , "from-spec" , false ,
193+ "Run the plan from the component's rendered spec directory (under the configured " +
194+ "'project.rendered-specs-dir', e.g. 'SPECS/c/curl') instead of cloning the catalog " +
195+ "'source'. Requires 'render.skip-file-filter = true' and a prior " +
196+ "'azldev component render'. Local inner-loop convenience; not used by cloud (TEE) runs." )
180197 _ = cmd .MarkFlagRequired ("rpm" )
181-
182- return cmd
183198}
184199
185200func runComponentTMTTests (env * azldev.Env , componentName string , options * ComponentTestOptions ) error {
@@ -197,12 +212,12 @@ func runComponentTMTTests(env *azldev.Env, componentName string, options *Compon
197212 return err
198213 }
199214
200- resolved , err := resolveComponentTMTTests (env , componentName , options .Tests )
215+ resolved , specDir , err := resolveComponentTMTTests (env , componentName , options .Tests )
201216 if err != nil {
202217 return err
203218 }
204219
205- workDir , tmtProgramPath , err := prepareTMTEnvironment (env , options .WorkDir , options .Provision )
220+ workDir , tmtProgramPath , err := prepareTMTEnvironment (env , options .WorkDir , options .Provision , options . FromSpec )
206221 if err != nil {
207222 return err
208223 }
@@ -213,6 +228,8 @@ func runComponentTMTTests(env *azldev.Env, componentName string, options *Compon
213228 WorkDir : workDir ,
214229 TMTProgramPath : tmtProgramPath ,
215230 Provision : options .Provision ,
231+ FromSpec : options .FromSpec ,
232+ SpecDir : specDir ,
216233 }
217234
218235 for _ , test := range resolved {
@@ -275,32 +292,36 @@ func resolveTMTImagePath(env *azldev.Env, provision string, configuredImagePath
275292
276293func resolveComponentTMTTests (
277294 env * azldev.Env , componentName string , selectors []string ,
278- ) ([]projectconfig.ResolvedTest , error ) {
295+ ) ([]projectconfig.ResolvedTest , string , error ) {
279296 resolver := components .NewResolver (env )
280297
281298 set , err := resolver .FindComponents (& components.ComponentFilter {ComponentNamePatterns : []string {componentName }})
282299 if err != nil {
283- return nil , fmt .Errorf ("resolve component %#q:\n %w" , componentName , err )
300+ return nil , "" , fmt .Errorf ("resolve component %#q:\n %w" , componentName , err )
284301 }
285302
286303 if set .Len () != 1 {
287- return nil , fmt .Errorf ("expected exactly one component named %#q, found %d" , componentName , set .Len ())
304+ return nil , "" , fmt .Errorf ("expected exactly one component named %#q, found %d" , componentName , set .Len ())
288305 }
289306
290- resolved , err := env .Config ().ResolveComponentTests (set .Components ()[0 ].GetConfig ())
307+ componentConfig := set .Components ()[0 ].GetConfig ()
308+
309+ resolved , err := env .Config ().ResolveComponentTests (componentConfig )
291310 if err != nil {
292- return nil , fmt .Errorf ("resolve tests for component %#q:\n %w" , componentName , err )
311+ return nil , "" , fmt .Errorf ("resolve tests for component %#q:\n %w" , componentName , err )
293312 }
294313
295314 resolved = selectTMTTests (resolved , selectors )
296315 if len (resolved ) == 0 {
297- return nil , fmt .Errorf ("component %#q has no selected TMT tests" , componentName )
316+ return nil , "" , fmt .Errorf ("component %#q has no selected TMT tests" , componentName )
298317 }
299318
300- return resolved , nil
319+ return resolved , componentConfig . RenderedSpecDir , nil
301320}
302321
303- func prepareTMTEnvironment (env * azldev.Env , configuredWorkDir string , provision string ) (string , string , error ) {
322+ func prepareTMTEnvironment (
323+ env * azldev.Env , configuredWorkDir string , provision string , fromSpec bool ,
324+ ) (string , string , error ) {
304325 workDir , err := componentTMTWorkDir (env , configuredWorkDir )
305326 if err != nil {
306327 return "" , "" , fmt .Errorf ("resolve work directory:\n %w" , err )
@@ -315,7 +336,7 @@ func prepareTMTEnvironment(env *azldev.Env, configuredWorkDir string, provision
315336 return "" , "" , fmt .Errorf ("create work directory:\n %w" , err )
316337 }
317338
318- tmtProgramPath , err = ensureTMTVenv (env , workDir , provision )
339+ tmtProgramPath , err = ensureTMTVenv (env , workDir , provision , fromSpec )
319340 if err != nil {
320341 return "" , "" , err
321342 }
@@ -324,18 +345,23 @@ func prepareTMTEnvironment(env *azldev.Env, configuredWorkDir string, provision
324345}
325346
326347// ensureTMTVenv creates or reuses an isolated TMT installation. This follows
327- // the local LISA runner pattern: Python and git are explicit host
328- // prerequisites, while the test framework itself is installed in a venv under
329- // the selected work directory rather than assumed to be packaged by the host
330- // distribution. For virtual provisioning, the testcloud plugin supplies
331- // provisioner support. For local provisioning, only base TMT is required.
332- func ensureTMTVenv (env * azldev.Env , workDir string , provision string ) (string , error ) {
348+ // the local LISA runner pattern: Python (and git, unless --from-spec avoids
349+ // cloning) are explicit host prerequisites, while the test framework itself is
350+ // installed in a venv under the selected work directory rather than assumed to
351+ // be packaged by the host distribution. For virtual provisioning, the testcloud
352+ // plugin supplies provisioner support. For local provisioning, only base TMT is
353+ // required.
354+ func ensureTMTVenv (env * azldev.Env , workDir string , provision string , fromSpec bool ) (string , error ) {
333355 if err := prereqs .RequireExecutable (env , tmtPythonProgram , nil ); err != nil {
334356 return "" , fmt .Errorf ("python3 is required to run TMT tests:\n %w" , err )
335357 }
336358
337- if err := prereqs .RequireExecutable (env , "git" , nil ); err != nil {
338- return "" , fmt .Errorf ("git is required to clone TMT test metadata:\n %w" , err )
359+ // --from-spec runs the plan from the rendered spec directory and never clones,
360+ // so git is only a prerequisite for the default (clone) path.
361+ if ! fromSpec {
362+ if err := prereqs .RequireExecutable (env , "git" , nil ); err != nil {
363+ return "" , fmt .Errorf ("git is required to clone TMT test metadata:\n %w" , err )
364+ }
339365 }
340366
341367 venvDir := filepath .Join (workDir , "tmt" , tmtVenvDirName )
@@ -468,18 +494,28 @@ func runOneTMTTest(env *azldev.Env, test projectconfig.ResolvedTest, settings tm
468494 return err
469495 }
470496
471- if err := runHostCommand (env , testDir , "git" , "clone" , "--no-checkout" , config .Source .GitURL , repoDir ); err != nil {
472- return fmt .Errorf ("clone test metadata:\n %w" , err )
473- }
497+ // runDir is the fmf tree tmt runs against: either the freshly cloned catalog
498+ // source (default) or the component's rendered spec directory (--from-spec).
499+ runDir := repoDir
500+ if settings .FromSpec {
501+ runDir , err = resolveSpecRunDir (env , settings .SpecDir )
502+ if err != nil {
503+ return err
504+ }
505+ } else {
506+ if err := runHostCommand (env , testDir , "git" , "clone" , "--no-checkout" , config .Source .GitURL , repoDir ); err != nil {
507+ return fmt .Errorf ("clone test metadata:\n %w" , err )
508+ }
474509
475- if err := runHostCommand (env , repoDir , "git" , "checkout" , "--detach" , config .Source .Ref ); err != nil {
476- return fmt .Errorf ("checkout test metadata:\n %w" , err )
510+ if err := runHostCommand (env , repoDir , "git" , "checkout" , "--detach" , config .Source .Ref ); err != nil {
511+ return fmt .Errorf ("checkout test metadata:\n %w" , err )
512+ }
477513 }
478514
479515 var hardwareArgs []string
480516 if settings .Provision == tmtProvisionVirtual {
481517 hardwareArgs , err = resolvedPlanHardwareArgs (
482- env , repoDir , settings .TMTProgramPath , config .Plan ,
518+ env , runDir , settings .TMTProgramPath , config .Plan ,
483519 )
484520 if err != nil {
485521 return fmt .Errorf ("resolve hardware for TMT plan %#q:\n %w" , config .Plan , err )
@@ -488,13 +524,44 @@ func runOneTMTTest(env *azldev.Env, test projectconfig.ResolvedTest, settings tm
488524
489525 args := componentTMTArgs (config , tmtWorkDir , settings .Provision , settings .ImagePath , hardwareArgs , settings .RPMs )
490526
491- if err := runTMTCommand (env , repoDir , pluginDir , settings .TMTProgramPath , settings .Provision , args ... ); err != nil {
527+ if err := runTMTCommand (env , runDir , pluginDir , settings .TMTProgramPath , settings .Provision , args ... ); err != nil {
492528 return fmt .Errorf ("run TMT plan %#q (artifacts: %#q):\n %w" , config .Plan , tmtWorkDir , err )
493529 }
494530
495531 return nil
496532}
497533
534+ // resolveSpecRunDir validates that the component's rendered spec directory
535+ // exists and carries an fmf root, returning it as the tmt run directory. It
536+ // backs the --from-spec flow, which runs a plan straight from the component's
537+ // rendered spec directory (derived from the project 'rendered-specs-dir'
538+ // setting, e.g. 'SPECS/c/curl') instead of cloning the catalog 'source'. tmt
539+ // writes its run artifacts under a separate --workdir-root, so the rendered
540+ // tree is only read.
541+ func resolveSpecRunDir (env * azldev.Env , specDir string ) (string , error ) {
542+ if specDir == "" {
543+ return "" , errors .New (
544+ "'--from-spec' requires a rendered spec directory; ensure 'project.rendered-specs-dir' is set" )
545+ }
546+
547+ fmfVersion := filepath .Join (specDir , ".fmf" , "version" )
548+
549+ exists , err := fileutils .Exists (env .FS (), fmfVersion )
550+ if err != nil {
551+ return "" , fmt .Errorf ("check fmf root %#q:\n %w" , fmfVersion , err )
552+ }
553+
554+ if ! exists {
555+ return "" , fmt .Errorf (
556+ "no fmf metadata in rendered spec dir %#q (missing %#q); set " +
557+ "'render.skip-file-filter = true' on the component and run " +
558+ "'azldev component render' before using '--from-spec'" ,
559+ specDir , filepath .Join (".fmf" , "version" ))
560+ }
561+
562+ return specDir , nil
563+ }
564+
498565// prepareTMTTestDir creates the per-test directory and, for virtual runs, the
499566// testcloud plugin. It returns the plugin directory, which is empty when no
500567// plugin is needed.
0 commit comments