diff --git a/audit-cli/.gitignore b/audit-cli/.gitignore index b028d6f..66fab6a 100644 --- a/audit-cli/.gitignore +++ b/audit-cli/.gitignore @@ -1,2 +1,3 @@ audit-cli output/ +bin/ \ No newline at end of file diff --git a/audit-cli/README.md b/audit-cli/README.md index c5e124c..57a0a24 100644 --- a/audit-cli/README.md +++ b/audit-cli/README.md @@ -11,6 +11,7 @@ A Go CLI tool for extracting and analyzing code examples from MongoDB documentat - [Search Commands](#search-commands) - [Analyze Commands](#analyze-commands) - [Compare Commands](#compare-commands) + - [Count Commands](#count-commands) - [Development](#development) - [Project Structure](#project-structure) - [Adding New Commands](#adding-new-commands) @@ -34,11 +35,11 @@ This CLI tool helps maintain code quality across MongoDB's documentation by: ### Build from Source ```bash -cd audit-cli -go build +cd audit-cli/bin +go build ../ ``` -This creates an `audit-cli` executable in the current directory. +This creates an `audit-cli` executable in the `bin` directory. ### Run Without Building @@ -62,8 +63,12 @@ audit-cli │ ├── includes │ ├── usage │ └── procedures -└── compare # Compare files across versions - └── file-contents +│ └── usage +├── compare # Compare files across versions +│ └── file-contents +└── count # Count code examples and documentation pages + ├── tested-examples + └── pages ``` ### Extract Commands @@ -930,6 +935,188 @@ product-dir/ Files that don't exist in certain versions are reported separately and do not cause errors. This is expected behavior since features may be added or removed across versions. +### Count Commands + +#### `count tested-examples` + +Count tested code examples in the MongoDB documentation monorepo. + +This command navigates to the `content/code-examples/tested` directory from the monorepo root and counts all files recursively. The tested directory has a two-level structure: L1 (language directories) and L2 (product directories). + +**Use Cases:** + +This command helps writers and maintainers: +- Track the total number of tested code examples +- Monitor code example coverage by product +- Identify products with few or many examples +- Count only source files (excluding output files) + +**Basic Usage:** + +```bash +# Get total count of all tested code examples +./audit-cli count tested-examples /path/to/docs-monorepo + +# Count examples for a specific product +./audit-cli count tested-examples /path/to/docs-monorepo --for-product pymongo + +# Show counts broken down by product +./audit-cli count tested-examples /path/to/docs-monorepo --count-by-product + +# Count only source files (exclude .txt and .sh output files) +./audit-cli count tested-examples /path/to/docs-monorepo --exclude-output +``` + +**Flags:** + +- `--for-product ` - Only count code examples for a specific product +- `--count-by-product` - Display counts for each product +- `--exclude-output` - Only count source files (exclude .txt and .sh files) + +**Current Valid Products:** + +- `mongosh` - MongoDB Shell +- `csharp/driver` - C#/.NET Driver +- `go/driver` - Go Driver +- `go/atlas-sdk` - Atlas Go SDK +- `java/driver-sync` - Java Sync Driver +- `javascript/driver` - Node.js Driver +- `pymongo` - PyMongo Driver + +**Output:** + +By default, prints a single integer (total count) for use in CI or scripting. With `--count-by-product`, displays a formatted table with product names and counts. + +#### `count pages` + +Count documentation pages (.txt files) in the MongoDB documentation monorepo. + +This command navigates to the `content` directory and recursively counts all `.txt` files, which represent documentation pages that resolve to unique URLs. The command automatically excludes certain directories and file types that don't represent actual documentation pages. + +**Use Cases:** + +This command helps writers and maintainers: +- Track the total number of documentation pages across the monorepo +- Monitor documentation coverage by product/project +- Identify projects with extensive or minimal documentation +- Exclude auto-generated or deprecated content from counts +- Count only current versions of versioned documentation +- Compare page counts across different documentation versions + +**Automatic Exclusions:** + +The command automatically excludes: +- Files in `code-examples` directories at the root of `content` or `source` (these contain plain text examples, not pages) +- Files in the following directories at the root of `content`: + - `404` - Error pages + - `docs-platform` - Documentation for the MongoDB website and meta content + - `meta` - MongoDB Meta Documentation - style guide, tools, etc. + - `table-of-contents` - Navigation files +- All non-`.txt` files (configuration files, YAML, etc.) + +**Basic Usage:** + +```bash +# Get total count of all documentation pages +./audit-cli count pages /path/to/docs-monorepo + +# Count pages for a specific project +./audit-cli count pages /path/to/docs-monorepo --for-project manual + +# Show counts broken down by project +./audit-cli count pages /path/to/docs-monorepo --count-by-project + +# Exclude specific directories from counting +./audit-cli count pages /path/to/docs-monorepo --exclude-dirs api-reference,generated + +# Count only current versions (for versioned projects) +./audit-cli count pages /path/to/docs-monorepo --current-only + +# Show counts by project and version +./audit-cli count pages /path/to/docs-monorepo --by-version + +# Combine flags: count pages for a specific project, excluding certain directories +./audit-cli count pages /path/to/docs-monorepo --for-project atlas --exclude-dirs deprecated +``` + +**Flags:** + +- `--for-project ` - Only count pages for a specific project (directory name under `content/`) +- `--count-by-project` - Display counts for each project in a formatted table +- `--exclude-dirs ` - Comma-separated list of directory names to exclude from counting (e.g., `deprecated,archive`) +- `--current-only` - Only count pages in the current version (for versioned projects, counts only `current` or `manual` version directories; for non-versioned projects, counts all pages) +- `--by-version` - Display counts grouped by project and version (shows version breakdown for versioned projects; non-versioned projects show as "(no version)") + +**Output:** + +By default, prints a single integer (total count) for use in CI or scripting. With `--count-by-project`, displays a formatted table with project names and counts. With `--by-version`, displays a hierarchical breakdown by project and version. + +**Versioned Documentation:** + +Some MongoDB documentation projects contain multiple versions, represented as distinct directories between the project directory and the `source` directory: +- **Versioned project structure**: `content/{project}/{version}/source/...` +- **Non-versioned project structure**: `content/{project}/source/...` + +Version directory names follow these patterns: +- `current` or `manual` - The current/latest version +- `upcoming` - Pre-release version +- `v{number}` - Specific version (e.g., `v8.0`, `v7.0`) + +The `--current-only` flag counts only files in the current version directory (`current` or `manual`) for versioned projects, while counting all files for non-versioned projects. + +The `--by-version` flag shows a breakdown of page counts for each version within each project. + +**Note:** The `--current-only` and `--by-version` flags are mutually exclusive. + +**Examples:** + +```bash +# Quick count for CI/CD +TOTAL_PAGES=$(./audit-cli count pages ~/docs-monorepo) +echo "Total documentation pages: $TOTAL_PAGES" + +# Detailed breakdown by project +./audit-cli count pages ~/docs-monorepo --count-by-project +# Output: +# Page Counts by Project: +# +# app-services 245 +# atlas 512 +# manual 1024 +# ... +# +# Total: 2891 + +# Count only Atlas pages +./audit-cli count pages ~/docs-monorepo --for-project atlas +# Output: 512 + +# Exclude deprecated content +./audit-cli count pages ~/docs-monorepo --exclude-dirs deprecated,archive --count-by-project + +# Count only current versions +./audit-cli count pages ~/docs-monorepo --current-only +# Output: 1245 (only counts current/manual versions) + +# Show breakdown by version +./audit-cli count pages ~/docs-monorepo --by-version +# Output: +# Project: drivers +# manual 150 +# upcoming 145 +# v8.0 140 +# v7.0 135 +# +# Project: atlas +# (no version) 200 +# +# Total: 770 + +# Count current version for a specific project +./audit-cli count pages ~/docs-monorepo --for-project drivers --current-only +# Output: 150 +``` + ## Development ### Project Structure @@ -979,16 +1166,30 @@ audit-cli/ │ │ ├── analyzer.go # Reference finding logic │ │ ├── output.go # Output formatting │ │ └── types.go # Type definitions -│ └── compare/ # Compare parent command -│ ├── compare.go # Parent command definition -│ └── file-contents/ # File contents comparison subcommand -│ ├── file_contents.go # Command logic -│ ├── file_contents_test.go # Tests -│ ├── comparer.go # Comparison logic -│ ├── differ.go # Diff generation +│ ├── compare/ # Compare parent command +│ │ ├── compare.go # Parent command definition +│ │ └── file-contents/ # File contents comparison subcommand +│ │ ├── file_contents.go # Command logic +│ │ ├── file_contents_test.go # Tests +│ │ ├── comparer.go # Comparison logic +│ │ ├── differ.go # Diff generation +│ │ ├── output.go # Output formatting +│ │ ├── types.go # Type definitions +│ │ └── version_resolver.go # Version path resolution +│ └── count/ # Count parent command +│ ├── count.go # Parent command definition +│ ├── tested-examples/ # Tested examples counting subcommand +│ │ ├── tested_examples.go # Command logic +│ │ ├── tested_examples_test.go # Tests +│ │ ├── counter.go # Counting logic +│ │ ├── output.go # Output formatting +│ │ └── types.go # Type definitions +│ └── pages/ # Pages counting subcommand +│ ├── pages.go # Command logic +│ ├── pages_test.go # Tests +│ ├── counter.go # Counting logic │ ├── output.go # Output formatting -│ ├── types.go # Type definitions -│ └── version_resolver.go # Version path resolution +│ └── types.go # Type definitions ├── internal/ # Internal packages │ ├── pathresolver/ # Path resolution utilities │ │ ├── pathresolver.go # Core path resolution @@ -1014,12 +1215,14 @@ audit-cli/ │ ├── includes/ # Included RST files │ └── code-examples/ # Code files for literalinclude ├── expected-output/ # Expected extraction results - └── compare/ # Compare command test data - ├── product/ # Version structure tests - │ ├── manual/ # Manual version - │ ├── upcoming/ # Upcoming version - │ └── v8.0/ # v8.0 version - └── *.txt # Direct comparison tests + ├── compare/ # Compare command test data + │ ├── product/ # Version structure tests + │ │ ├── manual/ # Manual version + │ │ ├── upcoming/ # Upcoming version + │ │ └── v8.0/ # v8.0 version + │ └── *.txt # Direct comparison tests + └── count-test-monorepo/ # Count command test data + └── content/code-examples/tested/ # Tested examples structure ``` ### Adding New Commands diff --git a/audit-cli/commands/count/count.go b/audit-cli/commands/count/count.go new file mode 100644 index 0000000..4d291c2 --- /dev/null +++ b/audit-cli/commands/count/count.go @@ -0,0 +1,36 @@ +// Package count provides the parent command for counting code examples and documentation pages. +// +// This package serves as the parent command for various counting operations. +// Currently supports: +// - tested-examples: Count tested code examples in the MongoDB documentation monorepo +// - pages: Count documentation pages (.txt files) in the MongoDB documentation monorepo +package count + +import ( + "github.com/mongodb/code-example-tooling/audit-cli/commands/count/pages" + "github.com/mongodb/code-example-tooling/audit-cli/commands/count/tested-examples" + "github.com/spf13/cobra" +) + +// NewCountCommand creates the count parent command. +// +// This command serves as a parent for various counting operations on code examples and documentation pages. +// It doesn't perform any operations itself but provides a namespace for subcommands. +func NewCountCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "count", + Short: "Count code examples and documentation pages", + Long: `Count various types of content in the MongoDB documentation. + +Currently supports: + - tested-examples: Count tested code examples in the documentation monorepo + - pages: Count documentation pages (.txt files) in the documentation monorepo`, + } + + // Add subcommands + cmd.AddCommand(tested_examples.NewTestedExamplesCommand()) + cmd.AddCommand(pages.NewPagesCommand()) + + return cmd +} + diff --git a/audit-cli/commands/count/pages/counter.go b/audit-cli/commands/count/pages/counter.go new file mode 100644 index 0000000..9b99f16 --- /dev/null +++ b/audit-cli/commands/count/pages/counter.go @@ -0,0 +1,322 @@ +// Package pages provides counting functionality for documentation pages. +package pages + +import ( + "fmt" + "os" + "path/filepath" + "strings" +) + +// CountPages counts .txt files in the content directory. +// +// This function navigates to the content directory from the monorepo root +// and counts .txt files based on the specified filters. +// +// Parameters: +// - dirPath: Path to the directory to count (can be monorepo root or content dir) +// - forProject: If non-empty, only count files for this project +// - excludeDirs: List of directory names to exclude from counting +// - currentOnly: If true, only count files in the current version +// - byVersion: If true, track counts by version +// +// Returns: +// - *CountResult: The counting results +// - error: Any error encountered during counting +func CountPages(dirPath string, forProject string, excludeDirs []string, currentOnly bool, byVersion bool) (*CountResult, error) { + // Get absolute path + absDirPath, err := filepath.Abs(dirPath) + if err != nil { + return nil, fmt.Errorf("failed to get absolute path: %w", err) + } + + // Check if path exists + if _, err := os.Stat(absDirPath); os.IsNotExist(err) { + return nil, fmt.Errorf("directory does not exist: %s", absDirPath) + } + + // Find the content directory + contentDir, err := findContentDirectory(absDirPath) + if err != nil { + return nil, err + } + + result := &CountResult{ + TotalCount: 0, + ProjectCounts: make(map[string]int), + VersionCounts: make(map[string]map[string]int), + ContentDir: contentDir, + } + + // Default exclusions at the root of content or source + defaultExclusions := map[string]bool{ + "404": true, + "docs-platform": true, + "meta": true, + "table-of-contents": true, + } + + // Add user-specified exclusions + userExclusions := make(map[string]bool) + for _, dir := range excludeDirs { + userExclusions[dir] = true + } + + // Walk through the content directory + err = filepath.Walk(contentDir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + // Get relative path from content directory + relPath, err := filepath.Rel(contentDir, path) + if err != nil { + return err + } + + // Skip the content directory itself + if relPath == "." { + return nil + } + + // Check if this is a directory we should skip + if info.IsDir() { + dirName := info.Name() + + // Check if this is a code-examples directory at root of content or source + if dirName == "code-examples" { + parentDir := filepath.Dir(path) + // Skip if at root of content + if parentDir == contentDir { + return filepath.SkipDir + } + // Skip if at root of source (content/project/source/code-examples) + if filepath.Base(parentDir) == "source" { + grandparentDir := filepath.Dir(parentDir) + // Check if grandparent is a direct child of content + if filepath.Dir(grandparentDir) == contentDir { + return filepath.SkipDir + } + } + } + + // Check default exclusions (only at root of content) + if filepath.Dir(path) == contentDir && defaultExclusions[dirName] { + return filepath.SkipDir + } + + // Check user exclusions (anywhere in the tree) + if userExclusions[dirName] { + return filepath.SkipDir + } + + return nil + } + + // Only count .txt files + if filepath.Ext(path) != ".txt" { + return nil + } + + // Extract project name from path (first directory under content) + projectName := extractProjectName(relPath) + if projectName == "" { + // File is directly in content directory, not in a project + return nil + } + + // If filtering by project, check if this file matches + if forProject != "" && projectName != forProject { + return nil + } + + // Extract version information if needed + var versionName string + if byVersion || currentOnly { + projectDir := filepath.Join(contentDir, projectName) + versionName = extractVersionFromPath(relPath, projectName) + + // If currentOnly is set, check if this file is in the current version + if currentOnly { + // Check if project is versioned + versions, err := findVersionDirectories(projectDir) + if err != nil { + return err + } + + // For non-versioned projects, versionName will be empty, which is fine + if len(versions) > 0 { + // This is a versioned project - only count if in current version + if !isCurrentVersion(versionName) { + return nil + } + } + } + } + + // Count this file + result.TotalCount++ + result.ProjectCounts[projectName]++ + + // Track by version if requested + if byVersion { + if result.VersionCounts[projectName] == nil { + result.VersionCounts[projectName] = make(map[string]int) + } + result.VersionCounts[projectName][versionName]++ + } + + return nil + }) + + if err != nil { + return nil, fmt.Errorf("failed to walk content directory: %w", err) + } + + return result, nil +} + +// findContentDirectory finds the content directory from the given path. +// It checks if the path is already a content directory, or if it contains one. +func findContentDirectory(dirPath string) (string, error) { + // Check if this is already a content directory + if filepath.Base(dirPath) == "content" { + return dirPath, nil + } + + // Check if there's a content subdirectory + contentDir := filepath.Join(dirPath, "content") + if _, err := os.Stat(contentDir); err == nil { + return contentDir, nil + } + + return "", fmt.Errorf("content directory not found in: %s\n\nPlease provide the path to the monorepo root or content directory", dirPath) +} + +// extractProjectName extracts the project name from a relative path. +// Returns the first directory component, which represents the project. +func extractProjectName(relPath string) string { + parts := strings.Split(relPath, string(filepath.Separator)) + if len(parts) < 1 { + return "" + } + return parts[0] +} + +// extractVersionFromPath extracts the version name from a relative path. +// For versioned projects: content/project/version/source/file.txt -> "version" +// For non-versioned projects: content/project/source/file.txt -> "" +// Parameters: +// - relPath: Relative path from content directory +// - projectName: Name of the project (first directory component) +// +// Returns the version name, or empty string if non-versioned +func extractVersionFromPath(relPath string, projectName string) string { + parts := strings.Split(relPath, string(filepath.Separator)) + + // Need at least: project/version/source/file or project/source/file + if len(parts) < 3 { + return "" + } + + // parts[0] is the project name + // parts[1] could be either "source" (non-versioned) or version name (versioned) + if parts[1] == "source" { + // Non-versioned project + return "" + } + + // Check if parts[1] looks like a version directory + if isVersionDirectory(parts[1]) { + return parts[1] + } + + return "" +} + +// isVersionDirectory checks if a directory name looks like a version directory. +// Version directories can be: +// - "current" or "manual" (current version) +// - "upcoming" (upcoming version) +// - Starting with "v" (e.g., "v8.0", "v7.3") +func isVersionDirectory(dirName string) bool { + if dirName == "current" || dirName == "manual" || dirName == "upcoming" { + return true + } + return strings.HasPrefix(dirName, "v") +} + +// isCurrentVersion checks if a version name represents the current version. +// The current version is either "current" or "manual". +func isCurrentVersion(versionName string) bool { + return versionName == "current" || versionName == "manual" +} + +// findVersionDirectories finds all version directories within a project directory. +// Returns a list of VersionInfo structs with version names and whether they're current. +// If the project has no versions (source is directly under project), returns empty slice. +func findVersionDirectories(projectDir string) ([]VersionInfo, error) { + entries, err := os.ReadDir(projectDir) + if err != nil { + return nil, fmt.Errorf("failed to read project directory: %w", err) + } + + var versions []VersionInfo + hasSourceDir := false + + for _, entry := range entries { + if !entry.IsDir() { + continue + } + + dirName := entry.Name() + + // Check if there's a direct "source" directory (non-versioned project) + if dirName == "source" { + hasSourceDir = true + continue + } + + // Check if this looks like a version directory + if isVersionDirectory(dirName) { + // Verify it has a source subdirectory + sourceDir := filepath.Join(projectDir, dirName, "source") + if _, err := os.Stat(sourceDir); err == nil { + versions = append(versions, VersionInfo{ + Name: dirName, + IsCurrent: isCurrentVersion(dirName), + }) + } + } + } + + // If there's a direct source directory, this is a non-versioned project + if hasSourceDir { + return []VersionInfo{}, nil + } + + return versions, nil +} + +// getCurrentVersion finds the current version directory within a project. +// Returns the version name if found, empty string if not found or non-versioned. +func getCurrentVersion(projectDir string) (string, error) { + versions, err := findVersionDirectories(projectDir) + if err != nil { + return "", err + } + + // Non-versioned project + if len(versions) == 0 { + return "", nil + } + + // Find the current version + for _, v := range versions { + if v.IsCurrent { + return v.Name, nil + } + } + + return "", fmt.Errorf("no current version found in project directory: %s", projectDir) +} diff --git a/audit-cli/commands/count/pages/output.go b/audit-cli/commands/count/pages/output.go new file mode 100644 index 0000000..dc852a4 --- /dev/null +++ b/audit-cli/commands/count/pages/output.go @@ -0,0 +1,106 @@ +// Package pages provides output formatting for page count results. +package pages + +import ( + "fmt" + "sort" +) + +// PrintResults prints the counting results. +// +// If countByProject is true, prints a breakdown by project. +// If byVersion is true, prints a breakdown by project and version. +// Otherwise, prints only the total count. +// +// Parameters: +// - result: The counting results +// - countByProject: If true, show breakdown by project +// - byVersion: If true, show breakdown by project and version +func PrintResults(result *CountResult, countByProject bool, byVersion bool) { + if byVersion { + printByVersion(result) + } else if countByProject { + printByProject(result) + } else { + printTotal(result) + } +} + +// printTotal prints only the total count as a single integer. +func printTotal(result *CountResult) { + fmt.Println(result.TotalCount) +} + +// printByProject prints a breakdown of counts by project. +func printByProject(result *CountResult) { + if len(result.ProjectCounts) == 0 { + fmt.Println("No pages found") + return + } + + // Get sorted list of project names + var projectNames []string + for name := range result.ProjectCounts { + projectNames = append(projectNames, name) + } + sort.Strings(projectNames) + + // Print header + fmt.Println("Page Counts by Project:") + fmt.Println() + + // Print each project with its count + for _, name := range projectNames { + count := result.ProjectCounts[name] + fmt.Printf(" %-30s %5d\n", name, count) + } + + // Print total + fmt.Println() + fmt.Printf("Total: %d\n", result.TotalCount) +} + +// printByVersion prints a breakdown of counts by project and version. +func printByVersion(result *CountResult) { + if len(result.VersionCounts) == 0 { + fmt.Println("No pages found") + return + } + + // Get sorted list of project names + var projectNames []string + for name := range result.VersionCounts { + projectNames = append(projectNames, name) + } + sort.Strings(projectNames) + + // Print each project with its versions + for _, projectName := range projectNames { + versionCounts := result.VersionCounts[projectName] + + fmt.Printf("Project: %s\n", projectName) + + // Get sorted list of version names + var versionNames []string + for version := range versionCounts { + versionNames = append(versionNames, version) + } + sort.Strings(versionNames) + + // Print each version with its count + for _, versionName := range versionNames { + count := versionCounts[versionName] + displayName := versionName + if displayName == "" { + displayName = "(no version)" + } + fmt.Printf(" %-28s %5d\n", displayName, count) + } + + fmt.Println() + } + + // Print total + fmt.Printf("Total: %d\n", result.TotalCount) +} + diff --git a/audit-cli/commands/count/pages/pages.go b/audit-cli/commands/count/pages/pages.go new file mode 100644 index 0000000..d7ed726 --- /dev/null +++ b/audit-cli/commands/count/pages/pages.go @@ -0,0 +1,128 @@ +// Package pages implements the pages subcommand for counting documentation pages. +package pages + +import ( + "fmt" + "strings" + + "github.com/spf13/cobra" +) + +// NewPagesCommand creates the pages subcommand. +// +// This command counts documentation pages (.txt files) in the MongoDB documentation monorepo. +// +// Usage: +// +// count pages /path/to/docs-monorepo +// count pages /path/to/docs-monorepo --for-project manual +// count pages /path/to/docs-monorepo --count-by-project +// count pages /path/to/docs-monorepo --exclude-dirs api-reference,generated +// +// Flags: +// - --for-project: Only count pages for a specific project +// - --count-by-project: Display a list of projects with counts for each +// - --exclude-dirs: Comma-separated list of directory names to exclude +func NewPagesCommand() *cobra.Command { + var ( + forProject string + countByProject bool + excludeDirs string + currentOnly bool + byVersion bool + ) + + cmd := &cobra.Command{ + Use: "pages [directory-path]", + Short: "Count documentation pages in the monorepo", + Long: `Count documentation pages (.txt files) in the MongoDB documentation monorepo. + +This command navigates to the content directory and counts all .txt files recursively, +with the following exclusions: + +Automatic exclusions: + - Files in code-examples directories (at root of content or source) + - Files in the following directories at the root of content: + - 404 + - docs-platform + - meta + - table-of-contents + - All non-.txt files + +Each directory under content/ represents a different product/project. + +By default, returns only a total count of all pages. + +Examples: + # Get total count of all documentation pages + count pages /path/to/docs-monorepo + + # Count pages for a specific project + count pages /path/to/docs-monorepo --for-project manual + + # Show counts broken down by project + count pages /path/to/docs-monorepo --count-by-project + + # Exclude specific directories from counting + count pages /path/to/docs-monorepo --exclude-dirs api-reference,generated + + # Combine flags: count pages for a specific project, excluding certain directories + count pages /path/to/docs-monorepo --for-project atlas --exclude-dirs deprecated + + # Count only current versions + count pages /path/to/docs-monorepo --current-only + + # Show counts by version + count pages /path/to/docs-monorepo --by-version`, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return runPages(args[0], forProject, countByProject, excludeDirs, currentOnly, byVersion) + }, + } + + cmd.Flags().StringVar(&forProject, "for-project", "", "Only count pages for a specific project") + cmd.Flags().BoolVar(&countByProject, "count-by-project", false, "Display counts for each project") + cmd.Flags().StringVar(&excludeDirs, "exclude-dirs", "", "Comma-separated list of directory names to exclude") + cmd.Flags().BoolVar(¤tOnly, "current-only", false, "Only count pages in the current version") + cmd.Flags().BoolVar(&byVersion, "by-version", false, "Display counts grouped by project and version") + + return cmd +} + +// runPages executes the pages counting operation. +func runPages(dirPath string, forProject string, countByProject bool, excludeDirs string, currentOnly bool, byVersion bool) error { + // Validate flag combinations + if forProject != "" && countByProject { + return fmt.Errorf("cannot use --for-project and --count-by-project together") + } + + if currentOnly && byVersion { + return fmt.Errorf("cannot use --current-only and --by-version together") + } + + // If byVersion is set, it implies countByProject + if byVersion { + countByProject = true + } + + // Parse exclude-dirs flag + var excludeDirsList []string + if excludeDirs != "" { + excludeDirsList = strings.Split(excludeDirs, ",") + // Trim whitespace from each directory name + for i, dir := range excludeDirsList { + excludeDirsList[i] = strings.TrimSpace(dir) + } + } + + // Count the pages + result, err := CountPages(dirPath, forProject, excludeDirsList, currentOnly, byVersion) + if err != nil { + return fmt.Errorf("failed to count pages: %w", err) + } + + // Print the results + PrintResults(result, countByProject, byVersion) + + return nil +} diff --git a/audit-cli/commands/count/pages/pages_test.go b/audit-cli/commands/count/pages/pages_test.go new file mode 100644 index 0000000..41c0113 --- /dev/null +++ b/audit-cli/commands/count/pages/pages_test.go @@ -0,0 +1,202 @@ +// Package pages provides tests for the pages counting functionality. +package pages + +import ( + "path/filepath" + "testing" +) + +// TestCountPages tests the basic page counting functionality. +func TestCountPages(t *testing.T) { + testDataDir := filepath.Join("..", "..", "..", "testdata", "count-test-monorepo") + + result, err := CountPages(testDataDir, "", nil, false, false) + if err != nil { + t.Fatalf("CountPages failed: %v", err) + } + + // Expected: 3 manual + 2 atlas + 1 app-services + 1 shared + 1 deprecated + 7 drivers = 15 + // Excluded: 404, meta, table-of-contents, code-examples at root + expectedTotal := 15 + if result.TotalCount != expectedTotal { + t.Errorf("Expected total count %d, got %d", expectedTotal, result.TotalCount) + } + + // Check individual project counts + expectedCounts := map[string]int{ + "manual": 4, // index, tutorial, reference, deprecated/old + "atlas": 2, // getting-started, clusters + "app-services": 1, // index + "shared": 1, // include + "drivers": 7, // manual(2) + v8.0(2) + v7.0(1) + upcoming(2) + } + + for project, expectedCount := range expectedCounts { + if result.ProjectCounts[project] != expectedCount { + t.Errorf("Expected %s count %d, got %d", project, expectedCount, result.ProjectCounts[project]) + } + } +} + +// TestCountPagesForProject tests filtering by project. +func TestCountPagesForProject(t *testing.T) { + testDataDir := filepath.Join("..", "..", "..", "testdata", "count-test-monorepo") + + result, err := CountPages(testDataDir, "manual", nil, false, false) + if err != nil { + t.Fatalf("CountPages failed: %v", err) + } + + expectedTotal := 4 // index, tutorial, reference, deprecated/old + if result.TotalCount != expectedTotal { + t.Errorf("Expected total count %d, got %d", expectedTotal, result.TotalCount) + } + + // Should only have manual in the counts + if len(result.ProjectCounts) != 1 { + t.Errorf("Expected 1 project in counts, got %d", len(result.ProjectCounts)) + } + + if result.ProjectCounts["manual"] != expectedTotal { + t.Errorf("Expected manual count %d, got %d", expectedTotal, result.ProjectCounts["manual"]) + } +} + +// TestCountPagesWithExclusions tests excluding directories. +func TestCountPagesWithExclusions(t *testing.T) { + testDataDir := filepath.Join("..", "..", "..", "testdata", "count-test-monorepo") + + result, err := CountPages(testDataDir, "", []string{"deprecated"}, false, false) + if err != nil { + t.Fatalf("CountPages failed: %v", err) + } + + // Expected: 3 manual + 2 atlas + 1 app-services + 1 shared + 7 drivers = 14 + // Excluded: deprecated directory + expectedTotal := 14 + if result.TotalCount != expectedTotal { + t.Errorf("Expected total count %d, got %d", expectedTotal, result.TotalCount) + } + + // Manual should have 3 files (not 4, since deprecated is excluded) + if result.ProjectCounts["manual"] != 3 { + t.Errorf("Expected manual count 3, got %d", result.ProjectCounts["manual"]) + } +} + +// TestCountPagesExcludesDefaultDirectories tests that default exclusions work. +func TestCountPagesExcludesDefaultDirectories(t *testing.T) { + testDataDir := filepath.Join("..", "..", "..", "testdata", "count-test-monorepo") + + result, err := CountPages(testDataDir, "", nil, false, false) + if err != nil { + t.Fatalf("CountPages failed: %v", err) + } + + // Should not include 404, meta, table-of-contents, or code-examples at root + excludedProjects := []string{"404", "meta", "table-of-contents", "code-examples"} + for _, project := range excludedProjects { + if count, exists := result.ProjectCounts[project]; exists && count > 0 { + t.Errorf("Expected %s to be excluded, but found %d files", project, count) + } + } +} + +// TestCountPagesNonTxtFiles tests that non-.txt files are not counted. +func TestCountPagesNonTxtFiles(t *testing.T) { + testDataDir := filepath.Join("..", "..", "..", "testdata", "count-test-monorepo") + + result, err := CountPages(testDataDir, "manual", nil, false, false) + if err != nil { + t.Fatalf("CountPages failed: %v", err) + } + + // Manual has config.yaml which should not be counted + // Only .txt files should be counted + expectedTotal := 4 // index.txt, tutorial.txt, reference.txt, deprecated/old.txt + if result.TotalCount != expectedTotal { + t.Errorf("Expected total count %d (only .txt files), got %d", expectedTotal, result.TotalCount) + } +} + +// TestCountPagesCodeExamplesInSubdirectory tests that code-examples subdirectories are NOT excluded. +func TestCountPagesCodeExamplesInSubdirectory(t *testing.T) { + testDataDir := filepath.Join("..", "..", "..", "testdata", "count-test-monorepo") + + result, err := CountPages(testDataDir, "manual", nil, false, false) + if err != nil { + t.Fatalf("CountPages failed: %v", err) + } + + // Manual has a code-examples subdirectory with example.txt + // Expected: index, tutorial, reference, deprecated/old = 4 + expectedTotal := 4 // We're excluding code-examples at source level too + if result.TotalCount != expectedTotal { + t.Errorf("Expected total count %d, got %d", expectedTotal, result.TotalCount) + } +} + +// TestCountPagesCurrentOnly tests the --current-only flag. +func TestCountPagesCurrentOnly(t *testing.T) { + testDataDir := filepath.Join("..", "..", "..", "testdata", "count-test-monorepo") + + result, err := CountPages(testDataDir, "", nil, true, false) + if err != nil { + t.Fatalf("CountPages failed: %v", err) + } + + // Expected: 4 manual (non-versioned) + 2 atlas + 1 app-services + 1 shared + 2 drivers (manual version only) = 10 + expectedTotal := 10 + if result.TotalCount != expectedTotal { + t.Errorf("Expected total count %d, got %d", expectedTotal, result.TotalCount) + } + + // Drivers should only have 2 files (from manual version, not v8.0, v7.0, or upcoming) + if result.ProjectCounts["drivers"] != 2 { + t.Errorf("Expected drivers count 2 (current version only), got %d", result.ProjectCounts["drivers"]) + } +} + +// TestCountPagesByVersion tests the --by-version flag. +func TestCountPagesByVersion(t *testing.T) { + testDataDir := filepath.Join("..", "..", "..", "testdata", "count-test-monorepo") + + result, err := CountPages(testDataDir, "", nil, false, true) + if err != nil { + t.Fatalf("CountPages failed: %v", err) + } + + // Check that version counts are populated + if len(result.VersionCounts) == 0 { + t.Fatal("Expected version counts to be populated") + } + + // Check drivers project has all versions + driversVersions := result.VersionCounts["drivers"] + if driversVersions == nil { + t.Fatal("Expected drivers to have version counts") + } + + expectedDriversVersions := map[string]int{ + "manual": 2, // index, tutorial + "v8.0": 2, // index, tutorial + "v7.0": 1, // index + "upcoming": 2, // index, tutorial + } + + for version, expectedCount := range expectedDriversVersions { + if driversVersions[version] != expectedCount { + t.Errorf("Expected drivers/%s count %d, got %d", version, expectedCount, driversVersions[version]) + } + } + + // Check non-versioned projects have empty version string + atlasVersions := result.VersionCounts["atlas"] + if atlasVersions == nil { + t.Fatal("Expected atlas to have version counts") + } + if atlasVersions[""] != 2 { + t.Errorf("Expected atlas/(no version) count 2, got %d", atlasVersions[""]) + } +} + diff --git a/audit-cli/commands/count/pages/types.go b/audit-cli/commands/count/pages/types.go new file mode 100644 index 0000000..96a21ee --- /dev/null +++ b/audit-cli/commands/count/pages/types.go @@ -0,0 +1,25 @@ +// Package pages provides functionality for counting documentation pages. +package pages + +// CountResult represents the result of counting pages. +type CountResult struct { + // TotalCount is the total number of .txt files counted + TotalCount int + // ProjectCounts maps project directory names to their page counts + ProjectCounts map[string]int + // VersionCounts maps project names to version names to counts + // For versioned projects: {"manual": {"manual": 100, "v8.0": 95}} + // For non-versioned projects: {"atlas": {"": 200}} + VersionCounts map[string]map[string]int + // ContentDir is the path to the content directory + ContentDir string +} + +// VersionInfo contains information about a version directory. +type VersionInfo struct { + // Name is the version directory name (e.g., "manual", "v8.0", "current") + Name string + // IsCurrent indicates if this is the current version + IsCurrent bool +} + diff --git a/audit-cli/commands/count/tested-examples/counter.go b/audit-cli/commands/count/tested-examples/counter.go new file mode 100644 index 0000000..9a4c934 --- /dev/null +++ b/audit-cli/commands/count/tested-examples/counter.go @@ -0,0 +1,163 @@ +// Package tested_examples provides counting functionality for tested code examples. +package tested_examples + +import ( + "fmt" + "os" + "path/filepath" + "strings" +) + +// CountTestedExamples counts tested code examples in the monorepo. +// +// This function navigates to content/code-examples/tested from the monorepo root +// and counts files based on the specified filters. +// +// Parameters: +// - monorepoPath: Path to the documentation monorepo root +// - forProduct: If non-empty, only count files for this product +// - excludeOutput: If true, exclude output files (.txt, .sh) +// +// Returns: +// - *CountResult: The counting results +// - error: Any error encountered during counting +func CountTestedExamples(monorepoPath string, forProduct string, excludeOutput bool) (*CountResult, error) { + // Get absolute path to monorepo + absMonorepoPath, err := filepath.Abs(monorepoPath) + if err != nil { + return nil, fmt.Errorf("failed to get absolute path: %w", err) + } + + // Check if monorepo path exists + if _, err := os.Stat(absMonorepoPath); os.IsNotExist(err) { + return nil, fmt.Errorf("monorepo path does not exist: %s", absMonorepoPath) + } + + // Navigate to tested directory + testedDir := filepath.Join(absMonorepoPath, "content", "code-examples", "tested") + if _, err := os.Stat(testedDir); os.IsNotExist(err) { + return nil, fmt.Errorf("tested directory does not exist: %s\n\nPlease ensure you provided the path to the monorepo root", testedDir) + } + + result := &CountResult{ + TotalCount: 0, + ProductCounts: make(map[string]int), + TestedDir: testedDir, + } + + // Walk through the tested directory + err = filepath.Walk(testedDir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + // Skip directories + if info.IsDir() { + return nil + } + + // Get the file extension + ext := filepath.Ext(path) + + // If excluding output files, skip .txt and .sh files + if excludeOutput && isOutputFile(ext) { + return nil + } + + // Get relative path from tested directory + relPath, err := filepath.Rel(testedDir, path) + if err != nil { + return err + } + + // Extract product key from path + // Path structure: // + productKey := extractProductKey(relPath) + if productKey == "" { + // Skip files not in a product directory + return nil + } + + // If filtering by product, check if this file matches + if forProduct != "" && productKey != forProduct { + return nil + } + + // If excluding output, verify this is a source file for the product + if excludeOutput && !isSourceFileForProduct(ext, productKey) { + return nil + } + + // Count this file + result.TotalCount++ + result.ProductCounts[productKey]++ + + return nil + }) + + if err != nil { + return nil, fmt.Errorf("failed to walk tested directory: %w", err) + } + + return result, nil +} + +// extractProductKey extracts the product key from a relative path. +// +// Path structure: // +// Returns: "/" or just "" for special cases +func extractProductKey(relPath string) string { + parts := strings.Split(relPath, string(filepath.Separator)) + if len(parts) < 2 { + return "" + } + + language := parts[0] + product := parts[1] + + // Special case for mongosh (command-line/mongosh) + if language == "command-line" && product == "mongosh" { + return "mongosh" + } + + // Special case for pymongo (python/pymongo) + if language == "python" && product == "pymongo" { + return "pymongo" + } + + // For all other products, use language/product format + productKey := language + "/" + product + + // Verify this is a known product + if IsValidProduct(productKey) { + return productKey + } + + return "" +} + +// isOutputFile checks if a file extension represents an output file. +func isOutputFile(ext string) bool { + for _, outputExt := range OutputExtensions { + if ext == outputExt { + return true + } + } + return false +} + +// isSourceFileForProduct checks if a file extension is a source file for the given product. +func isSourceFileForProduct(ext string, productKey string) bool { + productInfo, exists := ProductMap[productKey] + if !exists { + return false + } + + for _, sourceExt := range productInfo.SourceExtensions { + if ext == sourceExt { + return true + } + } + return false +} + diff --git a/audit-cli/commands/count/tested-examples/output.go b/audit-cli/commands/count/tested-examples/output.go new file mode 100644 index 0000000..9f9f4e2 --- /dev/null +++ b/audit-cli/commands/count/tested-examples/output.go @@ -0,0 +1,59 @@ +// Package tested_examples provides output formatting for count results. +package tested_examples + +import ( + "fmt" + "sort" +) + +// PrintResults prints the counting results. +// +// If countByProduct is true, prints a breakdown by product. +// Otherwise, prints only the total count. +// +// Parameters: +// - result: The counting results +// - countByProduct: If true, show breakdown by product +func PrintResults(result *CountResult, countByProduct bool) { + if countByProduct { + printByProduct(result) + } else { + printTotal(result) + } +} + +// printTotal prints only the total count as a single integer. +func printTotal(result *CountResult) { + fmt.Println(result.TotalCount) +} + +// printByProduct prints a breakdown of counts by product. +func printByProduct(result *CountResult) { + if len(result.ProductCounts) == 0 { + fmt.Println("No files found") + return + } + + // Get sorted list of product keys + var productKeys []string + for key := range result.ProductCounts { + productKeys = append(productKeys, key) + } + sort.Strings(productKeys) + + // Print header + fmt.Println("Product Counts:") + fmt.Println() + + // Print each product with its count + for _, key := range productKeys { + count := result.ProductCounts[key] + productInfo := ProductMap[key] + fmt.Printf(" %-25s %5d (%s)\n", key, count, productInfo.Name) + } + + // Print total + fmt.Println() + fmt.Printf("Total: %d\n", result.TotalCount) +} + diff --git a/audit-cli/commands/count/tested-examples/tested_examples.go b/audit-cli/commands/count/tested-examples/tested_examples.go new file mode 100644 index 0000000..7f5e465 --- /dev/null +++ b/audit-cli/commands/count/tested-examples/tested_examples.go @@ -0,0 +1,98 @@ +// Package tested_examples implements the tested-examples subcommand for counting code examples. +package tested_examples + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +// NewTestedExamplesCommand creates the tested-examples subcommand. +// +// This command counts tested code examples in the MongoDB documentation monorepo. +// +// Usage: +// count tested-examples /path/to/docs-monorepo +// count tested-examples /path/to/docs-monorepo --for-product pymongo +// count tested-examples /path/to/docs-monorepo --count-by-product +// count tested-examples /path/to/docs-monorepo --exclude-output +// +// Flags: +// - --for-product: Only count code examples for a specific product +// - --count-by-product: Display a list of products with counts for each +// - --exclude-output: Only count source files, excluding output files (.txt, .sh) +func NewTestedExamplesCommand() *cobra.Command { + var ( + forProduct string + countByProduct bool + excludeOutput bool + ) + + cmd := &cobra.Command{ + Use: "tested-examples [monorepo-path]", + Short: "Count tested code examples in the documentation monorepo", + Long: `Count tested code examples in the MongoDB documentation monorepo. + +This command navigates to the content/code-examples/tested directory from the +monorepo root and counts all files recursively. + +The tested directory structure has two levels: + L1: Language directories (command-line, csharp, go, java, javascript, python) + L2: Product directories (mongosh, driver, atlas-sdk, driver-sync, pymongo, etc.) + +By default, returns only a total count of all files. + +` + GetProductList() + ` + +Examples: + # Get total count of all tested code examples + count tested-examples /path/to/docs-monorepo + + # Count examples for a specific product + count tested-examples /path/to/docs-monorepo --for-product pymongo + + # Show counts broken down by product + count tested-examples /path/to/docs-monorepo --count-by-product + + # Count only source files (exclude .txt and .sh output files) + count tested-examples /path/to/docs-monorepo --exclude-output + + # Combine flags: count source files for a specific product + count tested-examples /path/to/docs-monorepo --for-product pymongo --exclude-output`, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return runTestedExamples(args[0], forProduct, countByProduct, excludeOutput) + }, + } + + cmd.Flags().StringVar(&forProduct, "for-product", "", "Only count code examples for a specific product") + cmd.Flags().BoolVar(&countByProduct, "count-by-product", false, "Display counts for each product") + cmd.Flags().BoolVar(&excludeOutput, "exclude-output", false, "Only count source files (exclude .txt and .sh files)") + + return cmd +} + +// runTestedExamples executes the tested-examples counting operation. +func runTestedExamples(monorepoPath string, forProduct string, countByProduct bool, excludeOutput bool) error { + // Validate product if specified + if forProduct != "" && !IsValidProduct(forProduct) { + return fmt.Errorf("invalid product: %s\n\n%s", forProduct, GetProductList()) + } + + // Validate flag combinations + if forProduct != "" && countByProduct { + return fmt.Errorf("cannot use --for-product and --count-by-product together") + } + + // Count the files + result, err := CountTestedExamples(monorepoPath, forProduct, excludeOutput) + if err != nil { + return fmt.Errorf("failed to count tested examples: %w", err) + } + + // Print the results + PrintResults(result, countByProduct) + + return nil +} + diff --git a/audit-cli/commands/count/tested-examples/tested_examples_test.go b/audit-cli/commands/count/tested-examples/tested_examples_test.go new file mode 100644 index 0000000..0171367 --- /dev/null +++ b/audit-cli/commands/count/tested-examples/tested_examples_test.go @@ -0,0 +1,359 @@ +package tested_examples + +import ( + "path/filepath" + "testing" +) + +// TestCountAllFiles tests counting all files in the tested directory +func TestCountAllFiles(t *testing.T) { + testDataDir := filepath.Join("..", "..", "..", "testdata", "count-test-monorepo") + + result, err := CountTestedExamples(testDataDir, "", false) + if err != nil { + t.Fatalf("CountTestedExamples failed: %v", err) + } + + // Total files: + // pymongo: 4 files (2 .py, 1 .txt, 1 .sh) + // mongosh: 3 files (2 .js, 1 .txt) + // go/driver: 1 file (.go) + // go/atlas-sdk: 1 file (.go) + // javascript/driver: 2 files (1 .js, 1 .txt) + // java/driver-sync: 1 file (.java) + // csharp/driver: 1 file (.cs) + // Total: 13 files + expectedTotal := 13 + if result.TotalCount != expectedTotal { + t.Errorf("Expected total count %d, got %d", expectedTotal, result.TotalCount) + } +} + +// TestCountByProduct tests counting files for a specific product +func TestCountByProduct(t *testing.T) { + testDataDir := filepath.Join("..", "..", "..", "testdata", "count-test-monorepo") + + tests := []struct { + name string + product string + expectedCount int + }{ + { + name: "count pymongo files", + product: "pymongo", + expectedCount: 4, // 2 .py + 1 .txt + 1 .sh + }, + { + name: "count mongosh files", + product: "mongosh", + expectedCount: 3, // 2 .js + 1 .txt + }, + { + name: "count go/driver files", + product: "go/driver", + expectedCount: 1, // 1 .go + }, + { + name: "count go/atlas-sdk files", + product: "go/atlas-sdk", + expectedCount: 1, // 1 .go + }, + { + name: "count javascript/driver files", + product: "javascript/driver", + expectedCount: 2, // 1 .js + 1 .txt + }, + { + name: "count java/driver-sync files", + product: "java/driver-sync", + expectedCount: 1, // 1 .java + }, + { + name: "count csharp/driver files", + product: "csharp/driver", + expectedCount: 1, // 1 .cs + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := CountTestedExamples(testDataDir, tt.product, false) + if err != nil { + t.Fatalf("CountTestedExamples failed: %v", err) + } + + if result.TotalCount != tt.expectedCount { + t.Errorf("Expected count %d for product %s, got %d", tt.expectedCount, tt.product, result.TotalCount) + } + + // Verify product counts map + if result.ProductCounts[tt.product] != tt.expectedCount { + t.Errorf("Expected product count %d for %s, got %d", tt.expectedCount, tt.product, result.ProductCounts[tt.product]) + } + }) + } +} + +// TestExcludeOutput tests excluding output files (.txt, .sh) +func TestExcludeOutput(t *testing.T) { + testDataDir := filepath.Join("..", "..", "..", "testdata", "count-test-monorepo") + + result, err := CountTestedExamples(testDataDir, "", true) + if err != nil { + t.Fatalf("CountTestedExamples failed: %v", err) + } + + // Total source files only: + // pymongo: 2 .py files + // mongosh: 2 .js files + // go/driver: 1 .go file + // go/atlas-sdk: 1 .go file + // javascript/driver: 1 .js file + // java/driver-sync: 1 .java file + // csharp/driver: 1 .cs file + // Total: 9 files + expectedTotal := 9 + if result.TotalCount != expectedTotal { + t.Errorf("Expected total count %d (excluding output), got %d", expectedTotal, result.TotalCount) + } +} + +// TestExcludeOutputForProduct tests excluding output files for a specific product +func TestExcludeOutputForProduct(t *testing.T) { + testDataDir := filepath.Join("..", "..", "..", "testdata", "count-test-monorepo") + + tests := []struct { + name string + product string + expectedCount int + }{ + { + name: "pymongo source only", + product: "pymongo", + expectedCount: 2, // 2 .py files (excluding .txt and .sh) + }, + { + name: "mongosh source only", + product: "mongosh", + expectedCount: 2, // 2 .js files (excluding .txt) + }, + { + name: "javascript/driver source only", + product: "javascript/driver", + expectedCount: 1, // 1 .js file (excluding .txt) + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := CountTestedExamples(testDataDir, tt.product, true) + if err != nil { + t.Fatalf("CountTestedExamples failed: %v", err) + } + + if result.TotalCount != tt.expectedCount { + t.Errorf("Expected count %d for product %s (excluding output), got %d", tt.expectedCount, tt.product, result.TotalCount) + } + }) + } +} + +// TestProductCounts tests the product counts map +func TestProductCounts(t *testing.T) { + testDataDir := filepath.Join("..", "..", "..", "testdata", "count-test-monorepo") + + result, err := CountTestedExamples(testDataDir, "", false) + if err != nil { + t.Fatalf("CountTestedExamples failed: %v", err) + } + + expectedCounts := map[string]int{ + "pymongo": 4, + "mongosh": 3, + "go/driver": 1, + "go/atlas-sdk": 1, + "javascript/driver": 2, + "java/driver-sync": 1, + "csharp/driver": 1, + } + + for product, expectedCount := range expectedCounts { + actualCount := result.ProductCounts[product] + if actualCount != expectedCount { + t.Errorf("Expected %d files for product %s, got %d", expectedCount, product, actualCount) + } + } + + // Verify we have exactly the expected number of products + if len(result.ProductCounts) != len(expectedCounts) { + t.Errorf("Expected %d products, got %d", len(expectedCounts), len(result.ProductCounts)) + } +} + +// TestInvalidMonorepoPath tests error handling for invalid monorepo path +func TestInvalidMonorepoPath(t *testing.T) { + _, err := CountTestedExamples("/nonexistent/path", "", false) + if err == nil { + t.Error("Expected error for nonexistent monorepo path, got nil") + } +} + +// TestMissingTestedDirectory tests error handling when tested directory doesn't exist +func TestMissingTestedDirectory(t *testing.T) { + testDataDir := filepath.Join("..", "..", "..", "testdata") + + _, err := CountTestedExamples(testDataDir, "", false) + if err == nil { + t.Error("Expected error for missing tested directory, got nil") + } +} + +// TestIsValidProduct tests product validation +func TestIsValidProduct(t *testing.T) { + tests := []struct { + product string + valid bool + }{ + {"pymongo", true}, + {"mongosh", true}, + {"go/driver", true}, + {"go/atlas-sdk", true}, + {"javascript/driver", true}, + {"java/driver-sync", true}, + {"csharp/driver", true}, + {"invalid-product", false}, + {"python/driver", false}, + {"", false}, + } + + for _, tt := range tests { + t.Run(tt.product, func(t *testing.T) { + result := IsValidProduct(tt.product) + if result != tt.valid { + t.Errorf("IsValidProduct(%s) = %v, expected %v", tt.product, result, tt.valid) + } + }) + } +} + +// TestExtractProductKey tests product key extraction from paths +func TestExtractProductKey(t *testing.T) { + tests := []struct { + name string + relPath string + expectedKey string + }{ + { + name: "pymongo path", + relPath: "python/pymongo/example.py", + expectedKey: "pymongo", + }, + { + name: "mongosh path", + relPath: "command-line/mongosh/example.js", + expectedKey: "mongosh", + }, + { + name: "go driver path", + relPath: "go/driver/example.go", + expectedKey: "go/driver", + }, + { + name: "go atlas-sdk path", + relPath: "go/atlas-sdk/example.go", + expectedKey: "go/atlas-sdk", + }, + { + name: "javascript driver path", + relPath: "javascript/driver/example.js", + expectedKey: "javascript/driver", + }, + { + name: "java driver-sync path", + relPath: "java/driver-sync/Example.java", + expectedKey: "java/driver-sync", + }, + { + name: "csharp driver path", + relPath: "csharp/driver/Example.cs", + expectedKey: "csharp/driver", + }, + { + name: "invalid path - too short", + relPath: "python", + expectedKey: "", + }, + { + name: "invalid path - unknown product", + relPath: "python/unknown/example.py", + expectedKey: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := extractProductKey(tt.relPath) + if result != tt.expectedKey { + t.Errorf("extractProductKey(%s) = %s, expected %s", tt.relPath, result, tt.expectedKey) + } + }) + } +} + +// TestIsOutputFile tests output file detection +func TestIsOutputFile(t *testing.T) { + tests := []struct { + ext string + isOutput bool + }{ + {".txt", true}, + {".sh", true}, + {".py", false}, + {".js", false}, + {".go", false}, + {".java", false}, + {".cs", false}, + {".md", false}, + {"", false}, + } + + for _, tt := range tests { + t.Run(tt.ext, func(t *testing.T) { + result := isOutputFile(tt.ext) + if result != tt.isOutput { + t.Errorf("isOutputFile(%s) = %v, expected %v", tt.ext, result, tt.isOutput) + } + }) + } +} + +// TestIsSourceFileForProduct tests source file detection for products +func TestIsSourceFileForProduct(t *testing.T) { + tests := []struct { + name string + ext string + product string + isSource bool + }{ + {"python source for pymongo", ".py", "pymongo", true}, + {"txt not source for pymongo", ".txt", "pymongo", false}, + {"js source for mongosh", ".js", "mongosh", true}, + {"js source for javascript/driver", ".js", "javascript/driver", true}, + {"go source for go/driver", ".go", "go/driver", true}, + {"go source for go/atlas-sdk", ".go", "go/atlas-sdk", true}, + {"java source for java/driver-sync", ".java", "java/driver-sync", true}, + {"cs source for csharp/driver", ".cs", "csharp/driver", true}, + {"py not source for go/driver", ".py", "go/driver", false}, + {"invalid product", ".py", "invalid", false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := isSourceFileForProduct(tt.ext, tt.product) + if result != tt.isSource { + t.Errorf("isSourceFileForProduct(%s, %s) = %v, expected %v", tt.ext, tt.product, result, tt.isSource) + } + }) + } +} + diff --git a/audit-cli/commands/count/tested-examples/types.go b/audit-cli/commands/count/tested-examples/types.go new file mode 100644 index 0000000..a112284 --- /dev/null +++ b/audit-cli/commands/count/tested-examples/types.go @@ -0,0 +1,83 @@ +// Package tested_examples provides functionality for counting tested code examples. +package tested_examples + +// ProductInfo represents information about a MongoDB product. +type ProductInfo struct { + // Key is the product identifier used in commands (e.g., "pymongo", "go/driver") + Key string + // Name is the human-readable product name + Name string + // SourceExtensions are the file extensions for source code files + SourceExtensions []string +} + +// CountResult represents the result of counting files. +type CountResult struct { + // TotalCount is the total number of files counted + TotalCount int + // ProductCounts maps product keys to their file counts + ProductCounts map[string]int + // TestedDir is the path to the tested directory + TestedDir string +} + +// ProductMap defines the mapping of product keys to product information. +var ProductMap = map[string]ProductInfo{ + "mongosh": { + Key: "mongosh", + Name: "MongoDB Shell", + SourceExtensions: []string{".js"}, + }, + "csharp/driver": { + Key: "csharp/driver", + Name: "C#/.NET Driver", + SourceExtensions: []string{".cs"}, + }, + "go/driver": { + Key: "go/driver", + Name: "Go Driver", + SourceExtensions: []string{".go"}, + }, + "go/atlas-sdk": { + Key: "go/atlas-sdk", + Name: "Atlas Go SDK", + SourceExtensions: []string{".go"}, + }, + "java/driver-sync": { + Key: "java/driver-sync", + Name: "Java Sync Driver", + SourceExtensions: []string{".java"}, + }, + "javascript/driver": { + Key: "javascript/driver", + Name: "Node.js Driver", + SourceExtensions: []string{".js"}, + }, + "pymongo": { + Key: "pymongo", + Name: "PyMongo Driver", + SourceExtensions: []string{".py"}, + }, +} + +// OutputExtensions are file extensions that represent output files (not source code). +var OutputExtensions = []string{".txt", ".sh"} + +// IsValidProduct checks if a product key is valid. +func IsValidProduct(productKey string) bool { + _, exists := ProductMap[productKey] + return exists +} + +// GetProductList returns a formatted list of valid products for help text. +func GetProductList() string { + return `Valid products: + - mongosh (MongoDB Shell) + - csharp/driver (C#/.NET Driver) + - go/driver (Go Driver) + - go/atlas-sdk (Atlas Go SDK) + - java/driver-sync (Java Sync Driver) + - javascript/driver (Node.js Driver) + - pymongo (PyMongo Driver)` +} + diff --git a/audit-cli/main.go b/audit-cli/main.go index a6ce75d..2345b84 100644 --- a/audit-cli/main.go +++ b/audit-cli/main.go @@ -12,11 +12,14 @@ // - includes: Analyze include directive relationships // - compare: Compare files across different versions // - file-contents: Compare file contents across versions +// - count: Count code examples +// - tested-examples: Count tested code examples in the monorepo package main import ( "github.com/mongodb/code-example-tooling/audit-cli/commands/analyze" "github.com/mongodb/code-example-tooling/audit-cli/commands/compare" + "github.com/mongodb/code-example-tooling/audit-cli/commands/count" "github.com/mongodb/code-example-tooling/audit-cli/commands/extract" "github.com/mongodb/code-example-tooling/audit-cli/commands/search" "github.com/spf13/cobra" @@ -38,6 +41,7 @@ with special handling for MongoDB documentation conventions.`, rootCmd.AddCommand(search.NewSearchCommand()) rootCmd.AddCommand(analyze.NewAnalyzeCommand()) rootCmd.AddCommand(compare.NewCompareCommand()) + rootCmd.AddCommand(count.NewCountCommand()) err := rootCmd.Execute() if err != nil { diff --git a/audit-cli/testdata/count-test-monorepo/content/404/source/not-found.txt b/audit-cli/testdata/count-test-monorepo/content/404/source/not-found.txt new file mode 100644 index 0000000..9a08fe9 --- /dev/null +++ b/audit-cli/testdata/count-test-monorepo/content/404/source/not-found.txt @@ -0,0 +1 @@ +404 Page diff --git a/audit-cli/testdata/count-test-monorepo/content/app-services/source/index.txt b/audit-cli/testdata/count-test-monorepo/content/app-services/source/index.txt new file mode 100644 index 0000000..e150c25 --- /dev/null +++ b/audit-cli/testdata/count-test-monorepo/content/app-services/source/index.txt @@ -0,0 +1 @@ +App Services Index diff --git a/audit-cli/testdata/count-test-monorepo/content/atlas/source/clusters.txt b/audit-cli/testdata/count-test-monorepo/content/atlas/source/clusters.txt new file mode 100644 index 0000000..37646be --- /dev/null +++ b/audit-cli/testdata/count-test-monorepo/content/atlas/source/clusters.txt @@ -0,0 +1 @@ +Atlas Clusters diff --git a/audit-cli/testdata/count-test-monorepo/content/atlas/source/getting-started.txt b/audit-cli/testdata/count-test-monorepo/content/atlas/source/getting-started.txt new file mode 100644 index 0000000..2835c31 --- /dev/null +++ b/audit-cli/testdata/count-test-monorepo/content/atlas/source/getting-started.txt @@ -0,0 +1 @@ +Atlas Getting Started diff --git a/audit-cli/testdata/count-test-monorepo/content/code-examples/example.txt b/audit-cli/testdata/count-test-monorepo/content/code-examples/example.txt new file mode 100644 index 0000000..361ea96 --- /dev/null +++ b/audit-cli/testdata/count-test-monorepo/content/code-examples/example.txt @@ -0,0 +1 @@ +Code Example diff --git a/audit-cli/testdata/count-test-monorepo/content/code-examples/tested/command-line/mongosh/example1.js b/audit-cli/testdata/count-test-monorepo/content/code-examples/tested/command-line/mongosh/example1.js new file mode 100644 index 0000000..8d5c819 --- /dev/null +++ b/audit-cli/testdata/count-test-monorepo/content/code-examples/tested/command-line/mongosh/example1.js @@ -0,0 +1,5 @@ +// MongoDB Shell example 1 +db.restaurants.aggregate([ + { $match: { category: "cafe" } } +]) + diff --git a/audit-cli/testdata/count-test-monorepo/content/code-examples/tested/command-line/mongosh/example2.js b/audit-cli/testdata/count-test-monorepo/content/code-examples/tested/command-line/mongosh/example2.js new file mode 100644 index 0000000..e39749e --- /dev/null +++ b/audit-cli/testdata/count-test-monorepo/content/code-examples/tested/command-line/mongosh/example2.js @@ -0,0 +1,3 @@ +// MongoDB Shell example 2 +db.users.find({ age: { $gt: 25 } }) + diff --git a/audit-cli/testdata/count-test-monorepo/content/code-examples/tested/command-line/mongosh/output1.txt b/audit-cli/testdata/count-test-monorepo/content/code-examples/tested/command-line/mongosh/output1.txt new file mode 100644 index 0000000..a9ce79b --- /dev/null +++ b/audit-cli/testdata/count-test-monorepo/content/code-examples/tested/command-line/mongosh/output1.txt @@ -0,0 +1,5 @@ +[ + { _id: 1, category: 'café', status: 'Open' }, + { _id: 2, category: 'cafe', status: 'open' } +] + diff --git a/audit-cli/testdata/count-test-monorepo/content/code-examples/tested/csharp/driver/Example1.cs b/audit-cli/testdata/count-test-monorepo/content/code-examples/tested/csharp/driver/Example1.cs new file mode 100644 index 0000000..be1a70d --- /dev/null +++ b/audit-cli/testdata/count-test-monorepo/content/code-examples/tested/csharp/driver/Example1.cs @@ -0,0 +1,13 @@ +// C#/.NET Driver example 1 +using MongoDB.Driver; + +class Example1 +{ + static void Main() + { + var client = new MongoClient("mongodb://localhost:27017"); + var database = client.GetDatabase("test"); + Console.WriteLine("Connected successfully"); + } +} + diff --git a/audit-cli/testdata/count-test-monorepo/content/code-examples/tested/go/atlas-sdk/example1.go b/audit-cli/testdata/count-test-monorepo/content/code-examples/tested/go/atlas-sdk/example1.go new file mode 100644 index 0000000..c1d24c2 --- /dev/null +++ b/audit-cli/testdata/count-test-monorepo/content/code-examples/tested/go/atlas-sdk/example1.go @@ -0,0 +1,13 @@ +// Atlas Go SDK example 1 +package main + +import ( + "context" + "go.mongodb.org/atlas-sdk/v20231115002/admin" +) + +func main() { + sdk, _ := admin.NewClient() + _ = sdk +} + diff --git a/audit-cli/testdata/count-test-monorepo/content/code-examples/tested/go/driver/example1.go b/audit-cli/testdata/count-test-monorepo/content/code-examples/tested/go/driver/example1.go new file mode 100644 index 0000000..506f281 --- /dev/null +++ b/audit-cli/testdata/count-test-monorepo/content/code-examples/tested/go/driver/example1.go @@ -0,0 +1,14 @@ +// Go Driver example 1 +package main + +import ( + "context" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +func main() { + client, _ := mongo.Connect(context.TODO(), options.Client().ApplyURI("mongodb://localhost:27017")) + defer client.Disconnect(context.TODO()) +} + diff --git a/audit-cli/testdata/count-test-monorepo/content/code-examples/tested/java/driver-sync/Example1.java b/audit-cli/testdata/count-test-monorepo/content/code-examples/tested/java/driver-sync/Example1.java new file mode 100644 index 0000000..79769ff --- /dev/null +++ b/audit-cli/testdata/count-test-monorepo/content/code-examples/tested/java/driver-sync/Example1.java @@ -0,0 +1,12 @@ +// Java Sync Driver example 1 +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoClients; + +public class Example1 { + public static void main(String[] args) { + MongoClient client = MongoClients.create("mongodb://localhost:27017"); + System.out.println("Connected successfully"); + client.close(); + } +} + diff --git a/audit-cli/testdata/count-test-monorepo/content/code-examples/tested/javascript/driver/example1.js b/audit-cli/testdata/count-test-monorepo/content/code-examples/tested/javascript/driver/example1.js new file mode 100644 index 0000000..5fa8840 --- /dev/null +++ b/audit-cli/testdata/count-test-monorepo/content/code-examples/tested/javascript/driver/example1.js @@ -0,0 +1,12 @@ +// Node.js Driver example 1 +const { MongoClient } = require('mongodb'); + +async function main() { + const client = new MongoClient('mongodb://localhost:27017'); + await client.connect(); + console.log('Connected successfully'); + await client.close(); +} + +main(); + diff --git a/audit-cli/testdata/count-test-monorepo/content/code-examples/tested/javascript/driver/output1.txt b/audit-cli/testdata/count-test-monorepo/content/code-examples/tested/javascript/driver/output1.txt new file mode 100644 index 0000000..927b2da --- /dev/null +++ b/audit-cli/testdata/count-test-monorepo/content/code-examples/tested/javascript/driver/output1.txt @@ -0,0 +1,2 @@ +Connected successfully + diff --git a/audit-cli/testdata/count-test-monorepo/content/code-examples/tested/python/pymongo/example1.py b/audit-cli/testdata/count-test-monorepo/content/code-examples/tested/python/pymongo/example1.py new file mode 100644 index 0000000..bc338d4 --- /dev/null +++ b/audit-cli/testdata/count-test-monorepo/content/code-examples/tested/python/pymongo/example1.py @@ -0,0 +1,9 @@ +# PyMongo example 1 +from pymongo import MongoClient + +client = MongoClient('mongodb://localhost:27017') +db = client.test_database +collection = db.test_collection +result = collection.insert_one({'name': 'Alice', 'age': 30}) +print(result.inserted_id) + diff --git a/audit-cli/testdata/count-test-monorepo/content/code-examples/tested/python/pymongo/example2.py b/audit-cli/testdata/count-test-monorepo/content/code-examples/tested/python/pymongo/example2.py new file mode 100644 index 0000000..6f21ece --- /dev/null +++ b/audit-cli/testdata/count-test-monorepo/content/code-examples/tested/python/pymongo/example2.py @@ -0,0 +1,7 @@ +# PyMongo example 2 +from pymongo import MongoClient + +client = MongoClient('mongodb://localhost:27017') +result = client.admin.command('ping') +print(result) + diff --git a/audit-cli/testdata/count-test-monorepo/content/code-examples/tested/python/pymongo/output1.txt b/audit-cli/testdata/count-test-monorepo/content/code-examples/tested/python/pymongo/output1.txt new file mode 100644 index 0000000..b8e39bc --- /dev/null +++ b/audit-cli/testdata/count-test-monorepo/content/code-examples/tested/python/pymongo/output1.txt @@ -0,0 +1,2 @@ +ObjectId('507f1f77bcf86cd799439011') + diff --git a/audit-cli/testdata/count-test-monorepo/content/code-examples/tested/python/pymongo/setup.sh b/audit-cli/testdata/count-test-monorepo/content/code-examples/tested/python/pymongo/setup.sh new file mode 100644 index 0000000..b8328bd --- /dev/null +++ b/audit-cli/testdata/count-test-monorepo/content/code-examples/tested/python/pymongo/setup.sh @@ -0,0 +1,3 @@ +#!/bin/bash +pip install pymongo + diff --git a/audit-cli/testdata/count-test-monorepo/content/drivers/manual/source/index.txt b/audit-cli/testdata/count-test-monorepo/content/drivers/manual/source/index.txt new file mode 100644 index 0000000..630cd15 --- /dev/null +++ b/audit-cli/testdata/count-test-monorepo/content/drivers/manual/source/index.txt @@ -0,0 +1 @@ +Drivers Manual Index diff --git a/audit-cli/testdata/count-test-monorepo/content/drivers/manual/source/tutorial.txt b/audit-cli/testdata/count-test-monorepo/content/drivers/manual/source/tutorial.txt new file mode 100644 index 0000000..d2ba9a7 --- /dev/null +++ b/audit-cli/testdata/count-test-monorepo/content/drivers/manual/source/tutorial.txt @@ -0,0 +1 @@ +Drivers Manual Tutorial diff --git a/audit-cli/testdata/count-test-monorepo/content/drivers/upcoming/source/index.txt b/audit-cli/testdata/count-test-monorepo/content/drivers/upcoming/source/index.txt new file mode 100644 index 0000000..0ec95e3 --- /dev/null +++ b/audit-cli/testdata/count-test-monorepo/content/drivers/upcoming/source/index.txt @@ -0,0 +1 @@ +Drivers Upcoming Index diff --git a/audit-cli/testdata/count-test-monorepo/content/drivers/upcoming/source/tutorial.txt b/audit-cli/testdata/count-test-monorepo/content/drivers/upcoming/source/tutorial.txt new file mode 100644 index 0000000..1378fb0 --- /dev/null +++ b/audit-cli/testdata/count-test-monorepo/content/drivers/upcoming/source/tutorial.txt @@ -0,0 +1 @@ +Drivers Upcoming Tutorial diff --git a/audit-cli/testdata/count-test-monorepo/content/drivers/v7.0/source/index.txt b/audit-cli/testdata/count-test-monorepo/content/drivers/v7.0/source/index.txt new file mode 100644 index 0000000..55247cb --- /dev/null +++ b/audit-cli/testdata/count-test-monorepo/content/drivers/v7.0/source/index.txt @@ -0,0 +1 @@ +Drivers v7.0 Index diff --git a/audit-cli/testdata/count-test-monorepo/content/drivers/v8.0/source/index.txt b/audit-cli/testdata/count-test-monorepo/content/drivers/v8.0/source/index.txt new file mode 100644 index 0000000..8d683d2 --- /dev/null +++ b/audit-cli/testdata/count-test-monorepo/content/drivers/v8.0/source/index.txt @@ -0,0 +1 @@ +Drivers v8.0 Index diff --git a/audit-cli/testdata/count-test-monorepo/content/drivers/v8.0/source/tutorial.txt b/audit-cli/testdata/count-test-monorepo/content/drivers/v8.0/source/tutorial.txt new file mode 100644 index 0000000..dbba9cf --- /dev/null +++ b/audit-cli/testdata/count-test-monorepo/content/drivers/v8.0/source/tutorial.txt @@ -0,0 +1 @@ +Drivers v8.0 Tutorial diff --git a/audit-cli/testdata/count-test-monorepo/content/manual/source/code-examples/example.txt b/audit-cli/testdata/count-test-monorepo/content/manual/source/code-examples/example.txt new file mode 100644 index 0000000..d8e006c --- /dev/null +++ b/audit-cli/testdata/count-test-monorepo/content/manual/source/code-examples/example.txt @@ -0,0 +1 @@ +Code example in subdirectory diff --git a/audit-cli/testdata/count-test-monorepo/content/manual/source/config.yaml b/audit-cli/testdata/count-test-monorepo/content/manual/source/config.yaml new file mode 100644 index 0000000..b53e65d --- /dev/null +++ b/audit-cli/testdata/count-test-monorepo/content/manual/source/config.yaml @@ -0,0 +1 @@ +config: value diff --git a/audit-cli/testdata/count-test-monorepo/content/manual/source/deprecated/old.txt b/audit-cli/testdata/count-test-monorepo/content/manual/source/deprecated/old.txt new file mode 100644 index 0000000..3e2fa96 --- /dev/null +++ b/audit-cli/testdata/count-test-monorepo/content/manual/source/deprecated/old.txt @@ -0,0 +1 @@ +Deprecated content diff --git a/audit-cli/testdata/count-test-monorepo/content/manual/source/index.txt b/audit-cli/testdata/count-test-monorepo/content/manual/source/index.txt new file mode 100644 index 0000000..32973a7 --- /dev/null +++ b/audit-cli/testdata/count-test-monorepo/content/manual/source/index.txt @@ -0,0 +1 @@ +Manual Index Page diff --git a/audit-cli/testdata/count-test-monorepo/content/manual/source/reference.txt b/audit-cli/testdata/count-test-monorepo/content/manual/source/reference.txt new file mode 100644 index 0000000..3914791 --- /dev/null +++ b/audit-cli/testdata/count-test-monorepo/content/manual/source/reference.txt @@ -0,0 +1 @@ +Reference Page diff --git a/audit-cli/testdata/count-test-monorepo/content/manual/source/tutorial.txt b/audit-cli/testdata/count-test-monorepo/content/manual/source/tutorial.txt new file mode 100644 index 0000000..89e3682 --- /dev/null +++ b/audit-cli/testdata/count-test-monorepo/content/manual/source/tutorial.txt @@ -0,0 +1 @@ +Tutorial Page diff --git a/audit-cli/testdata/count-test-monorepo/content/meta/source/metadata.txt b/audit-cli/testdata/count-test-monorepo/content/meta/source/metadata.txt new file mode 100644 index 0000000..507c72f --- /dev/null +++ b/audit-cli/testdata/count-test-monorepo/content/meta/source/metadata.txt @@ -0,0 +1 @@ +Metadata diff --git a/audit-cli/testdata/count-test-monorepo/content/shared/source/include.txt b/audit-cli/testdata/count-test-monorepo/content/shared/source/include.txt new file mode 100644 index 0000000..8de97e3 --- /dev/null +++ b/audit-cli/testdata/count-test-monorepo/content/shared/source/include.txt @@ -0,0 +1 @@ +Shared Include diff --git a/audit-cli/testdata/count-test-monorepo/content/table-of-contents/source/toc.txt b/audit-cli/testdata/count-test-monorepo/content/table-of-contents/source/toc.txt new file mode 100644 index 0000000..728da4f --- /dev/null +++ b/audit-cli/testdata/count-test-monorepo/content/table-of-contents/source/toc.txt @@ -0,0 +1 @@ +TOC