7
7
"log/slog"
8
8
"os"
9
9
"path/filepath"
10
+ "slices"
10
11
"sort"
11
12
"testing"
12
13
"testing/fstest"
@@ -18,6 +19,7 @@ import (
18
19
"github.com/aquasecurity/trivy/internal/testutil"
19
20
"github.com/aquasecurity/trivy/pkg/iac/terraform"
20
21
"github.com/aquasecurity/trivy/pkg/log"
22
+ "github.com/aquasecurity/trivy/pkg/set"
21
23
)
22
24
23
25
func Test_BasicParsing (t * testing.T ) {
@@ -1714,6 +1716,20 @@ func TestNestedModulesOptions(t *testing.T) {
1714
1716
var buf bytes.Buffer
1715
1717
slog .SetDefault (slog .New (log .NewHandler (& buf , nil )))
1716
1718
1719
+ // Folder structure
1720
+ // ./
1721
+ // ├── main.tf
1722
+ // └── modules
1723
+ // ├── city
1724
+ // │ └── main.tf
1725
+ // ├── queens
1726
+ // │ └── main.tf
1727
+ // └── brooklyn
1728
+ // └── main.tf
1729
+ //
1730
+ // Modules referenced
1731
+ // main -> city ├─> brooklyn
1732
+ // └─> queens
1717
1733
files := map [string ]string {
1718
1734
"main.tf" : `
1719
1735
module "city" {
@@ -1769,6 +1785,140 @@ module "invalid" {
1769
1785
}
1770
1786
1771
1787
require .NotContains (t , buf .String (), "failed to download" )
1788
+
1789
+ // Verify module parents are set correctly.
1790
+ expectedParents := map [string ]string {
1791
+ "." : "" ,
1792
+ "modules/city" : "." ,
1793
+ "modules/city/brooklyn" : "modules/city" ,
1794
+ "modules/city/queens" : "modules/city" ,
1795
+ }
1796
+
1797
+ for _ , mod := range modules {
1798
+ expected , exists := expectedParents [mod .ModulePath ()]
1799
+ require .Truef (t , exists , "module %s does not exist in assertion" , mod .ModulePath ())
1800
+ if expected == "" {
1801
+ require .Nil (t , mod .Parent ())
1802
+ } else {
1803
+ require .Equal (t , expected , mod .Parent ().ModulePath (), "parent of module %q" , mod .ModulePath ())
1804
+ }
1805
+ }
1806
+ }
1807
+
1808
+ // TestModuleParents sets up a nested module structure and verifies the
1809
+ // parent-child relationships are correctly set.
1810
+ func TestModuleParents (t * testing.T ) {
1811
+ // The setup is a list of continents, some countries, some cities, etc.
1812
+ dirfs := os .DirFS ("./testdata/nested" )
1813
+ parser := New (dirfs , "" ,
1814
+ OptionStopOnHCLError (true ),
1815
+ OptionWithDownloads (false ),
1816
+ )
1817
+ require .NoError (t , parser .ParseFS (context .TODO (), "." ))
1818
+
1819
+ modules , _ , err := parser .EvaluateAll (context .TODO ())
1820
+ require .NoError (t , err )
1821
+
1822
+ // modules only have 'parent'. They do not have children, so create
1823
+ // a structure that allows traversal from the root to the leafs.
1824
+ modChildren := make (map [* terraform.Module ][]* terraform.Module )
1825
+ // Keep track of every module that exists
1826
+ modSet := set .New [* terraform.Module ]()
1827
+ var root * terraform.Module
1828
+ for _ , mod := range modules {
1829
+ mod := mod
1830
+ modChildren [mod ] = make ([]* terraform.Module , 0 )
1831
+ modSet .Append (mod )
1832
+
1833
+ if mod .Parent () == nil {
1834
+ // Only 1 root should exist
1835
+ require .Nil (t , root , "root module already set" )
1836
+ root = mod
1837
+ }
1838
+ modChildren [mod .Parent ()] = append (modChildren [mod .Parent ()], mod )
1839
+ }
1840
+
1841
+ type node struct {
1842
+ prefix string
1843
+ modulePath string
1844
+ children []node
1845
+ }
1846
+
1847
+ // expectedTree is the full module tree structure.
1848
+ expectedTree := node {
1849
+ modulePath : "." ,
1850
+ children : []node {
1851
+ {
1852
+ modulePath : "north-america" ,
1853
+ children : []node {
1854
+ {
1855
+ modulePath : "north-america/united-states" ,
1856
+ children : []node {
1857
+ {modulePath : "north-america/united-states/springfield" , prefix : "illinois-" },
1858
+ {modulePath : "north-america/united-states/springfield" , prefix : "idaho-" },
1859
+ {modulePath : "north-america/united-states/new-york" , children : []node {
1860
+ {modulePath : "north-america/united-states/new-york/new-york-city" },
1861
+ }},
1862
+ },
1863
+ },
1864
+ {
1865
+ modulePath : "north-america/canada" ,
1866
+ children : []node {
1867
+ {modulePath : "north-america/canada/springfield" , prefix : "ontario-" },
1868
+ },
1869
+ },
1870
+ },
1871
+ },
1872
+ },
1873
+ }
1874
+
1875
+ var assertChild func (t * testing.T , n node , mod * terraform.Module )
1876
+ assertChild = func (t * testing.T , n node , mod * terraform.Module ) {
1877
+ modSet .Remove (mod )
1878
+ children := modChildren [mod ]
1879
+
1880
+ t .Run (n .modulePath , func (t * testing.T ) {
1881
+ if ! assert .Equal (t , len (n .children ), len (children ), "modChildren count for %s" , n .modulePath ) {
1882
+ return
1883
+ }
1884
+ for _ , child := range children {
1885
+ // Find the child module that we are expecting.
1886
+ idx := slices .IndexFunc (n .children , func (node node ) bool {
1887
+ outputBlocks := child .GetBlocks ().OfType ("output" )
1888
+ outIdx := slices .IndexFunc (outputBlocks , func (outputBlock * terraform.Block ) bool {
1889
+ return outputBlock .Labels ()[0 ] == "name"
1890
+ })
1891
+ if outIdx == - 1 {
1892
+ return false
1893
+ }
1894
+
1895
+ output := outputBlocks [outIdx ]
1896
+ outVal := output .GetAttribute ("value" ).Value ()
1897
+ if ! outVal .Type ().Equals (cty .String ) {
1898
+ return false
1899
+ }
1900
+
1901
+ modName := filepath .Base (node .modulePath )
1902
+ if outVal .AsString () != node .prefix + modName {
1903
+ return false
1904
+ }
1905
+
1906
+ return node .modulePath == child .ModulePath ()
1907
+ })
1908
+ if ! assert .NotEqualf (t , - 1 , idx , "module prefix=%s path=%s not found in %s" , n .prefix , child .ModulePath (), n .modulePath ) {
1909
+ continue
1910
+ }
1911
+
1912
+ assertChild (t , n .children [idx ], child )
1913
+ }
1914
+ })
1915
+
1916
+ }
1917
+
1918
+ assertChild (t , expectedTree , root )
1919
+ // If any module was not asserted, the test will fail. This ensures the
1920
+ // entire module tree is checked.
1921
+ require .Equal (t , 0 , modSet .Size (), "all modules asserted" )
1772
1922
}
1773
1923
1774
1924
func TestCyclicModules (t * testing.T ) {
0 commit comments