3939from functools import partial
4040from pathlib import Path
4141from typing import List
42+ import pickle
43+ from hashlib import sha1
44+ import inspect
4245
4346__version__ = '1.7.3'
4447
@@ -307,9 +310,12 @@ class TerraformTest(object):
307310 directory above the one this module lives in.
308311 binary: path to the Terraform command.
309312 env: a dict with custom environment variables to pass to terraform.
313+ enable_cache: Determines if the caching enabled for specific methods
314+ cache_dir: optional base directory to use for caching, defaults to
315+ the directory of the python file that instantiates this class
310316 """
311317
312- def __init__ (self , tfdir , basedir = None , binary = 'terraform' , env = None ):
318+ def __init__ (self , tfdir , basedir = None , binary = 'terraform' , env = None , enable_cache = False , cache_dir = None ):
313319 """Set Terraform folder to operate on, and optional base directory."""
314320 self ._basedir = basedir or os .getcwd ()
315321 self .binary = binary
@@ -318,6 +324,12 @@ def __init__(self, tfdir, basedir=None, binary='terraform', env=None):
318324 self .tg_run_all = False
319325 self ._plan_formatter = lambda out : TerraformPlanOutput (json .loads (out ))
320326 self ._output_formatter = lambda out : TerraformValueDict (json .loads (out ))
327+ self .enable_cache = enable_cache
328+ if not cache_dir :
329+ self .cache_dir = Path (os .path .dirname (
330+ inspect .stack ()[1 ].filename )) / ".tftest-cache"
331+ else :
332+ self .cache_dir = Path (cache_dir )
321333 if env is not None :
322334 self .env .update (env )
323335
@@ -363,9 +375,74 @@ def _abspath(self, path):
363375 """Make relative path absolute from base dir."""
364376 return path if os .path .isabs (path ) else os .path .join (self ._basedir , path )
365377
378+ def _cache (func ):
379+ def cache (self , ** kwargs ):
380+ """
381+ Runs the tftest instance method or retreives the cache value if it exists
382+
383+ Args:
384+ kwargs: Keyword argument that are passed to the decorated method
385+ Returns:
386+ Output of the tftest instance method
387+ """
388+ _LOGGER .info ("Cache decorated method: %s" , func .__name__ )
389+
390+ if not self .enable_cache :
391+ return func (self , ** kwargs )
392+ elif not kwargs .get ("use_cache" , False ):
393+ return func (self , ** kwargs )
394+
395+ cache_dir = self .cache_dir / \
396+ Path (self .tfdir .strip ("/" )) / Path (func .__name__ )
397+ # creates cache dir if not exists
398+ cache_dir .mkdir (parents = True , exist_ok = True )
399+
400+ params = {
401+ ** {
402+ k : v
403+ for k , v in self .__dict__ .items ()
404+ # only uses instance attributes that are involved in the results of
405+ # the decorated method
406+ if k in ["binary" , "_basedir" , "tfdir" , "env" ]
407+ },
408+ ** kwargs ,
409+ }
410+
411+ hash_filename = sha1 (
412+ json .dumps (params , sort_keys = True , default = str ).encode ("cp037" )
413+ ).hexdigest () + ".pickle"
414+
415+ cache_key = cache_dir / hash_filename
416+ _LOGGER .debug ("Cache key: %s" , cache_key )
417+
418+ try :
419+ f = cache_key .open ("rb" )
420+ except OSError :
421+ _LOGGER .debug ("Could not read cache path" )
422+ else :
423+ _LOGGER .info ("Getting output from cache" )
424+ return pickle .load (f )
425+
426+ _LOGGER .info ("Running command" )
427+ out = func (self , ** kwargs )
428+
429+ if out :
430+ _LOGGER .info ("Writing command to cache" )
431+ try :
432+ f = cache_key .open ("wb" )
433+ except OSError as e :
434+ _LOGGER .error ("Cache could not write path" )
435+ else :
436+ with f :
437+ pickle .dump (out , f , pickle .HIGHEST_PROTOCOL )
438+
439+ return out
440+ return cache
441+
442+ @_cache
366443 def setup (self , extra_files = None , plugin_dir = None , init_vars = None ,
367444 backend = True , cleanup_on_exit = True , disable_prevent_destroy = False ,
368- workspace_name = None , ** kw ):
445+ workspace_name = None , use_cache = False , ** kw ):
369446 """Setup method to use in test fixtures.
370447
371448 This method prepares a new Terraform environment for testing the module
@@ -437,13 +514,14 @@ def setup(self, extra_files=None, plugin_dir=None, init_vars=None,
437514 filenames , deep = cleanup_on_exit ,
438515 restore_files = disable_prevent_destroy )
439516 setup_output = self .init (plugin_dir = plugin_dir , init_vars = init_vars ,
440- backend = backend , ** kw )
517+ backend = backend , use_cache = use_cache , ** kw )
441518 if workspace_name :
442519 setup_output += self .workspace (name = workspace_name )
443520 return setup_output
444521
522+ @_cache
445523 def init (self , input = False , color = False , force_copy = False , plugin_dir = None ,
446- init_vars = None , backend = True , ** kw ):
524+ init_vars = None , backend = True , use_cache = False , ** kw ):
447525 """Run Terraform init command."""
448526 cmd_args = parse_args (input = input , color = color , backend = backend ,
449527 force_copy = force_copy , plugin_dir = plugin_dir ,
@@ -462,8 +540,9 @@ def workspace(self, name=None):
462540 cmd_args = ['new' , name ]
463541 return self .execute_command ('workspace' , * cmd_args ).out
464542
543+ @_cache
465544 def plan (self , input = False , color = False , refresh = True , tf_vars = None ,
466- targets = None , output = False , tf_var_file = None , ** kw ):
545+ targets = None , output = False , tf_var_file = None , use_cache = False , ** kw ):
467546 """
468547 Run Terraform plan command, optionally returning parsed plan output.
469548
@@ -496,8 +575,9 @@ def plan(self, input=False, color=False, refresh=True, tf_vars=None,
496575 except json .JSONDecodeError as e :
497576 raise TerraformTestError ('Error decoding plan output: {}' .format (e ))
498577
578+ @_cache
499579 def apply (self , input = False , color = False , auto_approve = True , tf_vars = None ,
500- targets = None , tf_var_file = None , ** kw ):
580+ targets = None , tf_var_file = None , use_cache = False , ** kw ):
501581 """
502582 Run Terraform apply command.
503583
@@ -515,7 +595,8 @@ def apply(self, input=False, color=False, auto_approve=True, tf_vars=None,
515595 tf_var_file = tf_var_file , ** kw )
516596 return self .execute_command ('apply' , * cmd_args ).out
517597
518- def output (self , name = None , color = False , json_format = True , ** kw ):
598+ @_cache
599+ def output (self , name = None , color = False , json_format = True , use_cache = False , ** kw ):
519600 """Run Terraform output command."""
520601 cmd_args = []
521602 if name :
@@ -530,8 +611,9 @@ def output(self, name=None, color=False, json_format=True, **kw):
530611 _LOGGER .warning ('error decoding output: {}' .format (e ))
531612 return output
532613
614+ @_cache
533615 def destroy (self , color = False , auto_approve = True , tf_vars = None , targets = None ,
534- tf_var_file = None , ** kw ):
616+ tf_var_file = None , use_cache = False , ** kw ):
535617 """Run Terraform destroy command."""
536618 cmd_args = parse_args (color = color , auto_approve = auto_approve ,
537619 tf_vars = tf_vars , targets = targets ,
@@ -612,7 +694,7 @@ def _parse_run_all_out(output: str, formatter: TerraformJSONBase) -> str:
612694class TerragruntTest (TerraformTest ):
613695
614696 def __init__ (self , tfdir , basedir = None , binary = 'terragrunt' , env = None ,
615- tg_run_all = False ):
697+ tg_run_all = False , enable_cache = False , cache_dir = None ):
616698 """A helper class that could be used for testing terragrunt
617699
618700 Most operations that apply to :func:`~TerraformTest` also apply to this class.
@@ -629,8 +711,12 @@ def __init__(self, tfdir, basedir=None, binary='terragrunt', env=None,
629711 binary: (Optional) path to terragrunt command.
630712 env: a dict with custom environment variables to pass to terraform.
631713 tg_run_all: whether the test is for terragrunt run-all, default to False
714+ enable_cache: Determines if the caching enabled for specific methods
715+ cache_dir: optional base directory to use for caching, defaults to
716+ the directory of the python file that instantiates this class
632717 """
633- TerraformTest .__init__ (self , tfdir , basedir , binary , env )
718+ TerraformTest .__init__ (self , tfdir , basedir , binary ,
719+ env , enable_cache , cache_dir )
634720 self .tg_run_all = tg_run_all
635721 if self .tg_run_all :
636722 self ._plan_formatter = partial (_parse_run_all_out ,
0 commit comments