From 3f93352965638e1793fbbd4f96a226b351b4f664 Mon Sep 17 00:00:00 2001 From: Xiwen Cheng Date: Fri, 24 Jul 2026 17:48:28 +0200 Subject: [PATCH 1/3] track original model names --- mpr/export_stream.go | 54 ++++- mpr/microflow_test.go | 8 +- mpr/mpr.go | 220 +++++++++++++++++---- mpr/mpr_test.go | 443 ++++++++++++++++-------------------------- mpr/mpr_v1_test.go | 4 +- mpr/mpr_v2_test.go | 2 +- mpr/types.go | 11 +- 7 files changed, 404 insertions(+), 338 deletions(-) diff --git a/mpr/export_stream.go b/mpr/export_stream.go index b57ddb9..98896e2 100644 --- a/mpr/export_stream.go +++ b/mpr/export_stream.go @@ -25,12 +25,13 @@ var documentContainmentTypes = map[string]struct{}{ } type exportDocumentDescriptor struct { - UnitID string - Name string - Type string - ContainerID string - Path string - ContentsHash string + UnitID string + Name string + Type string + ContainerID string + Path string // sanitized on-disk folder path + OriginalFolderPath string // unsanitized Mendix folder path + ContentsHash string } type cachedUnitContent struct { @@ -47,9 +48,39 @@ type exportPlan struct { manifest *exportManifest manifestPath string manifestMu sync.RWMutex + pathMap map[string]string // disk relative path -> original relative path + pathMapMu sync.Mutex Close func() error } +func (p *exportPlan) recordOriginalPath(diskRel, originalRel string) { + if p == nil || diskRel == "" { + return + } + p.pathMapMu.Lock() + defer p.pathMapMu.Unlock() + if p.pathMap == nil { + p.pathMap = make(map[string]string) + } + p.pathMap[filepath.ToSlash(diskRel)] = filepath.ToSlash(originalRel) +} + +func (p *exportPlan) pathMapSnapshot() map[string]string { + if p == nil { + return nil + } + p.pathMapMu.Lock() + defer p.pathMapMu.Unlock() + if len(p.pathMap) == 0 { + return nil + } + out := make(map[string]string, len(p.pathMap)) + for k, v := range p.pathMap { + out[k] = v + } + return out +} + func (p *exportPlan) loadDocument(unitID string) (bson.M, error) { p.unitCacheMu.Lock() if cached, ok := p.unitCache[unitID]; ok { @@ -152,6 +183,7 @@ func buildExportPlanV1(mprPath string) (*exportPlan, error) { connectFolderParents(folders) for i := range documents { documents[i].Path = getMxDocumentPath(documents[i].ContainerID, folders) + documents[i].OriginalFolderPath = getMxDocumentOriginalPath(documents[i].ContainerID, folders) if cached, ok := unitCache[documents[i].UnitID]; ok { documents[i].ContentsHash = cached.ContentsHash } @@ -162,6 +194,7 @@ func buildExportPlanV1(mprPath string) (*exportPlan, error) { Modules: modules, Documents: documents, unitCache: unitCache, + pathMap: make(map[string]string), Close: func() error { return nil }, @@ -259,6 +292,9 @@ func buildExportPlanV2(inputDirectory string, mprPath string) (*exportPlan, erro if documents[i].Path == "" { documents[i].Path = getMxDocumentPath(documents[i].ContainerID, folders) } + if documents[i].OriginalFolderPath == "" { + documents[i].OriginalFolderPath = getMxDocumentOriginalPath(documents[i].ContainerID, folders) + } } log.Debugf("Built export plan from SQLite: %d modules, %d documents (%d structure units cached)", @@ -271,6 +307,7 @@ func buildExportPlanV2(inputDirectory string, mprPath string) (*exportPlan, erro mxunitPaths: mxunitPaths, manifest: manifest, manifestPath: manifestPath, + pathMap: make(map[string]string), Close: func() error { return nil }, @@ -532,6 +569,9 @@ func exportDocument(plan *exportPlan, document exportDocumentDescriptor, outputD return false, err } else if skipped { log.Debugf("Skipping unchanged document '%s'", doc.Name) + if entry, ok := plan.lookupManifestEntry(doc.UnitID, doc.ContentsHash); ok && entry.RelativePath != "" { + plan.recordOriginalPath(entry.RelativePath, originalDocumentRelativePath(doc.OriginalFolderPath, doc.Name, doc.Type)) + } return true, nil } @@ -539,6 +579,7 @@ func exportDocument(plan *exportPlan, document exportDocumentDescriptor, outputD return false, err } else if ok { plan.recordManifestEntry(doc.UnitID, manifestEntryForDocument(doc, relPath, plan.mxunitPaths[doc.UnitID])) + plan.recordOriginalPath(relPath, originalDocumentRelativePath(doc.OriginalFolderPath, doc.Name, doc.Type)) return true, nil } @@ -559,6 +600,7 @@ func exportDocument(plan *exportPlan, document exportDocumentDescriptor, outputD return false, err } plan.recordManifestEntry(doc.UnitID, manifestEntryForDocument(doc, relPath, plan.mxunitPaths[doc.UnitID])) + plan.recordOriginalPath(relPath, originalDocumentRelativePath(doc.OriginalFolderPath, doc.Name, doc.Type)) return true, nil } diff --git a/mpr/microflow_test.go b/mpr/microflow_test.go index c8f8706..ece8af2 100644 --- a/mpr/microflow_test.go +++ b/mpr/microflow_test.go @@ -11,7 +11,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, ""); err != nil { + if _, _, err := exportUnits("./../resources/app-mpr-v1/App.mpr", "./../tmp", true, ""); err != nil { t.Errorf("Failed to export units from MPR file") } @@ -42,7 +42,7 @@ func TestMPRMicroflow(t *testing.T) { } }) t.Run("microflow-simple-has-pseudocode", func(t *testing.T) { - if _, err := exportUnits("./../resources/app-mpr-v1/App.mpr", "./../tmp", true, "MicroflowSimple"); err != nil { + if _, _, err := exportUnits("./../resources/app-mpr-v1/App.mpr", "./../tmp", true, "MicroflowSimple"); err != nil { t.Errorf("Failed to export units from MPR file") } @@ -66,7 +66,7 @@ func TestMPRMicroflow(t *testing.T) { } }) t.Run("microflow-with-split-has-pseudocode", func(t *testing.T) { - if _, err := exportUnits("./../resources/app-mpr-v1/App.mpr", "./../tmp", true, ""); err != nil { + if _, _, err := exportUnits("./../resources/app-mpr-v1/App.mpr", "./../tmp", true, ""); err != nil { t.Errorf("Failed to export units from MPR file") } @@ -92,7 +92,7 @@ func TestMPRMicroflow(t *testing.T) { } }) t.Run("microflow-split-then-merge-has-pseudocode", func(t *testing.T) { - if _, err := exportUnits("./../resources/app-mpr-v1/App.mpr", "./../tmp", true, ""); err != nil { + if _, _, err := exportUnits("./../resources/app-mpr-v1/App.mpr", "./../tmp", true, ""); err != nil { t.Errorf("Failed to export units from MPR file") } diff --git a/mpr/mpr.go b/mpr/mpr.go index 2c82598..7510a69 100644 --- a/mpr/mpr.go +++ b/mpr/mpr.go @@ -109,7 +109,7 @@ func ExportModel(inputDirectory string, outputDirectory string, raw bool, appsto } if exportedCount > 0 { - if err := generateAppYaml(outputDirectory); err != nil { + if err := generateAppYaml(outputDirectory, plan.pathMapSnapshot()); err != nil { return fmt.Errorf("error generating app.yaml: %v", err) } } @@ -300,6 +300,47 @@ func getMxDocumentPath(containerID string, folders []MxFolder) string { return "" } +func getMxDocumentOriginalPathRecursive(folder MxFolder, depth int) string { + if depth == 0 { + return "" + } + if folder.Parent == nil { + return folder.Name + } + parent := getMxDocumentOriginalPathRecursive(*folder.Parent, depth-1) + if parent == "" { + return folder.Name + } + if folder.Name == "" { + return parent + } + return filepath.Join(parent, folder.Name) +} + +func getMxDocumentOriginalPath(containerID string, folders []MxFolder) string { + for _, folder := range folders { + if folder.ID == containerID { + return getMxDocumentOriginalPathRecursive(folder, 10) + } + } + return "" +} + +func originalFilename(name, typ string) string { + if name == "" { + return typ + ".yaml" + } + return name + "." + typ + ".yaml" +} + +func originalDocumentRelativePath(originalFolderPath, name, typ string) string { + fname := originalFilename(name, typ) + if originalFolderPath == "" { + return filepath.ToSlash(fname) + } + return filepath.ToSlash(filepath.Join(originalFolderPath, fname)) +} + // sanitizePathComponent sanitizes a single path component (folder or file name) by replacing // characters that are invalid in file systems with underscores func sanitizePathComponent(name string) string { @@ -485,11 +526,12 @@ func getMxDocuments(units []MxUnit, folders []MxFolder) ([]MxDocument, error) { } myDocument := MxDocument{ - Name: name, - Type: unit.Contents["$Type"].(string), - Path: getMxDocumentPath(unit.ContainerID, folders), - Attributes: unit.Contents, - ContentsHash: unit.ContentsHash, + Name: name, + Type: unit.Contents["$Type"].(string), + Path: getMxDocumentPath(unit.ContainerID, folders), + OriginalFolderPath: getMxDocumentOriginalPath(unit.ContainerID, folders), + Attributes: unit.Contents, + ContentsHash: unit.ContentsHash, } if unit.Contents["$Type"] == microflowDocumentType { @@ -502,25 +544,25 @@ func getMxDocuments(units []MxUnit, folders []MxFolder) ([]MxDocument, error) { return documents, nil } -func exportUnits(inputDirectory string, outputDirectory string, raw bool, filter string) (int, error) { +func exportUnits(inputDirectory string, outputDirectory string, raw bool, filter string) (int, map[string]string, 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 0, fmt.Errorf("error getting units: %v", err) + return 0, nil, fmt.Errorf("error getting units: %v", err) } return exportUnitsFromLoadedUnits(units, outputDirectory, raw, filter) } -func exportUnitsFromLoadedUnits(units []MxUnit, outputDirectory string, raw bool, filter string) (int, error) { +func exportUnitsFromLoadedUnits(units []MxUnit, outputDirectory string, raw bool, filter string) (int, map[string]string, error) { folders, err := getMxFolders(units) if err != nil { - return 0, fmt.Errorf("error getting folders: %v", err) + return 0, nil, fmt.Errorf("error getting folders: %v", err) } documents, err := getMxDocuments(units, folders) if err != nil { - return 0, fmt.Errorf("error getting documents: %v", err) + return 0, nil, fmt.Errorf("error getting documents: %v", err) } // Compile the filter regex if provided @@ -528,11 +570,12 @@ func exportUnitsFromLoadedUnits(units []MxUnit, outputDirectory string, raw bool if filter != "" { filterRegex, err = regexp.Compile(filter) if err != nil { - return 0, fmt.Errorf("invalid filter regex pattern: %v", err) + return 0, nil, fmt.Errorf("invalid filter regex pattern: %v", err) } log.Infof("Applying filter: %s", filter) } + pathMap := make(map[string]string) exportedCount := 0 for _, document := range documents { // Apply filter if provided @@ -564,7 +607,7 @@ func exportUnitsFromLoadedUnits(units []MxUnit, outputDirectory string, raw bool // Validate and adjust path length to prevent exceeding OS limits adjustedPath, adjustedFilename, err := validatePathLength(outputDirectory, sanitizedPath, fname) if err != nil { - return 0, fmt.Errorf("error adjusting path length: %v", err) + return 0, nil, fmt.Errorf("error adjusting path length: %v", err) } directory := filepath.Join(outputDirectory, adjustedPath) @@ -572,16 +615,19 @@ func exportUnitsFromLoadedUnits(units []MxUnit, outputDirectory string, raw bool // ensure directory exists if _, err := os.Stat(directory); os.IsNotExist(err) { if err := os.MkdirAll(directory, 0755); err != nil { - return 0, fmt.Errorf("error creating directory: %v", err) + return 0, nil, fmt.Errorf("error creating directory: %v", err) } } attributes := cleanData(document.Attributes, raw) - err = writeFileWithPersistentCache(filepath.Join(directory, adjustedFilename), attributes, document.ContentsHash, raw) + outPath := filepath.Join(directory, adjustedFilename) + err = writeFileWithPersistentCache(outPath, attributes, document.ContentsHash, raw) if err != nil { log.Errorf("Error writing file: %v", err) - return 0, err + return 0, nil, err } + relPath := filepath.ToSlash(filepath.Join(adjustedPath, adjustedFilename)) + pathMap[relPath] = originalDocumentRelativePath(document.OriginalFolderPath, document.Name, document.Type) exportedCount++ } @@ -589,7 +635,7 @@ func exportUnitsFromLoadedUnits(units []MxUnit, outputDirectory string, raw bool log.Infof("Exported %d documents matching filter (out of %d total)", exportedCount, len(documents)) } - return exportedCount, nil + return exportedCount, pathMap, nil } @@ -1000,21 +1046,29 @@ func hashFile(path string) (string, error) { return hex.EncodeToString(hasher.Sum(nil)), nil } -// FileNode represents a file or directory in the file structure +// FileNode represents a file or directory in the hierarchical app.yaml content tree. type FileNode struct { - Name string `yaml:"name"` - Type string `yaml:"type"` // "file" or "directory" - Path string `yaml:"path,omitempty"` - Content []FileNode `yaml:"content,omitempty"` + Name string `yaml:"name"` + Type string `yaml:"type"` // "file" or "directory" + Path string `yaml:"path,omitempty"` + OriginalName string `yaml:"originalName"` + Content []FileNode `yaml:"content,omitempty"` } -// AppStructure represents the entire file structure for app.yaml +// AppFileEntry is a flat disk→original path mapping entry for easy resolving. +type AppFileEntry struct { + Path string `yaml:"path"` + OriginalPath string `yaml:"originalPath"` +} + +// AppStructure is written to app.yaml: hierarchical content plus a flat path map. type AppStructure struct { - Content []FileNode `yaml:"content"` + Content []FileNode `yaml:"content"` + Files []AppFileEntry `yaml:"files"` } -// buildFileStructure recursively builds the file structure for a directory -func buildFileStructure(basePath string, currentPath string) (*FileNode, error) { +// buildFileStructure recursively builds the hierarchical file structure for a directory. +func buildFileStructure(basePath string, currentPath string, pathMap map[string]string) (*FileNode, error) { fullPath := filepath.Join(basePath, currentPath) info, err := os.Stat(fullPath) if err != nil { @@ -1025,14 +1079,29 @@ func buildFileStructure(basePath string, currentPath string) (*FileNode, error) if relPath == "" { relPath = "." } + relSlash := filepath.ToSlash(relPath) + name := filepath.Base(fullPath) + if relPath == "." { + name = filepath.Base(basePath) + } + + originalPath := resolveOriginalPath(relSlash, pathMap) + originalName := name + if relSlash != "." { + originalName = filepath.Base(filepath.FromSlash(originalPath)) + } node := &FileNode{ - Name: filepath.Base(fullPath), - Path: relPath, + Name: name, + Path: relSlash, + OriginalName: originalName, } if info.IsDir() { node.Type = "directory" + if relPath == "." { + node.Path = "" + } entries, err := os.ReadDir(fullPath) if err != nil { @@ -1040,13 +1109,12 @@ func buildFileStructure(basePath string, currentPath string) (*FileNode, error) } for _, entry := range entries { - // Skip app.yaml to avoid self-reference, and hidden files/directories (e.g. .git) - name := entry.Name() - if name == "app.yaml" || strings.HasPrefix(name, ".") { + entryName := entry.Name() + if entryName == "app.yaml" || strings.HasPrefix(entryName, ".") { continue } - childRelPath := filepath.Join(currentPath, entry.Name()) - childNode, err := buildFileStructure(basePath, childRelPath) + childRelPath := filepath.Join(currentPath, entryName) + childNode, err := buildFileStructure(basePath, childRelPath, pathMap) if err != nil { log.Warnf("Error processing %s: %v", childRelPath, err) continue @@ -1060,33 +1128,101 @@ func buildFileStructure(basePath string, currentPath string) (*FileNode, error) 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") +// resolveOriginalPath returns the Mendix original path for a disk-relative path. +// For directories, derives the original prefix from any mapped child file. +func resolveOriginalPath(diskRel string, pathMap map[string]string) string { + diskRel = filepath.ToSlash(diskRel) + if diskRel == "" || diskRel == "." { + return diskRel + } + if pathMap != nil { + if mapped, ok := pathMap[diskRel]; ok && mapped != "" { + return filepath.ToSlash(mapped) + } + prefix := diskRel + "/" + diskParts := strings.Split(diskRel, "/") + for disk, orig := range pathMap { + disk = filepath.ToSlash(disk) + orig = filepath.ToSlash(orig) + if !strings.HasPrefix(disk, prefix) { + continue + } + origParts := strings.Split(orig, "/") + if len(origParts) >= len(diskParts) { + return strings.Join(origParts[:len(diskParts)], "/") + } + } + } + return diskRel +} + +func buildFlatPathMap(outputDirectory string, pathMap map[string]string) ([]AppFileEntry, error) { + files := make([]AppFileEntry, 0) + err := filepath.WalkDir(outputDirectory, func(path string, d os.DirEntry, walkErr error) error { + if walkErr != nil { + return walkErr + } + name := d.Name() + if d.IsDir() { + if path != outputDirectory && strings.HasPrefix(name, ".") { + return filepath.SkipDir + } + return nil + } + if name == "app.yaml" || strings.HasPrefix(name, ".") { + return nil + } - // Build the file structure - rootNode, err := buildFileStructure(outputDirectory, "") + relPath, err := filepath.Rel(outputDirectory, path) + if err != nil { + return err + } + relPath = filepath.ToSlash(relPath) + files = append(files, AppFileEntry{ + Path: relPath, + OriginalPath: resolveOriginalPath(relPath, pathMap), + }) + return nil + }) + if err != nil { + return nil, err + } + sort.Slice(files, func(i, j int) bool { + return files[i].Path < files[j].Path + }) + return files, nil +} + +// generateAppYaml writes hierarchical content (with originalName) plus a flat path mapping list. +// pathMap maps disk-relative file paths to Mendix original paths. +func generateAppYaml(outputDirectory string, pathMap map[string]string) error { + log.Infof("Generating app.yaml with file structure and path map") + + rootNode, err := buildFileStructure(outputDirectory, "", pathMap) if err != nil { return fmt.Errorf("error building file structure: %v", err) } - // Use the content of the root as the project structure + files, err := buildFlatPathMap(outputDirectory, pathMap) + if err != nil { + return fmt.Errorf("error building path map: %v", err) + } + appStructure := AppStructure{ Content: rootNode.Content, + Files: files, } - // 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) + log.Infof("Generated app.yaml at %s (%d content roots, %d mapped files)", appYamlPath, len(rootNode.Content), len(files)) return nil } diff --git a/mpr/mpr_test.go b/mpr/mpr_test.go index ce46bb5..e637f86 100644 --- a/mpr/mpr_test.go +++ b/mpr/mpr_test.go @@ -1100,60 +1100,58 @@ func containsSanitizedName(path, expected string) bool { return len(path) > 0 && len(expected) > 0 } +func TestGetMxDocumentOriginalPath(t *testing.T) { + longName := "Folder_testverylonglonglonglonglonglong name" + root := MxFolder{Name: "Module2", ID: "root", ParentID: ""} + child := MxFolder{Name: longName, ID: "child", ParentID: "root", Parent: &root} + + got := getMxDocumentOriginalPathRecursive(child, 10) + want := filepath.Join("Module2", longName) + if got != want { + t.Fatalf("getMxDocumentOriginalPathRecursive() = %q, want %q", got, want) + } +} + +func TestOriginalFilename(t *testing.T) { + if got := originalFilename("Constant_3", "Constants$Constant"); got != "Constant_3.Constants$Constant.yaml" { + t.Fatalf("originalFilename() = %q", got) + } + if got := originalFilename("", "DomainModels$DomainModel"); got != "DomainModels$DomainModel.yaml" { + t.Fatalf("originalFilename() empty name = %q", got) + } +} + 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, + files := []string{ + "Metadata.yaml", + "MyModule/DomainModel.yaml", + "MyModule/SubFolder/Page.yaml", + "Navigation.yaml", } - - // Create test files and directories - for path, isDir := range testStructure { + for _, path := range files { 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) - } + if err := os.MkdirAll(filepath.Dir(fullPath), 0755); err != nil { + t.Fatalf("Failed to create parent for %s: %v", path, err) + } + 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) + pathMap := map[string]string{ + "MyModule/SubFolder/Page.yaml": "MyModule/Very Long Folder Name/Page.yaml", } - - // 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 + if err := generateAppYaml(tmpDir, pathMap); err != nil { + t.Fatalf("generateAppYaml() unexpected error: %v", err) } - // Read and parse app.yaml - yamlContent, err := os.ReadFile(appYamlPath) + yamlContent, err := os.ReadFile(filepath.Join(tmpDir, "app.yaml")) if err != nil { t.Fatalf("Failed to read app.yaml: %v", err) } @@ -1163,13 +1161,13 @@ func TestGenerateAppYaml(t *testing.T) { 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 + t.Fatal("expected hierarchical content") + } + if len(appStructure.Files) != 4 { + t.Fatalf("expected 4 flat mapped files, got %d", len(appStructure.Files)) } - // Helper function to find a node by name findNode := func(nodes []FileNode, name string) *FileNode { for i := range nodes { if nodes[i].Name == name { @@ -1179,238 +1177,68 @@ func TestGenerateAppYaml(t *testing.T) { 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"}, - }, + metadata := findNode(appStructure.Content, "Metadata.yaml") + if metadata == nil || metadata.Type != "file" { + t.Fatal("Metadata.yaml missing from content tree") } - - 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) - } - }) + if metadata.OriginalName != "Metadata.yaml" { + t.Errorf("Metadata.yaml originalName = %q", metadata.OriginalName) } - // 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") + module := findNode(appStructure.Content, "MyModule") + if module == nil || module.Type != "directory" { + t.Fatal("MyModule missing from content tree") } - - // 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") - } + sub := findNode(module.Content, "SubFolder") + if sub == nil || sub.Type != "directory" { + t.Fatal("SubFolder missing from content tree") } -} - -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) + if sub.OriginalName != "Very Long Folder Name" { + t.Errorf("SubFolder originalName = %q, want %q", sub.OriginalName, "Very Long Folder Name") } - 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) {}, - }, + page := findNode(sub.Content, "Page.yaml") + if page == nil || page.Type != "file" { + t.Fatal("Page.yaml missing from content tree") + } + if page.OriginalName != "Page.yaml" { + t.Errorf("Page.yaml originalName = %q", page.OriginalName) } - 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 findNode(appStructure.Content, "app.yaml") != nil { + t.Error("app.yaml should not be in content tree") + } - 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) - } - } - }) + byPath := make(map[string]AppFileEntry, len(appStructure.Files)) + for _, entry := range appStructure.Files { + byPath[entry.Path] = entry + } + if entry := byPath["MyModule/SubFolder/Page.yaml"]; entry.OriginalPath != "MyModule/Very Long Folder Name/Page.yaml" { + t.Errorf("flat map originalPath = %q", entry.OriginalPath) + } + if entry := byPath["Metadata.yaml"]; entry.OriginalPath != "Metadata.yaml" { + t.Errorf("flat map Metadata originalPath = %q", entry.OriginalPath) } } 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 { + if err := os.WriteFile(filepath.Join(tmpDir, "test.yaml"), []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 { + if err := os.WriteFile(filepath.Join(tmpDir, "app.yaml"), []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 { + if err := generateAppYaml(tmpDir, nil); err != nil { t.Fatalf("generateAppYaml() unexpected error: %v", err) } - // Read and parse the generated app.yaml - yamlContent, err := os.ReadFile(existingAppYaml) + yamlContent, err := os.ReadFile(filepath.Join(tmpDir, "app.yaml")) if err != nil { t.Fatalf("Failed to read app.yaml: %v", err) } @@ -1420,23 +1248,25 @@ func TestGenerateAppYamlExcludesItself(t *testing.T) { 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") + t.Errorf("app.yaml should not be included in content tree") } } - - // Verify test.yaml is included - foundTestYaml := false - for _, node := range appStructure.Content { - if node.Name == "test.yaml" { - foundTestYaml = true - break + foundTest := false + for _, entry := range appStructure.Files { + if entry.Path == "app.yaml" { + t.Errorf("app.yaml should not be included in flat map") + } + if entry.Path == "test.yaml" { + foundTest = true + if entry.OriginalPath != "test.yaml" { + t.Errorf("test.yaml originalPath = %q, want identity", entry.OriginalPath) + } } } - if !foundTestYaml { - t.Errorf("test.yaml should be included in app.yaml structure") + if !foundTest { + t.Errorf("test.yaml should be included in flat map") } } @@ -1447,26 +1277,21 @@ func TestGenerateAppYamlExcludesDotEntries(t *testing.T) { } defer os.RemoveAll(tmpDir) - testFile := filepath.Join(tmpDir, "test.yaml") - if err := os.WriteFile(testFile, []byte("test content"), 0644); err != nil { + if err := os.WriteFile(filepath.Join(tmpDir, "test.yaml"), []byte("test content"), 0644); err != nil { t.Fatalf("Failed to create test file: %v", err) } - gitDir := filepath.Join(tmpDir, ".git") if err := os.MkdirAll(gitDir, 0755); err != nil { t.Fatalf("Failed to create .git directory: %v", err) } - gitHead := filepath.Join(gitDir, "HEAD") - if err := os.WriteFile(gitHead, []byte("ref: refs/heads/main\n"), 0644); err != nil { + if err := os.WriteFile(filepath.Join(gitDir, "HEAD"), []byte("ref: refs/heads/main\n"), 0644); err != nil { t.Fatalf("Failed to create .git/HEAD: %v", err) } - - hiddenFile := filepath.Join(tmpDir, ".DS_Store") - if err := os.WriteFile(hiddenFile, []byte("hidden"), 0644); err != nil { + if err := os.WriteFile(filepath.Join(tmpDir, ".DS_Store"), []byte("hidden"), 0644); err != nil { t.Fatalf("Failed to create hidden file: %v", err) } - if err := generateAppYaml(tmpDir); err != nil { + if err := generateAppYaml(tmpDir, nil); err != nil { t.Fatalf("generateAppYaml() unexpected error: %v", err) } @@ -1480,21 +1305,83 @@ func TestGenerateAppYamlExcludesDotEntries(t *testing.T) { t.Fatalf("Failed to unmarshal app.yaml: %v", err) } - for _, node := range appStructure.Content { - if strings.HasPrefix(node.Name, ".") { - t.Errorf("dot entry %q should not be included in app.yaml structure", node.Name) + var walkContent func([]FileNode) + walkContent = func(nodes []FileNode) { + for _, node := range nodes { + if strings.HasPrefix(node.Name, ".") { + t.Errorf("dot entry %q should not be in content tree", node.Name) + } + walkContent(node.Content) } } + walkContent(appStructure.Content) - foundTestYaml := false - for _, node := range appStructure.Content { - if node.Name == "test.yaml" { - foundTestYaml = true - break + foundTest := false + for _, entry := range appStructure.Files { + base := filepath.Base(entry.Path) + if strings.HasPrefix(base, ".") || strings.Contains(entry.Path, "/.") { + t.Errorf("dot entry %q should not be in flat map", entry.Path) } + if entry.Path == "test.yaml" { + foundTest = true + } + } + if !foundTest { + t.Errorf("test.yaml should be included in flat map") + } +} + +func TestGenerateAppYamlTruncationMapping(t *testing.T) { + tmpDir := t.TempDir() + diskDir := "Module2/Folder_testverylongl_TRUNCATED_70781_longlong name" + diskPath := diskDir + "/Constant_3.Constants$Constant.yaml" + originalDir := "Module2/Folder_testverylonglonglonglonglonglong name" + originalPath := originalDir + "/Constant_3.Constants$Constant.yaml" + + fullPath := filepath.Join(tmpDir, filepath.FromSlash(diskPath)) + if err := os.MkdirAll(filepath.Dir(fullPath), 0755); err != nil { + t.Fatalf("mkdir: %v", err) + } + if err := os.WriteFile(fullPath, []byte("x"), 0644); err != nil { + t.Fatalf("write: %v", err) + } + + if err := generateAppYaml(tmpDir, map[string]string{diskPath: originalPath}); err != nil { + t.Fatalf("generateAppYaml: %v", err) + } + + yamlContent, err := os.ReadFile(filepath.Join(tmpDir, "app.yaml")) + if err != nil { + t.Fatalf("read: %v", err) + } + var appStructure AppStructure + if err := yaml.Unmarshal(yamlContent, &appStructure); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if len(appStructure.Files) != 1 { + t.Fatalf("expected 1 flat mapped file, got %d", len(appStructure.Files)) + } + if appStructure.Files[0].Path != diskPath { + t.Errorf("path = %q, want %q", appStructure.Files[0].Path, diskPath) + } + if appStructure.Files[0].OriginalPath != originalPath { + t.Errorf("originalPath = %q, want %q", appStructure.Files[0].OriginalPath, originalPath) + } + + module := appStructure.Content[0] + if module.Name != "Module2" { + t.Fatalf("expected Module2 root, got %q", module.Name) + } + folder := module.Content[0] + if folder.Name != "Folder_testverylongl_TRUNCATED_70781_longlong name" { + t.Fatalf("unexpected folder name %q", folder.Name) + } + if folder.OriginalName != "Folder_testverylonglonglonglonglonglong name" { + t.Errorf("folder originalName = %q", folder.OriginalName) } - if !foundTestYaml { - t.Errorf("test.yaml should be included in app.yaml structure") + file := folder.Content[0] + if file.OriginalName != "Constant_3.Constants$Constant.yaml" { + t.Errorf("file originalName = %q", file.OriginalName) } } diff --git a/mpr/mpr_v1_test.go b/mpr/mpr_v1_test.go index caf35e2..143f7c1 100644 --- a/mpr/mpr_v1_test.go +++ b/mpr/mpr_v1_test.go @@ -46,7 +46,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, ""); err != nil { + if _, _, err := exportUnits("./../resources/app-mpr-v1", "./../tmp", false, ""); err != nil { t.Errorf("Failed to export units from MPR file") } }) @@ -55,7 +55,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, ""); err != nil { + if _, _, err := exportUnits("./../resources/app-mpr-v1", "./../tmp", false, ""); 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 2fd97ab..2652013 100644 --- a/mpr/mpr_v2_test.go +++ b/mpr/mpr_v2_test.go @@ -43,7 +43,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, ""); err != nil { + if _, _, err := exportUnits("./../resources/app-mpr-v2", "./../tmp", false, ""); err != nil { t.Errorf("Failed to export units from MPR file") } }) diff --git a/mpr/types.go b/mpr/types.go index 4a63e95..5204305 100644 --- a/mpr/types.go +++ b/mpr/types.go @@ -15,11 +15,12 @@ type MxUnit struct { } type MxDocument struct { - Name string `yaml:"Name"` - Type string `yaml:"Type"` - Path string `yaml:"Path"` - Attributes map[string]interface{} `yaml:"Attributes"` - ContentsHash string `yaml:"ContentsHash,omitempty"` + Name string `yaml:"Name"` + Type string `yaml:"Type"` + Path string `yaml:"Path"` + OriginalFolderPath string `yaml:"OriginalFolderPath,omitempty"` + Attributes map[string]interface{} `yaml:"Attributes"` + ContentsHash string `yaml:"ContentsHash,omitempty"` } type MxModule struct { From e941e4c1391888d8bb798b7d06538601d96af146 Mon Sep 17 00:00:00 2001 From: Xiwen Cheng Date: Fri, 24 Jul 2026 17:57:28 +0200 Subject: [PATCH 2/3] output original name in results --- lint/app_yaml.go | 62 +++++++++++++++++++++++++++++++++++++++++++ lint/app_yaml_test.go | 58 ++++++++++++++++++++++++++++++++++++++++ lint/lint.go | 11 +++++--- lint/lint_test.go | 22 +++++++-------- lint/types.go | 11 ++++---- 5 files changed, 145 insertions(+), 19 deletions(-) create mode 100644 lint/app_yaml.go create mode 100644 lint/app_yaml_test.go diff --git a/lint/app_yaml.go b/lint/app_yaml.go new file mode 100644 index 0000000..b5aacc2 --- /dev/null +++ b/lint/app_yaml.go @@ -0,0 +1,62 @@ +package lint + +import ( + "os" + "path/filepath" + "strings" + + "gopkg.in/yaml.v3" +) + +type appYamlFileEntry struct { + Path string `yaml:"path"` + OriginalPath string `yaml:"originalPath"` +} + +type appYamlStructure struct { + Files []appYamlFileEntry `yaml:"files"` +} + +// loadOriginalPathMap reads modelsource/app.yaml and returns disk path → original path. +// Missing or unreadable app.yaml yields an empty map (callers fall back to identity). +func loadOriginalPathMap(modelSourcePath string) map[string]string { + appYamlPath := filepath.Join(modelSourcePath, "app.yaml") + data, err := os.ReadFile(appYamlPath) + if err != nil { + return map[string]string{} + } + + var structure appYamlStructure + if err := yaml.Unmarshal(data, &structure); err != nil { + log.Debugf("Could not parse %s for originalPath mapping: %v", appYamlPath, err) + return map[string]string{} + } + + pathMap := make(map[string]string, len(structure.Files)) + for _, entry := range structure.Files { + path := filepath.ToSlash(strings.TrimSpace(entry.Path)) + if path == "" { + continue + } + original := filepath.ToSlash(strings.TrimSpace(entry.OriginalPath)) + if original == "" { + original = path + } + pathMap[path] = original + } + return pathMap +} + +// resolveOriginalPath returns the mapped original path, or name when unmapped. +func resolveOriginalPath(name string, pathMap map[string]string) string { + name = filepath.ToSlash(name) + if name == "" { + return name + } + if pathMap != nil { + if original, ok := pathMap[name]; ok && original != "" { + return original + } + } + return name +} diff --git a/lint/app_yaml_test.go b/lint/app_yaml_test.go new file mode 100644 index 0000000..44cdd71 --- /dev/null +++ b/lint/app_yaml_test.go @@ -0,0 +1,58 @@ +package lint + +import ( + "os" + "path/filepath" + "testing" +) + +func TestLoadOriginalPathMapMissingFile(t *testing.T) { + pathMap := loadOriginalPathMap(t.TempDir()) + if len(pathMap) != 0 { + t.Fatalf("expected empty map for missing app.yaml, got %v", pathMap) + } +} + +func TestLoadOriginalPathMapAndResolve(t *testing.T) { + tmpDir := t.TempDir() + appYaml := ` +content: [] +files: + - path: Module2/Folder_TRUNCATED/Constant.yaml + originalPath: Module2/Folder_very_long_name/Constant.yaml + - path: Metadata.yaml + originalPath: Metadata.yaml +` + if err := os.WriteFile(filepath.Join(tmpDir, "app.yaml"), []byte(appYaml), 0644); err != nil { + t.Fatalf("write app.yaml: %v", err) + } + + pathMap := loadOriginalPathMap(tmpDir) + if got := resolveOriginalPath("Module2/Folder_TRUNCATED/Constant.yaml", pathMap); got != "Module2/Folder_very_long_name/Constant.yaml" { + t.Fatalf("mapped path = %q", got) + } + if got := resolveOriginalPath("Metadata.yaml", pathMap); got != "Metadata.yaml" { + t.Fatalf("identity mapped path = %q", got) + } + if got := resolveOriginalPath("Unmapped/Doc.yaml", pathMap); got != "Unmapped/Doc.yaml" { + t.Fatalf("unmapped fallback = %q", got) + } +} + +func TestResolveOriginalPathNilMap(t *testing.T) { + if got := resolveOriginalPath("Module2/Doc.yaml", nil); got != "Module2/Doc.yaml" { + t.Fatalf("nil map fallback = %q", got) + } +} + +func TestFormatTestcaseNameWithOriginalPath(t *testing.T) { + modelSource := t.TempDir() + inputFile := filepath.Join(modelSource, "Module2", "Doc.yaml") + name := formatTestcaseName(inputFile, modelSource) + pathMap := map[string]string{ + "Module2/Doc.yaml": "Module2/Original Doc.yaml", + } + if got := resolveOriginalPath(name, pathMap); got != "Module2/Original Doc.yaml" { + t.Fatalf("originalPath = %q, name = %q", got, name) + } +} diff --git a/lint/lint.go b/lint/lint.go index 9cdbe6b..d736db1 100644 --- a/lint/lint.go +++ b/lint/lint.go @@ -48,6 +48,8 @@ func EvalAllWithResults(rulesPath string, modelSourcePath string, xunitReport st // Create a mutex to safely print testsuites var printMutex sync.Mutex + originalPathMap := loadOriginalPathMap(modelSourcePath) + maxConcurrency := effectiveLintConcurrency(len(rules)) sem := make(chan struct{}, maxConcurrency) @@ -59,7 +61,7 @@ func EvalAllWithResults(rulesPath string, modelSourcePath string, xunitReport st defer wg.Done() defer func() { <-sem }() - testsuite, err := evalTestsuite(r, modelSourcePath, ignoreNoqa, useCache, changedFiles) + testsuite, err := evalTestsuite(r, modelSourcePath, ignoreNoqa, useCache, changedFiles, originalPathMap) if err != nil { errChan <- err return @@ -161,6 +163,8 @@ func EvalAll(rulesPath string, modelSourcePath string, xunitReport string, jsonF // Create a mutex to safely print testsuites var printMutex sync.Mutex + originalPathMap := loadOriginalPathMap(modelSourcePath) + maxConcurrency := effectiveLintConcurrency(len(rules)) sem := make(chan struct{}, maxConcurrency) @@ -172,7 +176,7 @@ func EvalAll(rulesPath string, modelSourcePath string, xunitReport string, jsonF defer wg.Done() defer func() { <-sem }() - testsuite, err := evalTestsuite(r, modelSourcePath, ignoreNoqa, useCache, changedFiles) + testsuite, err := evalTestsuite(r, modelSourcePath, ignoreNoqa, useCache, changedFiles, originalPathMap) if err != nil { errChan <- err return @@ -269,7 +273,7 @@ func countTotalTestcases(testsuites []Testsuite) int { return count } -func evalTestsuite(rule Rule, modelSourcePath string, ignoreNoqa bool, useCache bool, changedFiles []string) (*Testsuite, error) { +func evalTestsuite(rule Rule, modelSourcePath string, ignoreNoqa bool, useCache bool, changedFiles []string, originalPathMap map[string]string) (*Testsuite, error) { log.Debugf("evaluating rule %s", rule.Path) @@ -327,6 +331,7 @@ func evalTestsuite(rule Rule, modelSourcePath string, ignoreNoqa bool, useCache // Normalize testcase name for output consistency regardless of cache source. testcase.Name = formatTestcaseName(inputFile, modelSourcePath) + testcase.OriginalPath = resolveOriginalPath(testcase.Name, originalPathMap) if testcase.Failure != nil { failuresCount++ diff --git a/lint/lint_test.go b/lint/lint_test.go index fc26296..a4ba7c9 100644 --- a/lint/lint_test.go +++ b/lint/lint_test.go @@ -14,7 +14,7 @@ func TestEvalTestsuite_Rego(t *testing.T) { t.Fatalf("Failed to parse rule metadata: %v", err) } - result, err := evalTestsuite(*rule, "./../resources/modelsource-v1", false, false, nil) + result, err := evalTestsuite(*rule, "./../resources/modelsource-v1", false, false, nil, nil) if err != nil { t.Fatalf("Failed to evaluate testsuite: %v", err) } @@ -67,7 +67,7 @@ errors contains "Always fails" Language: LanguageRego, } - result, err := evalTestsuite(rule, tempDir, false, false, nil) + result, err := evalTestsuite(rule, tempDir, false, false, nil, nil) if err != nil { t.Fatalf("Failed to evaluate testsuite: %v", err) } @@ -85,7 +85,7 @@ func TestEvalTestsuite_Javascript(t *testing.T) { t.Fatalf("Failed to parse rule metadata: %v", err) } - result, err := evalTestsuite(*rule, "./../resources/modelsource-v1", false, false, nil) + result, err := evalTestsuite(*rule, "./../resources/modelsource-v1", false, false, nil, nil) if err != nil { t.Fatalf("Failed to evaluate testsuite: %v", err) } @@ -134,7 +134,7 @@ function rule(input) { Language: LanguageJavascript, } - result, err := evalTestsuite(rule, tempDir, false, false, nil) + result, err := evalTestsuite(rule, tempDir, false, false, nil, nil) if err != nil { t.Fatalf("Failed to evaluate testsuite: %v", err) } @@ -152,7 +152,7 @@ func TestEvalTestsuite_Typescript(t *testing.T) { t.Fatalf("Failed to parse rule metadata: %v", err) } - result, err := evalTestsuite(*rule, "./../resources/modelsource-v1", false, false, nil) + result, err := evalTestsuite(*rule, "./../resources/modelsource-v1", false, false, nil, nil) if err != nil { t.Fatalf("Failed to evaluate testsuite: %v", err) } @@ -203,7 +203,7 @@ Name: "Test" } t.Run("documentation noqa is ignored", func(t *testing.T) { - result, err := evalTestsuite(rule, tempDir, false, false, nil) + result, err := evalTestsuite(rule, tempDir, false, false, nil, nil) if err != nil { t.Fatalf("Failed to evaluate testsuite: %v", err) } @@ -217,7 +217,7 @@ Name: "Test" }) t.Run("ignoreNoqa has no effect on documentation skip", func(t *testing.T) { - result, err := evalTestsuite(rule, tempDir, true, false, nil) + result, err := evalTestsuite(rule, tempDir, true, false, nil, nil) if err != nil { t.Fatalf("Failed to evaluate testsuite: %v", err) } @@ -275,7 +275,7 @@ function rule(input) { Language: LanguageJavascript, } - result, err := evalTestsuite(rule, tempDir, false, false, nil) + result, err := evalTestsuite(rule, tempDir, false, false, nil, nil) if err != nil { t.Fatalf("Failed to evaluate testsuite: %v", err) } @@ -584,7 +584,7 @@ function rule(input) { Language: LanguageJavascript, } - result, err := evalTestsuite(rule, tempDir, false, false, nil) + result, err := evalTestsuite(rule, tempDir, false, false, nil, nil) if err != nil { t.Fatalf("Expected testsuite evaluation to succeed, got: %v", err) } @@ -640,7 +640,7 @@ function rule(input) { Language: LanguageJavascript, } - result, err := evalTestsuite(rule, tempDir, false, false, nil) + result, err := evalTestsuite(rule, tempDir, false, false, nil, nil) if err != nil { t.Fatalf("Failed to evaluate testsuite: %v", err) } @@ -683,7 +683,7 @@ function rule(input) { Language: LanguageJavascript, } - result, err := evalTestsuite(rule, tempDir, false, false, nil) + result, err := evalTestsuite(rule, tempDir, false, false, nil, nil) if err != nil { t.Fatalf("Failed to evaluate testsuite: %v", err) } diff --git a/lint/types.go b/lint/types.go index a102067..b2e77ff 100644 --- a/lint/types.go +++ b/lint/types.go @@ -19,11 +19,12 @@ type Testsuite struct { } type Testcase struct { - XMLName xml.Name `xml:"testcase" json:"-"` - Name string `xml:"name,attr" json:"name"` - Time float64 `xml:"time,attr" json:"time"` - Failure *Failure `xml:"failure,omitempty" json:"failure,omitempty"` - Skipped *Skipped `xml:"skipped,omitempty" json:"skipped,omitempty"` + XMLName xml.Name `xml:"testcase" json:"-"` + Name string `xml:"name,attr" json:"name"` + OriginalPath string `xml:"originalPath,attr,omitempty" json:"originalPath,omitempty"` + Time float64 `xml:"time,attr" json:"time"` + Failure *Failure `xml:"failure,omitempty" json:"failure,omitempty"` + Skipped *Skipped `xml:"skipped,omitempty" json:"skipped,omitempty"` } type Failure struct { From 9305164591c25436c4e9de130f099699082b8aff Mon Sep 17 00:00:00 2001 From: Xiwen Cheng Date: Fri, 24 Jul 2026 17:58:19 +0200 Subject: [PATCH 3/3] refresh resources --- resources/modelsource-v1/app.yaml | 79 +++++++++++++++++++++++++++++++ resources/modelsource-v2/app.yaml | 74 +++++++++++++++++++++++++++++ 2 files changed, 153 insertions(+) diff --git a/resources/modelsource-v1/app.yaml b/resources/modelsource-v1/app.yaml index 9729fad..7af1521 100644 --- a/resources/modelsource-v1/app.yaml +++ b/resources/modelsource-v1/app.yaml @@ -2,87 +2,166 @@ content: - name: Metadata.yaml type: file path: Metadata.yaml + originalName: Metadata.yaml - name: MyFirstModule type: directory path: MyFirstModule + originalName: MyFirstModule content: - name: BadPage.Forms$Page.yaml type: file path: MyFirstModule/BadPage.Forms$Page.yaml + originalName: BadPage.Forms$Page.yaml - name: DomainModels$DomainModel.yaml type: file path: MyFirstModule/DomainModels$DomainModel.yaml + originalName: DomainModels$DomainModel.yaml - name: Folder type: directory path: MyFirstModule/Folder + originalName: Folder content: - name: EnumerationStatus.Enumerations$Enumeration.yaml type: file path: MyFirstModule/Folder/EnumerationStatus.Enumerations$Enumeration.yaml + originalName: EnumerationStatus.Enumerations$Enumeration.yaml - name: Folder2 type: directory path: MyFirstModule/Folder/Folder2 + originalName: Folder2 content: - name: Page_2.Forms$Page.yaml type: file path: MyFirstModule/Folder/Folder2/Page_2.Forms$Page.yaml + originalName: Page_2.Forms$Page.yaml - name: Snippet2.Forms$Snippet.yaml type: file path: MyFirstModule/Folder/Folder2/Snippet2.Forms$Snippet.yaml + originalName: Snippet2.Forms$Snippet.yaml - name: MicroflowComplexSplit.Microflows$Microflow.yaml type: file path: MyFirstModule/Folder/MicroflowComplexSplit.Microflows$Microflow.yaml + originalName: MicroflowComplexSplit.Microflows$Microflow.yaml - name: MicroflowForLoop.Microflows$Microflow.yaml type: file path: MyFirstModule/Folder/MicroflowForLoop.Microflows$Microflow.yaml + originalName: MicroflowForLoop.Microflows$Microflow.yaml - name: MicroflowLoop.Microflows$Microflow.yaml type: file path: MyFirstModule/Folder/MicroflowLoop.Microflows$Microflow.yaml + originalName: MicroflowLoop.Microflows$Microflow.yaml - name: MicroflowLoopNested.Microflows$Microflow.yaml type: file path: MyFirstModule/Folder/MicroflowLoopNested.Microflows$Microflow.yaml + originalName: MicroflowLoopNested.Microflows$Microflow.yaml - name: MicroflowSimple.Microflows$Microflow.yaml type: file path: MyFirstModule/Folder/MicroflowSimple.Microflows$Microflow.yaml + originalName: MicroflowSimple.Microflows$Microflow.yaml - name: MicroflowSplit.Microflows$Microflow.yaml type: file path: MyFirstModule/Folder/MicroflowSplit.Microflows$Microflow.yaml + originalName: MicroflowSplit.Microflows$Microflow.yaml - name: MicroflowSplitThenMerge.Microflows$Microflow.yaml type: file path: MyFirstModule/Folder/MicroflowSplitThenMerge.Microflows$Microflow.yaml + originalName: MicroflowSplitThenMerge.Microflows$Microflow.yaml - name: Page.Forms$Page.yaml type: file path: MyFirstModule/Folder/Page.Forms$Page.yaml + originalName: Page.Forms$Page.yaml - name: Home_Web.Forms$Page.yaml type: file path: MyFirstModule/Home_Web.Forms$Page.yaml + originalName: Home_Web.Forms$Page.yaml - name: Images.Images$ImageCollection.yaml type: file path: MyFirstModule/Images.Images$ImageCollection.yaml + originalName: Images.Images$ImageCollection.yaml - name: MyFirstLogic.Microflows$Microflow.yaml type: file path: MyFirstModule/MyFirstLogic.Microflows$Microflow.yaml + originalName: MyFirstLogic.Microflows$Microflow.yaml - name: Projects$ModuleSettings.yaml type: file path: MyFirstModule/Projects$ModuleSettings.yaml + originalName: Projects$ModuleSettings.yaml - name: Security$ModuleSecurity.yaml type: file path: MyFirstModule/Security$ModuleSecurity.yaml + originalName: Security$ModuleSecurity.yaml - name: VA_Age.Microflows$Microflow.yaml type: file path: MyFirstModule/VA_Age.Microflows$Microflow.yaml + originalName: VA_Age.Microflows$Microflow.yaml - name: headings.Forms$Page.yaml type: file path: MyFirstModule/headings.Forms$Page.yaml + originalName: headings.Forms$Page.yaml - name: Navigation$NavigationDocument.yaml type: file path: Navigation$NavigationDocument.yaml + originalName: Navigation$NavigationDocument.yaml - name: Security$ProjectSecurity.yaml type: file path: Security$ProjectSecurity.yaml + originalName: Security$ProjectSecurity.yaml - name: Settings$ProjectSettings.yaml type: file path: Settings$ProjectSettings.yaml + originalName: Settings$ProjectSettings.yaml - name: Texts$SystemTextCollection.yaml type: file path: Texts$SystemTextCollection.yaml + originalName: Texts$SystemTextCollection.yaml +files: + - path: Metadata.yaml + originalPath: Metadata.yaml + - path: MyFirstModule/BadPage.Forms$Page.yaml + originalPath: MyFirstModule/BadPage.Forms$Page.yaml + - path: MyFirstModule/DomainModels$DomainModel.yaml + originalPath: MyFirstModule/DomainModels$DomainModel.yaml + - path: MyFirstModule/Folder/EnumerationStatus.Enumerations$Enumeration.yaml + originalPath: MyFirstModule/Folder/EnumerationStatus.Enumerations$Enumeration.yaml + - path: MyFirstModule/Folder/Folder2/Page_2.Forms$Page.yaml + originalPath: MyFirstModule/Folder/Folder2/Page_2.Forms$Page.yaml + - path: MyFirstModule/Folder/Folder2/Snippet2.Forms$Snippet.yaml + originalPath: MyFirstModule/Folder/Folder2/Snippet2.Forms$Snippet.yaml + - path: MyFirstModule/Folder/MicroflowComplexSplit.Microflows$Microflow.yaml + originalPath: MyFirstModule/Folder/MicroflowComplexSplit.Microflows$Microflow.yaml + - path: MyFirstModule/Folder/MicroflowForLoop.Microflows$Microflow.yaml + originalPath: MyFirstModule/Folder/MicroflowForLoop.Microflows$Microflow.yaml + - path: MyFirstModule/Folder/MicroflowLoop.Microflows$Microflow.yaml + originalPath: MyFirstModule/Folder/MicroflowLoop.Microflows$Microflow.yaml + - path: MyFirstModule/Folder/MicroflowLoopNested.Microflows$Microflow.yaml + originalPath: MyFirstModule/Folder/MicroflowLoopNested.Microflows$Microflow.yaml + - path: MyFirstModule/Folder/MicroflowSimple.Microflows$Microflow.yaml + originalPath: MyFirstModule/Folder/MicroflowSimple.Microflows$Microflow.yaml + - path: MyFirstModule/Folder/MicroflowSplit.Microflows$Microflow.yaml + originalPath: MyFirstModule/Folder/MicroflowSplit.Microflows$Microflow.yaml + - path: MyFirstModule/Folder/MicroflowSplitThenMerge.Microflows$Microflow.yaml + originalPath: MyFirstModule/Folder/MicroflowSplitThenMerge.Microflows$Microflow.yaml + - path: MyFirstModule/Folder/Page.Forms$Page.yaml + originalPath: MyFirstModule/Folder/Page.Forms$Page.yaml + - path: MyFirstModule/Home_Web.Forms$Page.yaml + originalPath: MyFirstModule/Home_Web.Forms$Page.yaml + - path: MyFirstModule/Images.Images$ImageCollection.yaml + originalPath: MyFirstModule/Images.Images$ImageCollection.yaml + - path: MyFirstModule/MyFirstLogic.Microflows$Microflow.yaml + originalPath: MyFirstModule/MyFirstLogic.Microflows$Microflow.yaml + - path: MyFirstModule/Projects$ModuleSettings.yaml + originalPath: MyFirstModule/Projects$ModuleSettings.yaml + - path: MyFirstModule/Security$ModuleSecurity.yaml + originalPath: MyFirstModule/Security$ModuleSecurity.yaml + - path: MyFirstModule/VA_Age.Microflows$Microflow.yaml + originalPath: MyFirstModule/VA_Age.Microflows$Microflow.yaml + - path: MyFirstModule/headings.Forms$Page.yaml + originalPath: MyFirstModule/headings.Forms$Page.yaml + - path: Navigation$NavigationDocument.yaml + originalPath: Navigation$NavigationDocument.yaml + - path: Security$ProjectSecurity.yaml + originalPath: Security$ProjectSecurity.yaml + - path: Settings$ProjectSettings.yaml + originalPath: Settings$ProjectSettings.yaml + - path: Texts$SystemTextCollection.yaml + originalPath: Texts$SystemTextCollection.yaml diff --git a/resources/modelsource-v2/app.yaml b/resources/modelsource-v2/app.yaml index 2a128cb..b5f087b 100644 --- a/resources/modelsource-v2/app.yaml +++ b/resources/modelsource-v2/app.yaml @@ -2,94 +2,168 @@ content: - name: Metadata.yaml type: file path: Metadata.yaml + originalName: Metadata.yaml - name: Module2 type: directory path: Module2 + originalName: Module2 content: - name: DomainModels$DomainModel.yaml type: file path: Module2/DomainModels$DomainModel.yaml + originalName: DomainModels$DomainModel.yaml - name: Folder_ type: directory path: Module2/Folder_ + originalName: Folder? content: - name: Constant.Constants$Constant.yaml type: file path: Module2/Folder_/Constant.Constants$Constant.yaml + originalName: Constant.Constants$Constant.yaml - name: Folder_test3 type: directory path: Module2/Folder_test3 + originalName: Folder\test3 content: - name: Constant_4.Constants$Constant.yaml type: file path: Module2/Folder_test3/Constant_4.Constants$Constant.yaml + originalName: Constant_4.Constants$Constant.yaml - name: Folder_testverylongl_TRUNCATED_70781_longlong name type: directory path: Module2/Folder_testverylongl_TRUNCATED_70781_longlong name + originalName: Folder content: - name: Folder testverylongl_TRUNCATED_82413_glonglonglong type: directory path: Module2/Folder_testverylongl_TRUNCATED_70781_longlong name/Folder testverylongl_TRUNCATED_82413_glonglonglong + originalName: testverylonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglong name content: - name: Constant_3.Constants$Constant.yaml type: file path: Module2/Folder_testverylongl_TRUNCATED_70781_longlong name/Folder testverylongl_TRUNCATED_82413_glonglonglong/Constant_3.Constants$Constant.yaml + originalName: Constant_3.Constants$Constant.yaml - name: MicroflowLoopExample.Microflows$Microflow.yaml type: file path: Module2/MicroflowLoopExample.Microflows$Microflow.yaml + originalName: MicroflowLoopExample.Microflows$Microflow.yaml - name: MicroflowNonPersist.Microflows$Microflow.yaml type: file path: Module2/MicroflowNonPersist.Microflows$Microflow.yaml + originalName: MicroflowNonPersist.Microflows$Microflow.yaml - name: MultiLineMicroflow.Microflows$Microflow.yaml type: file path: Module2/MultiLineMicroflow.Microflows$Microflow.yaml + originalName: MultiLineMicroflow.Microflows$Microflow.yaml - name: Projects$ModuleSettings.yaml type: file path: Module2/Projects$ModuleSettings.yaml + originalName: Projects$ModuleSettings.yaml - name: Security$ModuleSecurity.yaml type: file path: Module2/Security$ModuleSecurity.yaml + originalName: Security$ModuleSecurity.yaml - name: SubMicroflowExample.Microflows$Microflow.yaml type: file path: Module2/SubMicroflowExample.Microflows$Microflow.yaml + originalName: SubMicroflowExample.Microflows$Microflow.yaml - name: _ type: directory path: Module2/_ + originalName: _ content: - name: Constant_2.Constants$Constant.yaml type: file path: Module2/_/Constant_2.Constants$Constant.yaml + originalName: Constant_2.Constants$Constant.yaml - name: MyFirstModule type: directory path: MyFirstModule + originalName: MyFirstModule content: - name: DomainModels$DomainModel.yaml type: file path: MyFirstModule/DomainModels$DomainModel.yaml + originalName: DomainModels$DomainModel.yaml - name: Home_Web.Forms$Page.yaml type: file path: MyFirstModule/Home_Web.Forms$Page.yaml + originalName: Home_Web.Forms$Page.yaml - name: Images.Images$ImageCollection.yaml type: file path: MyFirstModule/Images.Images$ImageCollection.yaml + originalName: Images.Images$ImageCollection.yaml - name: MyFirstLogic.Microflows$Microflow.yaml type: file path: MyFirstModule/MyFirstLogic.Microflows$Microflow.yaml + originalName: MyFirstLogic.Microflows$Microflow.yaml - name: Projects$ModuleSettings.yaml type: file path: MyFirstModule/Projects$ModuleSettings.yaml + originalName: Projects$ModuleSettings.yaml - name: Security$ModuleSecurity.yaml type: file path: MyFirstModule/Security$ModuleSecurity.yaml + originalName: Security$ModuleSecurity.yaml - name: Navigation$NavigationDocument.yaml type: file path: Navigation$NavigationDocument.yaml + originalName: Navigation$NavigationDocument.yaml - name: Security$ProjectSecurity.yaml type: file path: Security$ProjectSecurity.yaml + originalName: Security$ProjectSecurity.yaml - name: Settings$ProjectSettings.yaml type: file path: Settings$ProjectSettings.yaml + originalName: Settings$ProjectSettings.yaml - name: Texts$SystemTextCollection.yaml type: file path: Texts$SystemTextCollection.yaml + originalName: Texts$SystemTextCollection.yaml +files: + - path: Metadata.yaml + originalPath: Metadata.yaml + - path: Module2/DomainModels$DomainModel.yaml + originalPath: Module2/DomainModels$DomainModel.yaml + - path: Module2/Folder_/Constant.Constants$Constant.yaml + originalPath: Module2/Folder?/Constant.Constants$Constant.yaml + - path: Module2/Folder_test3/Constant_4.Constants$Constant.yaml + originalPath: Module2/Folder\test3/Constant_4.Constants$Constant.yaml + - path: Module2/Folder_testverylongl_TRUNCATED_70781_longlong name/Folder testverylongl_TRUNCATED_82413_glonglonglong/Constant_3.Constants$Constant.yaml + originalPath: Module2/Folder/testverylonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglong name/Folder testverylonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglong testverylonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglong testverylonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglong/Constant_3.Constants$Constant.yaml + - path: Module2/MicroflowLoopExample.Microflows$Microflow.yaml + originalPath: Module2/MicroflowLoopExample.Microflows$Microflow.yaml + - path: Module2/MicroflowNonPersist.Microflows$Microflow.yaml + originalPath: Module2/MicroflowNonPersist.Microflows$Microflow.yaml + - path: Module2/MultiLineMicroflow.Microflows$Microflow.yaml + originalPath: Module2/MultiLineMicroflow.Microflows$Microflow.yaml + - path: Module2/Projects$ModuleSettings.yaml + originalPath: Module2/Projects$ModuleSettings.yaml + - path: Module2/Security$ModuleSecurity.yaml + originalPath: Module2/Security$ModuleSecurity.yaml + - path: Module2/SubMicroflowExample.Microflows$Microflow.yaml + originalPath: Module2/SubMicroflowExample.Microflows$Microflow.yaml + - path: Module2/_/Constant_2.Constants$Constant.yaml + originalPath: Constant_2.Constants$Constant.yaml + - path: MyFirstModule/DomainModels$DomainModel.yaml + originalPath: MyFirstModule/DomainModels$DomainModel.yaml + - path: MyFirstModule/Home_Web.Forms$Page.yaml + originalPath: MyFirstModule/Home_Web.Forms$Page.yaml + - path: MyFirstModule/Images.Images$ImageCollection.yaml + originalPath: MyFirstModule/Images.Images$ImageCollection.yaml + - path: MyFirstModule/MyFirstLogic.Microflows$Microflow.yaml + originalPath: MyFirstModule/MyFirstLogic.Microflows$Microflow.yaml + - path: MyFirstModule/Projects$ModuleSettings.yaml + originalPath: MyFirstModule/Projects$ModuleSettings.yaml + - path: MyFirstModule/Security$ModuleSecurity.yaml + originalPath: MyFirstModule/Security$ModuleSecurity.yaml + - path: Navigation$NavigationDocument.yaml + originalPath: Navigation$NavigationDocument.yaml + - path: Security$ProjectSecurity.yaml + originalPath: Security$ProjectSecurity.yaml + - path: Settings$ProjectSettings.yaml + originalPath: Settings$ProjectSettings.yaml + - path: Texts$SystemTextCollection.yaml + originalPath: Texts$SystemTextCollection.yaml