From 8f00497e4edbfa4befcba3c56081ede16dbb27d3 Mon Sep 17 00:00:00 2001 From: Xiwen Cheng Date: Thu, 27 Nov 2025 00:56:48 +0100 Subject: [PATCH 1/5] Output app.yaml in model source --- mpr/microflow_test.go | 6 +- mpr/mpr.go | 120 +++++++++++++++++++++++++++--- mpr/mpr_v1_test.go | 4 +- mpr/mpr_v2_test.go | 2 +- resources/modelsource-v1/app.yaml | 88 ++++++++++++++++++++++ resources/modelsource-v2/app.yaml | 83 +++++++++++++++++++++ 6 files changed, 287 insertions(+), 16 deletions(-) create mode 100644 resources/modelsource-v1/app.yaml create mode 100644 resources/modelsource-v2/app.yaml diff --git a/mpr/microflow_test.go b/mpr/microflow_test.go index b910b56..b1272f9 100644 --- a/mpr/microflow_test.go +++ b/mpr/microflow_test.go @@ -13,7 +13,7 @@ import ( func TestMPRMicroflow(t *testing.T) { t.Run("microflow-simple", func(t *testing.T) { - if err := exportUnits("./../resources/app-mpr-v1/App.mpr", "./../tmp", true, "advanced", ""); err != nil { + if _, err := exportUnits("./../resources/app-mpr-v1/App.mpr", "./../tmp", true, "advanced", ""); err != nil { t.Errorf("Failed to export units from MPR file") } @@ -42,7 +42,7 @@ func TestMPRMicroflow(t *testing.T) { } }) t.Run("microflow-with-split", func(t *testing.T) { - if err := exportUnits("./../resources/app-mpr-v1/App.mpr", "./../tmp", true, "advanced", ""); err != nil { + if _, err := exportUnits("./../resources/app-mpr-v1/App.mpr", "./../tmp", true, "advanced", ""); err != nil { t.Errorf("Failed to export units from MPR file") } @@ -71,7 +71,7 @@ func TestMPRMicroflow(t *testing.T) { } }) t.Run("microflow-split-then-merge", func(t *testing.T) { - if err := exportUnits("./../resources/app-mpr-v1/App.mpr", "./../tmp", true, "advanced", ""); err != nil { + if _, err := exportUnits("./../resources/app-mpr-v1/App.mpr", "./../tmp", true, "advanced", ""); err != nil { t.Errorf("Failed to export units from MPR file") } diff --git a/mpr/mpr.go b/mpr/mpr.go index 6a36e21..f365e9e 100644 --- a/mpr/mpr.go +++ b/mpr/mpr.go @@ -64,8 +64,11 @@ func ExportModel(inputDirectory string, outputDirectory string, raw bool, mode s return fmt.Errorf("error exporting metadata: %v", err) } + exportedCount := 0 if filter != "^Metadata$" { - if err := exportUnits(inputDirectory, tmpDir, raw, mode, filter); err != nil { + var err error + exportedCount, err = exportUnits(inputDirectory, tmpDir, raw, mode, filter) + if err != nil { return fmt.Errorf("error exporting units: %v", err) } } @@ -97,6 +100,13 @@ func ExportModel(inputDirectory string, outputDirectory string, raw bool, mode s removeAppstoreModules(outputDirectory, modules) } + // Generate app.yaml with file structure only if documents were exported + if exportedCount > 0 { + if err := generateAppYaml(outputDirectory); err != nil { + return fmt.Errorf("error generating app.yaml: %v", err) + } + } + log.Infof("Completed model export") return nil } @@ -455,21 +465,21 @@ func getMxDocuments(units []MxUnit, folders []MxFolder, mode string) ([]MxDocume return documents, nil } -func exportUnits(inputDirectory string, outputDirectory string, raw bool, mode string, filter string) error { +func exportUnits(inputDirectory string, outputDirectory string, raw bool, mode string, filter string) (int, error) { log.Debugf("Exporting units from %s to %s", inputDirectory, outputDirectory) units, err := getMxUnits(inputDirectory) if err != nil { log.Errorf("Error getting units: %v", err) - return fmt.Errorf("error getting units: %v", err) + return 0, fmt.Errorf("error getting units: %v", err) } folders, err := getMxFolders(units) if err != nil { - return fmt.Errorf("error getting folders: %v", err) + return 0, fmt.Errorf("error getting folders: %v", err) } documents, err := getMxDocuments(units, folders, mode) if err != nil { - return fmt.Errorf("error getting documents: %v", err) + return 0, fmt.Errorf("error getting documents: %v", err) } // Compile the filter regex if provided @@ -477,7 +487,7 @@ func exportUnits(inputDirectory string, outputDirectory string, raw bool, mode s if filter != "" { filterRegex, err = regexp.Compile(filter) if err != nil { - return fmt.Errorf("invalid filter regex pattern: %v", err) + return 0, fmt.Errorf("invalid filter regex pattern: %v", err) } log.Infof("Applying filter: %s", filter) } @@ -513,7 +523,7 @@ func exportUnits(inputDirectory string, outputDirectory string, raw bool, mode s // Validate and adjust path length to prevent exceeding OS limits adjustedPath, adjustedFilename, err := validatePathLength(outputDirectory, sanitizedPath, fname) if err != nil { - return fmt.Errorf("error adjusting path length: %v", err) + return 0, fmt.Errorf("error adjusting path length: %v", err) } directory := filepath.Join(outputDirectory, adjustedPath) @@ -521,7 +531,7 @@ func exportUnits(inputDirectory string, outputDirectory string, raw bool, mode s // ensure directory exists if _, err := os.Stat(directory); os.IsNotExist(err) { if err := os.MkdirAll(directory, 0755); err != nil { - return fmt.Errorf("error creating directory: %v", err) + return 0, fmt.Errorf("error creating directory: %v", err) } } @@ -529,7 +539,7 @@ func exportUnits(inputDirectory string, outputDirectory string, raw bool, mode s err = writeFile(filepath.Join(directory, adjustedFilename), attributes) if err != nil { log.Errorf("Error writing file: %v", err) - return err + return 0, err } exportedCount++ } @@ -538,7 +548,7 @@ func exportUnits(inputDirectory string, outputDirectory string, raw bool, mode s log.Infof("Exported %d documents matching filter (out of %d total)", exportedCount, len(documents)) } - return nil + return exportedCount, nil } @@ -668,3 +678,93 @@ func copyFile(src, dst string, mode os.FileMode) error { return nil } + +// FileNode represents a file or directory in the file structure +type FileNode struct { + Name string `yaml:"name"` + Type string `yaml:"type"` // "file" or "directory" + Path string `yaml:"path,omitempty"` + Content []FileNode `yaml:"content,omitempty"` +} + +// AppStructure represents the entire file structure for app.yaml +type AppStructure struct { + Content []FileNode `yaml:"content"` +} + +// buildFileStructure recursively builds the file structure for a directory +func buildFileStructure(basePath string, currentPath string) (*FileNode, error) { + fullPath := filepath.Join(basePath, currentPath) + info, err := os.Stat(fullPath) + if err != nil { + return nil, fmt.Errorf("error reading path %s: %v", fullPath, err) + } + + relPath := currentPath + if relPath == "" { + relPath = "." + } + + node := &FileNode{ + Name: filepath.Base(fullPath), + Path: relPath, + } + + if info.IsDir() { + node.Type = "directory" + + entries, err := os.ReadDir(fullPath) + if err != nil { + return nil, fmt.Errorf("error reading directory %s: %v", fullPath, err) + } + + for _, entry := range entries { + // Skip app.yaml to avoid self-reference + if entry.Name() == "app.yaml" { + continue + } + childRelPath := filepath.Join(currentPath, entry.Name()) + childNode, err := buildFileStructure(basePath, childRelPath) + if err != nil { + log.Warnf("Error processing %s: %v", childRelPath, err) + continue + } + node.Content = append(node.Content, *childNode) + } + } else { + node.Type = "file" + } + + return node, nil +} + +// generateAppYaml generates an app.yaml file with the file structure of outputDirectory +func generateAppYaml(outputDirectory string) error { + log.Infof("Generating app.yaml with file structure") + + // Build the file structure + rootNode, err := buildFileStructure(outputDirectory, "") + if err != nil { + return fmt.Errorf("error building file structure: %v", err) + } + + // Use the content of the root as the project structure + appStructure := AppStructure{ + Content: rootNode.Content, + } + + // Marshal to YAML + yamlData, err := yaml.Marshal(appStructure) + if err != nil { + return fmt.Errorf("error marshaling app structure to YAML: %v", err) + } + + // Write to app.yaml + appYamlPath := filepath.Join(outputDirectory, "app.yaml") + if err := os.WriteFile(appYamlPath, yamlData, 0644); err != nil { + return fmt.Errorf("error writing app.yaml: %v", err) + } + + log.Infof("Generated app.yaml at %s", appYamlPath) + return nil +} diff --git a/mpr/mpr_v1_test.go b/mpr/mpr_v1_test.go index adc8aba..dbf5d72 100644 --- a/mpr/mpr_v1_test.go +++ b/mpr/mpr_v1_test.go @@ -42,7 +42,7 @@ func TestMPRMetadata(t *testing.T) { func TestMPRUnits(t *testing.T) { t.Run("single-mpr", func(t *testing.T) { - if err := exportUnits("./../resources/app-mpr-v1", "./../tmp", false, "basic", ""); err != nil { + if _, err := exportUnits("./../resources/app-mpr-v1", "./../tmp", false, "basic", ""); err != nil { t.Errorf("Failed to export units from MPR file") } }) @@ -51,7 +51,7 @@ func TestMPRUnits(t *testing.T) { func TestIDAttributesExclusion(t *testing.T) { t.Run("verify-id-attributes-excluded", func(t *testing.T) { // Export units with ID attributes excluded - if err := exportUnits("./../resources/app-mpr-v1", "./../tmp", false, "basic", ""); err != nil { + if _, err := exportUnits("./../resources/app-mpr-v1", "./../tmp", false, "basic", ""); err != nil { t.Errorf("Failed to export units from MPR file: %v", err) return } diff --git a/mpr/mpr_v2_test.go b/mpr/mpr_v2_test.go index 5ec4d07..9df1694 100644 --- a/mpr/mpr_v2_test.go +++ b/mpr/mpr_v2_test.go @@ -39,7 +39,7 @@ func TestMPRV2Metadata(t *testing.T) { func TestMPRV2Units(t *testing.T) { t.Run("single-mpr", func(t *testing.T) { - if err := exportUnits("./../resources/app-mpr-v2", "./../tmp", false, "basic", ""); err != nil { + if _, err := exportUnits("./../resources/app-mpr-v2", "./../tmp", false, "basic", ""); err != nil { t.Errorf("Failed to export units from MPR file") } }) diff --git a/resources/modelsource-v1/app.yaml b/resources/modelsource-v1/app.yaml new file mode 100644 index 0000000..9729fad --- /dev/null +++ b/resources/modelsource-v1/app.yaml @@ -0,0 +1,88 @@ +content: + - name: Metadata.yaml + type: file + path: Metadata.yaml + - name: MyFirstModule + type: directory + path: MyFirstModule + content: + - name: BadPage.Forms$Page.yaml + type: file + path: MyFirstModule/BadPage.Forms$Page.yaml + - name: DomainModels$DomainModel.yaml + type: file + path: MyFirstModule/DomainModels$DomainModel.yaml + - name: Folder + type: directory + path: MyFirstModule/Folder + content: + - name: EnumerationStatus.Enumerations$Enumeration.yaml + type: file + path: MyFirstModule/Folder/EnumerationStatus.Enumerations$Enumeration.yaml + - name: Folder2 + type: directory + path: MyFirstModule/Folder/Folder2 + content: + - name: Page_2.Forms$Page.yaml + type: file + path: MyFirstModule/Folder/Folder2/Page_2.Forms$Page.yaml + - name: Snippet2.Forms$Snippet.yaml + type: file + path: MyFirstModule/Folder/Folder2/Snippet2.Forms$Snippet.yaml + - name: MicroflowComplexSplit.Microflows$Microflow.yaml + type: file + path: MyFirstModule/Folder/MicroflowComplexSplit.Microflows$Microflow.yaml + - name: MicroflowForLoop.Microflows$Microflow.yaml + type: file + path: MyFirstModule/Folder/MicroflowForLoop.Microflows$Microflow.yaml + - name: MicroflowLoop.Microflows$Microflow.yaml + type: file + path: MyFirstModule/Folder/MicroflowLoop.Microflows$Microflow.yaml + - name: MicroflowLoopNested.Microflows$Microflow.yaml + type: file + path: MyFirstModule/Folder/MicroflowLoopNested.Microflows$Microflow.yaml + - name: MicroflowSimple.Microflows$Microflow.yaml + type: file + path: MyFirstModule/Folder/MicroflowSimple.Microflows$Microflow.yaml + - name: MicroflowSplit.Microflows$Microflow.yaml + type: file + path: MyFirstModule/Folder/MicroflowSplit.Microflows$Microflow.yaml + - name: MicroflowSplitThenMerge.Microflows$Microflow.yaml + type: file + path: MyFirstModule/Folder/MicroflowSplitThenMerge.Microflows$Microflow.yaml + - name: Page.Forms$Page.yaml + type: file + path: MyFirstModule/Folder/Page.Forms$Page.yaml + - name: Home_Web.Forms$Page.yaml + type: file + path: MyFirstModule/Home_Web.Forms$Page.yaml + - name: Images.Images$ImageCollection.yaml + type: file + path: MyFirstModule/Images.Images$ImageCollection.yaml + - name: MyFirstLogic.Microflows$Microflow.yaml + type: file + path: MyFirstModule/MyFirstLogic.Microflows$Microflow.yaml + - name: Projects$ModuleSettings.yaml + type: file + path: MyFirstModule/Projects$ModuleSettings.yaml + - name: Security$ModuleSecurity.yaml + type: file + path: MyFirstModule/Security$ModuleSecurity.yaml + - name: VA_Age.Microflows$Microflow.yaml + type: file + path: MyFirstModule/VA_Age.Microflows$Microflow.yaml + - name: headings.Forms$Page.yaml + type: file + path: MyFirstModule/headings.Forms$Page.yaml + - name: Navigation$NavigationDocument.yaml + type: file + path: Navigation$NavigationDocument.yaml + - name: Security$ProjectSecurity.yaml + type: file + path: Security$ProjectSecurity.yaml + - name: Settings$ProjectSettings.yaml + type: file + path: Settings$ProjectSettings.yaml + - name: Texts$SystemTextCollection.yaml + type: file + path: Texts$SystemTextCollection.yaml diff --git a/resources/modelsource-v2/app.yaml b/resources/modelsource-v2/app.yaml new file mode 100644 index 0000000..3899973 --- /dev/null +++ b/resources/modelsource-v2/app.yaml @@ -0,0 +1,83 @@ +content: + - name: Metadata.yaml + type: file + path: Metadata.yaml + - name: Module2 + type: directory + path: Module2 + content: + - name: DomainModels$DomainModel.yaml + type: file + path: Module2/DomainModels$DomainModel.yaml + - name: Folder_ + type: directory + path: Module2/Folder_ + content: + - name: Constant.Constants$Constant.yaml + type: file + path: Module2/Folder_/Constant.Constants$Constant.yaml + - name: Folder_test3 + type: directory + path: Module2/Folder_test3 + content: + - name: Constant_4.Constants$Constant.yaml + type: file + path: Module2/Folder_test3/Constant_4.Constants$Constant.yaml + - name: Folder_testverylonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglo_7078103c + type: directory + path: Module2/Folder_testverylonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglo_7078103c + content: + - name: F_b79b72e3 + type: directory + path: Module2/Folder_testverylonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglo_7078103c/F_b79b72e3 + content: + - name: Constant_3.Constants$Constant.yaml + type: file + path: Module2/Folder_testverylonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglo_7078103c/F_b79b72e3/Constant_3.Constants$Constant.yaml + - name: Projects$ModuleSettings.yaml + type: file + path: Module2/Projects$ModuleSettings.yaml + - name: Security$ModuleSecurity.yaml + type: file + path: Module2/Security$ModuleSecurity.yaml + - name: _ + type: directory + path: Module2/_ + content: + - name: Constant_2.Constants$Constant.yaml + type: file + path: Module2/_/Constant_2.Constants$Constant.yaml + - name: MyFirstModule + type: directory + path: MyFirstModule + content: + - name: DomainModels$DomainModel.yaml + type: file + path: MyFirstModule/DomainModels$DomainModel.yaml + - name: Home_Web.Forms$Page.yaml + type: file + path: MyFirstModule/Home_Web.Forms$Page.yaml + - name: Images.Images$ImageCollection.yaml + type: file + path: MyFirstModule/Images.Images$ImageCollection.yaml + - name: MyFirstLogic.Microflows$Microflow.yaml + type: file + path: MyFirstModule/MyFirstLogic.Microflows$Microflow.yaml + - name: Projects$ModuleSettings.yaml + type: file + path: MyFirstModule/Projects$ModuleSettings.yaml + - name: Security$ModuleSecurity.yaml + type: file + path: MyFirstModule/Security$ModuleSecurity.yaml + - name: Navigation$NavigationDocument.yaml + type: file + path: Navigation$NavigationDocument.yaml + - name: Security$ProjectSecurity.yaml + type: file + path: Security$ProjectSecurity.yaml + - name: Settings$ProjectSettings.yaml + type: file + path: Settings$ProjectSettings.yaml + - name: Texts$SystemTextCollection.yaml + type: file + path: Texts$SystemTextCollection.yaml From 18ea1071e01a1b71d663dad5668429d370126abf Mon Sep 17 00:00:00 2001 From: Xiwen Cheng Date: Thu, 27 Nov 2025 01:23:03 +0100 Subject: [PATCH 2/5] ensure clean state --- mpr/mpr.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/mpr/mpr.go b/mpr/mpr.go index f365e9e..3c028f1 100644 --- a/mpr/mpr.go +++ b/mpr/mpr.go @@ -89,6 +89,16 @@ func ExportModel(inputDirectory string, outputDirectory string, raw bool, mode s return fmt.Errorf("destination directory does not exist: %v", err) } + // Remove output directory to ensure clean state + if err := os.RemoveAll(outputDirectory); err != nil { + return fmt.Errorf("error removing output directory: %v", err) + } + + // Recreate output directory + if err := os.MkdirAll(outputDirectory, 0755); err != nil { + return fmt.Errorf("error creating output directory: %v", err) + } + // copy tmp directory to output directory err = syncDirectories(tmpDir, outputDirectory) if err != nil { From 715b26cc88222ce6c5fec0c3be3e470d48a8371e Mon Sep 17 00:00:00 2001 From: Xiwen Cheng Date: Thu, 27 Nov 2025 22:53:27 +0100 Subject: [PATCH 3/5] new file and directory name truncate strategy --- mpr/mpr.go | 60 ++++++++++++++++++++++++++++++--------------- mpr/mpr_test.go | 65 ++++++++++++++++++++++++++++++++----------------- 2 files changed, 82 insertions(+), 43 deletions(-) diff --git a/mpr/mpr.go b/mpr/mpr.go index 3c028f1..82a0b6b 100644 --- a/mpr/mpr.go +++ b/mpr/mpr.go @@ -27,8 +27,8 @@ const ( // Maximum safe path length for generated content MaxSafePath = MaxPathLength - SafePathBuffer // 200 chars - // Per-component limit (255 is filesystem limit, but we use lower for safety) - MaxComponentLength = 100 + // Per-component limit - filename or foldername can be at most 50 characters long + MaxComponentLength = 50 ) func ExportModel(inputDirectory string, outputDirectory string, raw bool, mode string, appstore bool, filter string) error { @@ -371,26 +371,46 @@ func sanitizePath(path string) string { } // truncatePathComponent truncates a path component to maxLen while maintaining uniqueness -// If truncation is needed, it appends a hash to ensure uniqueness +// If truncation is needed, uses format: first 20 chars + "_TRUNCATED_" + 5 char hash + "_" + last 13 chars func truncatePathComponent(name string, maxLen int) string { if len(name) <= maxLen { return name } - // Create a short hash of the full name for uniqueness + // Create a 5 character hash of the full name for uniqueness hash := sha256.Sum256([]byte(name)) - hashStr := hex.EncodeToString(hash[:])[:8] // Use first 8 chars of hash + hashStr := hex.EncodeToString(hash[:])[:5] // Use first 5 chars of hash + + // Format: first 20 chars + "_TRUNCATED_" + 5 char hash + "_" + last 13 chars + // Total length: 20 + 11 + 5 + 1 + 13 = 50 characters + const prefixLen = 20 + const suffixLen = 13 + const truncateMarker = "_TRUNCATED_" + + // If name is too short to extract meaningful prefix/suffix, adjust accordingly + if len(name) < prefixLen+suffixLen { + // For very short names that still exceed maxLen (edge case) + if len(name) <= maxLen { + return name + } + // Use what we have - take as much prefix as possible + availablePrefix := min(prefixLen, len(name)) + return name[:availablePrefix] + truncateMarker + hashStr + } - // Reserve space for hash and separator - maxTextLen := maxLen - len(hashStr) - 1 // -1 for underscore + // Standard truncation: first 20 + marker + hash + _ + last 13 + prefix := name[:prefixLen] + suffix := name[len(name)-suffixLen:] - if maxTextLen < 1 { - // If maxLen is too small, just use the hash - return hashStr[:maxLen] - } + return prefix + truncateMarker + hashStr + "_" + suffix +} - // Truncate and append hash - return name[:maxTextLen] + "_" + hashStr +// min returns the minimum of two integers +func min(a, b int) int { + if a < b { + return a + } + return b } // max returns the maximum of two integers @@ -421,21 +441,21 @@ func validatePathLength(basePath string, relativePath string, filename string) ( // Try to shorten components from the end (deepest folders) for i := len(components) - 1; i >= 0 && excess > 0; i-- { oldLen := len(components[i]) - targetLen := max(10, oldLen-excess) // Keep at least 10 chars if possible - if oldLen > targetLen { - components[i] = truncatePathComponent(components[i], targetLen) + // Only truncate if component is longer than MaxComponentLength + if oldLen > MaxComponentLength { + components[i] = truncatePathComponent(components[i], MaxComponentLength) excess -= (oldLen - len(components[i])) } } // If still too long, truncate the filename if excess > 0 { - maxFilenameLen := len(filename) - excess - 10 // Reserve 10 chars minimum - if maxFilenameLen < 10 { - maxFilenameLen = 10 + oldFilenameLen := len(filename) + if oldFilenameLen > MaxComponentLength { + filename = truncatePathComponent(filename, MaxComponentLength) + excess -= (oldFilenameLen - len(filename)) } - filename = truncatePathComponent(filename, maxFilenameLen) } newRelativePath := filepath.Join(components...) diff --git a/mpr/mpr_test.go b/mpr/mpr_test.go index a1b7857..ee11fc6 100644 --- a/mpr/mpr_test.go +++ b/mpr/mpr_test.go @@ -3,6 +3,7 @@ package mpr import ( "os" "path/filepath" + "strings" "testing" "gopkg.in/yaml.v3" @@ -276,28 +277,44 @@ func TestSanitizePath(t *testing.T) { func TestTruncatePathComponent(t *testing.T) { tests := []struct { - name string - input string - maxLen int - expectLen int + name string + input string + maxLen int + expectLen int + shouldContain []string + shouldNotExceed int }{ { - name: "short name no truncation", - input: "ShortName", - maxLen: 50, - expectLen: 9, + name: "short name no truncation", + input: "ShortName", + maxLen: 50, + expectLen: 9, + shouldContain: []string{"ShortName"}, + shouldNotExceed: 50, }, { - name: "long name truncation", - input: "VeryLongFolderNameThatExceedsTheMaximumLengthAllowed", - maxLen: 20, - expectLen: 20, + name: "long name truncation", + input: "VeryLongFolderNameThatExceedsTheMaximumLengthAllowed", + maxLen: 50, + expectLen: 50, + shouldContain: []string{"VeryLongFolderNameTh", "_TRUNCATED_", "_LengthAllowed"}, // first 20 + _TRUNCATED_ + hash + _ + last 13 + shouldNotExceed: 50, }, { - name: "exact length", - input: "ExactLength", - maxLen: 11, - expectLen: 11, + name: "exact length", + input: "ExactLength", + maxLen: 11, + expectLen: 11, + shouldContain: []string{"ExactLength"}, + shouldNotExceed: 11, + }, + { + name: "very long name", + input: "ThisIsAVeryVeryVeryVeryVeryLongFileNameThatDefinitelyExceedsTheLimit", + maxLen: 50, + expectLen: 50, + shouldContain: []string{"ThisIsAVeryVeryVeryV", "_TRUNCATED_", "_ceedsTheLimit"}, // first 20 + _TRUNCATED_ + hash + _ + last 13 + shouldNotExceed: 50, }, } @@ -305,12 +322,14 @@ func TestTruncatePathComponent(t *testing.T) { t.Run(tt.name, func(t *testing.T) { result := truncatePathComponent(tt.input, tt.maxLen) if len(result) != tt.expectLen { - t.Errorf("truncatePathComponent() length = %v, want %v", len(result), tt.expectLen) + t.Errorf("truncatePathComponent() length = %v, want %v, result = %v", len(result), tt.expectLen, result) + } + if len(result) > tt.shouldNotExceed { + t.Errorf("truncatePathComponent() length %v exceeds max %v", len(result), tt.shouldNotExceed) } - if len(tt.input) > tt.maxLen { - // Should contain hash for uniqueness - if len(result) < tt.maxLen-8 { - t.Errorf("truncatePathComponent() should contain hash for uniqueness") + for _, substring := range tt.shouldContain { + if !strings.Contains(result, substring) { + t.Errorf("truncatePathComponent() result %v should contain %v", result, substring) } } }) @@ -833,9 +852,9 @@ func TestSanitizePathComponent(t *testing.T) { expected: "Folder_test", }, { - name: "long folder name with slash", + name: "long folder name with slash - should be truncated", input: "Folder/testverylonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglong", - expected: "Folder_testverylonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglong", + expected: "Folder_testverylongl_TRUNCATED_80d74_glonglonglong", // Truncated to 50 chars }, { name: "multiple slashes", From 0a49bb1d79415305e41efef92e65e772a5ea7122 Mon Sep 17 00:00:00 2001 From: Xiwen Cheng Date: Thu, 27 Nov 2025 22:54:16 +0100 Subject: [PATCH 4/5] refresh modelsources --- .../Constant_3.Constants$Constant.yaml | 0 resources/modelsource-v2/app.yaml | 10 +++++----- 2 files changed, 5 insertions(+), 5 deletions(-) rename resources/modelsource-v2/Module2/{Folder_testverylonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglo_7078103c/F_b79b72e3 => Folder_testverylongl_TRUNCATED_70781_longlong name/Folder testverylongl_TRUNCATED_82413_glonglonglong}/Constant_3.Constants$Constant.yaml (100%) diff --git a/resources/modelsource-v2/Module2/Folder_testverylonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglo_7078103c/F_b79b72e3/Constant_3.Constants$Constant.yaml b/resources/modelsource-v2/Module2/Folder_testverylongl_TRUNCATED_70781_longlong name/Folder testverylongl_TRUNCATED_82413_glonglonglong/Constant_3.Constants$Constant.yaml similarity index 100% rename from resources/modelsource-v2/Module2/Folder_testverylonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglo_7078103c/F_b79b72e3/Constant_3.Constants$Constant.yaml rename to resources/modelsource-v2/Module2/Folder_testverylongl_TRUNCATED_70781_longlong name/Folder testverylongl_TRUNCATED_82413_glonglonglong/Constant_3.Constants$Constant.yaml diff --git a/resources/modelsource-v2/app.yaml b/resources/modelsource-v2/app.yaml index 3899973..b42f73f 100644 --- a/resources/modelsource-v2/app.yaml +++ b/resources/modelsource-v2/app.yaml @@ -23,17 +23,17 @@ content: - name: Constant_4.Constants$Constant.yaml type: file path: Module2/Folder_test3/Constant_4.Constants$Constant.yaml - - name: Folder_testverylonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglo_7078103c + - name: Folder_testverylongl_TRUNCATED_70781_longlong name type: directory - path: Module2/Folder_testverylonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglo_7078103c + path: Module2/Folder_testverylongl_TRUNCATED_70781_longlong name content: - - name: F_b79b72e3 + - name: Folder testverylongl_TRUNCATED_82413_glonglonglong type: directory - path: Module2/Folder_testverylonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglo_7078103c/F_b79b72e3 + path: Module2/Folder_testverylongl_TRUNCATED_70781_longlong name/Folder testverylongl_TRUNCATED_82413_glonglonglong content: - name: Constant_3.Constants$Constant.yaml type: file - path: Module2/Folder_testverylonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglo_7078103c/F_b79b72e3/Constant_3.Constants$Constant.yaml + path: Module2/Folder_testverylongl_TRUNCATED_70781_longlong name/Folder testverylongl_TRUNCATED_82413_glonglonglong/Constant_3.Constants$Constant.yaml - name: Projects$ModuleSettings.yaml type: file path: Module2/Projects$ModuleSettings.yaml From 05be7f780240f3c7febb7d68889e1466d4eb8f83 Mon Sep 17 00:00:00 2001 From: Xiwen Cheng Date: Thu, 27 Nov 2025 23:05:09 +0100 Subject: [PATCH 5/5] add test case for generateAppYaml --- mpr/mpr_test.go | 340 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 340 insertions(+) diff --git a/mpr/mpr_test.go b/mpr/mpr_test.go index ee11fc6..bca9930 100644 --- a/mpr/mpr_test.go +++ b/mpr/mpr_test.go @@ -999,3 +999,343 @@ func containsSanitizedName(path, expected string) bool { // Check if the key part (folder name with underscore instead of slash) is present return len(path) > 0 && len(expected) > 0 } + +func TestGenerateAppYaml(t *testing.T) { + // Create temporary directory for testing + tmpDir, err := os.MkdirTemp("", "mpr-test-appyaml-*") + if err != nil { + t.Fatalf("Failed to create temp directory: %v", err) + } + defer os.RemoveAll(tmpDir) + + // Create test directory structure + testStructure := map[string]bool{ + "Metadata.yaml": false, // file + "MyModule": true, // directory + "MyModule/DomainModel.yaml": false, + "MyModule/SubFolder": true, + "MyModule/SubFolder/Page.yaml": false, + "Navigation.yaml": false, + "Settings.yaml": false, + } + + // Create test files and directories + for path, isDir := range testStructure { + fullPath := filepath.Join(tmpDir, path) + if isDir { + if err := os.MkdirAll(fullPath, 0755); err != nil { + t.Fatalf("Failed to create directory %s: %v", path, err) + } + } else { + // Ensure parent directory exists + parentDir := filepath.Dir(fullPath) + if err := os.MkdirAll(parentDir, 0755); err != nil { + t.Fatalf("Failed to create parent directory for %s: %v", path, err) + } + // Create file + if err := os.WriteFile(fullPath, []byte("test content"), 0644); err != nil { + t.Fatalf("Failed to create file %s: %v", path, err) + } + } + } + + // Generate app.yaml + err = generateAppYaml(tmpDir) + if err != nil { + t.Fatalf("generateAppYaml() unexpected error: %v", err) + } + + // Verify app.yaml was created + appYamlPath := filepath.Join(tmpDir, "app.yaml") + if _, err := os.Stat(appYamlPath); os.IsNotExist(err) { + t.Errorf("generateAppYaml() did not create app.yaml file") + return + } + + // Read and parse app.yaml + yamlContent, err := os.ReadFile(appYamlPath) + if err != nil { + t.Fatalf("Failed to read app.yaml: %v", err) + } + + var appStructure AppStructure + if err := yaml.Unmarshal(yamlContent, &appStructure); err != nil { + t.Fatalf("Failed to unmarshal app.yaml: %v", err) + } + + // Validate structure is not empty + if len(appStructure.Content) == 0 { + t.Errorf("generateAppYaml() produced empty content array") + return + } + + // Helper function to find a node by name + findNode := func(nodes []FileNode, name string) *FileNode { + for i := range nodes { + if nodes[i].Name == name { + return &nodes[i] + } + } + return nil + } + + // Verify expected files exist in the structure + tests := []struct { + name string + nodePath []string // path to the node (e.g., ["MyModule", "SubFolder"]) + expected struct { + name string + typ string + } + }{ + { + name: "Metadata.yaml exists", + nodePath: []string{}, + expected: struct { + name string + typ string + }{name: "Metadata.yaml", typ: "file"}, + }, + { + name: "MyModule directory exists", + nodePath: []string{}, + expected: struct { + name string + typ string + }{name: "MyModule", typ: "directory"}, + }, + { + name: "DomainModel.yaml in MyModule", + nodePath: []string{"MyModule"}, + expected: struct { + name string + typ string + }{name: "DomainModel.yaml", typ: "file"}, + }, + { + name: "SubFolder in MyModule", + nodePath: []string{"MyModule"}, + expected: struct { + name string + typ string + }{name: "SubFolder", typ: "directory"}, + }, + { + name: "Page.yaml in SubFolder", + nodePath: []string{"MyModule", "SubFolder"}, + expected: struct { + name string + typ string + }{name: "Page.yaml", typ: "file"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + currentNodes := appStructure.Content + + // Navigate to the target level + for _, pathSegment := range tt.nodePath { + node := findNode(currentNodes, pathSegment) + if node == nil { + t.Fatalf("Could not find path segment '%s' in structure", pathSegment) + } + currentNodes = node.Content + } + + // Find the expected node + node := findNode(currentNodes, tt.expected.name) + if node == nil { + t.Errorf("Expected node '%s' not found in structure", tt.expected.name) + return + } + + if node.Type != tt.expected.typ { + t.Errorf("Node '%s' has type '%s', expected '%s'", tt.expected.name, node.Type, tt.expected.typ) + } + + // Verify path is set correctly + if node.Path == "" { + t.Errorf("Node '%s' has empty path", tt.expected.name) + } + }) + } + + // Verify app.yaml is not included in its own structure + appYamlNode := findNode(appStructure.Content, "app.yaml") + if appYamlNode != nil { + t.Errorf("generateAppYaml() included app.yaml in its own structure") + } + + // Verify directory nodes have content + myModuleNode := findNode(appStructure.Content, "MyModule") + if myModuleNode != nil && myModuleNode.Type == "directory" { + if len(myModuleNode.Content) == 0 { + t.Errorf("Directory node 'MyModule' has no content") + } + } +} + +func TestBuildFileStructure(t *testing.T) { + // Create temporary directory for testing + tmpDir, err := os.MkdirTemp("", "mpr-test-buildstructure-*") + if err != nil { + t.Fatalf("Failed to create temp directory: %v", err) + } + defer os.RemoveAll(tmpDir) + + tests := []struct { + name string + setup func() error // function to set up test structure + currentPath string + expectError bool + validate func(*testing.T, *FileNode) // function to validate the result + }{ + { + name: "single file", + setup: func() error { + return os.WriteFile(filepath.Join(tmpDir, "test.yaml"), []byte("test"), 0644) + }, + currentPath: "test.yaml", + expectError: false, + validate: func(t *testing.T, node *FileNode) { + if node.Type != "file" { + t.Errorf("Expected type 'file', got '%s'", node.Type) + } + if node.Name != "test.yaml" { + t.Errorf("Expected name 'test.yaml', got '%s'", node.Name) + } + }, + }, + { + name: "directory with files", + setup: func() error { + dirPath := filepath.Join(tmpDir, "testdir") + if err := os.MkdirAll(dirPath, 0755); err != nil { + return err + } + return os.WriteFile(filepath.Join(dirPath, "file.yaml"), []byte("test"), 0644) + }, + currentPath: "testdir", + expectError: false, + validate: func(t *testing.T, node *FileNode) { + if node.Type != "directory" { + t.Errorf("Expected type 'directory', got '%s'", node.Type) + } + if node.Name != "testdir" { + t.Errorf("Expected name 'testdir', got '%s'", node.Name) + } + if len(node.Content) != 1 { + t.Errorf("Expected 1 child node, got %d", len(node.Content)) + } + if len(node.Content) > 0 && node.Content[0].Name != "file.yaml" { + t.Errorf("Expected child 'file.yaml', got '%s'", node.Content[0].Name) + } + }, + }, + { + name: "empty directory", + setup: func() error { + return os.MkdirAll(filepath.Join(tmpDir, "emptydir"), 0755) + }, + currentPath: "emptydir", + expectError: false, + validate: func(t *testing.T, node *FileNode) { + if node.Type != "directory" { + t.Errorf("Expected type 'directory', got '%s'", node.Type) + } + if len(node.Content) != 0 { + t.Errorf("Expected 0 child nodes, got %d", len(node.Content)) + } + }, + }, + { + name: "non-existent path", + setup: func() error { return nil }, + currentPath: "nonexistent", + expectError: true, + validate: func(t *testing.T, node *FileNode) {}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Set up test structure + if err := tt.setup(); err != nil { + t.Fatalf("Setup failed: %v", err) + } + + // Build file structure + node, err := buildFileStructure(tmpDir, tt.currentPath) + + if tt.expectError { + if err == nil { + t.Errorf("buildFileStructure() expected error but got none") + } + } else { + if err != nil { + t.Errorf("buildFileStructure() unexpected error: %v", err) + } else if node != nil { + tt.validate(t, node) + } + } + }) + } +} + +func TestGenerateAppYamlExcludesItself(t *testing.T) { + // Create temporary directory for testing + tmpDir, err := os.MkdirTemp("", "mpr-test-selfexclude-*") + if err != nil { + t.Fatalf("Failed to create temp directory: %v", err) + } + defer os.RemoveAll(tmpDir) + + // Create a test file + testFile := filepath.Join(tmpDir, "test.yaml") + if err := os.WriteFile(testFile, []byte("test content"), 0644); err != nil { + t.Fatalf("Failed to create test file: %v", err) + } + + // Create an existing app.yaml that should be excluded + existingAppYaml := filepath.Join(tmpDir, "app.yaml") + if err := os.WriteFile(existingAppYaml, []byte("old content"), 0644); err != nil { + t.Fatalf("Failed to create existing app.yaml: %v", err) + } + + // Generate new app.yaml + err = generateAppYaml(tmpDir) + if err != nil { + t.Fatalf("generateAppYaml() unexpected error: %v", err) + } + + // Read and parse the generated app.yaml + yamlContent, err := os.ReadFile(existingAppYaml) + if err != nil { + t.Fatalf("Failed to read app.yaml: %v", err) + } + + var appStructure AppStructure + if err := yaml.Unmarshal(yamlContent, &appStructure); err != nil { + t.Fatalf("Failed to unmarshal app.yaml: %v", err) + } + + // Verify app.yaml is not included in its own structure + for _, node := range appStructure.Content { + if node.Name == "app.yaml" { + t.Errorf("app.yaml should not be included in its own structure") + } + } + + // Verify test.yaml is included + foundTestYaml := false + for _, node := range appStructure.Content { + if node.Name == "test.yaml" { + foundTestYaml = true + break + } + } + if !foundTestYaml { + t.Errorf("test.yaml should be included in app.yaml structure") + } +}