mirror of
https://github.com/kjanat/articulate-parser.git
synced 2026-01-16 07:02:09 +01:00
Refactors main function and enhances test suite
Refactors the main function for improved testability by extracting the core logic into a new run function. Updates argument handling and error reporting to use return codes instead of os.Exit. Adds comprehensive test coverage for main functionality, including integration tests and validation against edge cases. Enhances README with updated code coverage and feature improvement lists. Addresses improved maintainability and testability of the application. Bumps version to 0.3.1
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@ -48,9 +48,12 @@ build/
|
||||
# Test coverage files
|
||||
coverage.out
|
||||
coverage.txt
|
||||
coverage.html
|
||||
coverage.*
|
||||
coverage
|
||||
*.cover
|
||||
*.coverprofile
|
||||
main_coverage
|
||||
|
||||
# Other common exclusions
|
||||
*.exe
|
||||
|
||||
34
README.md
34
README.md
@ -2,6 +2,16 @@
|
||||
|
||||
A Go-based parser that converts Articulate Rise e-learning content to various formats including Markdown, HTML, and Word documents.
|
||||
|
||||
[][gomod]
|
||||
[][Package documentation]
|
||||
[][Go report]
|
||||
[][Tags] <!-- [][Latest release] -->
|
||||
[][MIT License] <!-- [][Commits] -->
|
||||
[][Commits]
|
||||
[][Issues]
|
||||
[][Build]
|
||||
[][Codecov]
|
||||
|
||||
## System Architecture
|
||||
|
||||
```mermaid
|
||||
@ -73,16 +83,6 @@ The system follows **Clean Architecture** principles with clear separation of co
|
||||
- **📤 Export Layer**: Factory pattern for format-specific exporters
|
||||
- **📊 Data Layer**: Domain models representing course structure
|
||||
|
||||
[][gomod]
|
||||
[][Package documentation]
|
||||
[][Go report]
|
||||
[][Tags] <!-- [][Latest release] -->
|
||||
[][MIT License] <!-- [][Commits] -->
|
||||
[][Commits]
|
||||
[][Issues]
|
||||
[][Build]
|
||||
[][Codecov]
|
||||
|
||||
## Features
|
||||
|
||||
- Parse Articulate Rise JSON data from URLs or local files
|
||||
@ -291,7 +291,7 @@ The parser includes error handling for:
|
||||
|
||||
<!-- ## Code coverage
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||
|
||||
@ -315,12 +315,12 @@ The parser includes error handling for:
|
||||
|
||||
Potential improvements could include:
|
||||
|
||||
- PDF export support
|
||||
- Media file downloading
|
||||
- ~~HTML export with preserved styling~~ ✅ **Completed**
|
||||
- SCORM package support
|
||||
- Batch processing capabilities
|
||||
- Custom template support
|
||||
- [ ] PDF export support
|
||||
- [ ] Media file downloading
|
||||
- [x] ~~HTML export with preserved styling~~
|
||||
- [ ] SCORM package support
|
||||
- [ ] Batch processing capabilities
|
||||
- [ ] Custom template support
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@ -5,7 +5,7 @@ package version
|
||||
// Version information.
|
||||
var (
|
||||
// Version is the current version of the application.
|
||||
Version = "0.3.0"
|
||||
Version = "0.3.1"
|
||||
|
||||
// BuildTime is the time the binary was built.
|
||||
BuildTime = "unknown"
|
||||
|
||||
26
main.go
26
main.go
@ -16,6 +16,12 @@ import (
|
||||
// It handles command-line arguments, sets up dependencies,
|
||||
// and coordinates the parsing and exporting of courses.
|
||||
func main() {
|
||||
os.Exit(run(os.Args))
|
||||
}
|
||||
|
||||
// run contains the main application logic and returns an exit code.
|
||||
// This function is testable as it doesn't call os.Exit directly.
|
||||
func run(args []string) int {
|
||||
// Dependency injection setup
|
||||
htmlCleaner := services.NewHTMLCleaner()
|
||||
parser := services.NewArticulateParser()
|
||||
@ -23,20 +29,20 @@ func main() {
|
||||
app := services.NewApp(parser, exporterFactory)
|
||||
|
||||
// Check for required command-line arguments
|
||||
if len(os.Args) < 4 {
|
||||
fmt.Printf("Usage: %s <source> <format> <output>\n", os.Args[0])
|
||||
if len(args) < 4 {
|
||||
fmt.Printf("Usage: %s <source> <format> <output>\n", args[0])
|
||||
fmt.Printf(" source: URI or file path to the course\n")
|
||||
fmt.Printf(" format: export format (%s)\n", joinStrings(app.GetSupportedFormats(), ", "))
|
||||
fmt.Printf(" output: output file path\n")
|
||||
fmt.Println("\nExample:")
|
||||
fmt.Printf(" %s articulate-sample.json markdown output.md\n", os.Args[0])
|
||||
fmt.Printf(" %s https://rise.articulate.com/share/xyz docx output.docx\n", os.Args[0])
|
||||
os.Exit(1)
|
||||
fmt.Printf(" %s articulate-sample.json markdown output.md\n", args[0])
|
||||
fmt.Printf(" %s https://rise.articulate.com/share/xyz docx output.docx\n", args[0])
|
||||
return 1
|
||||
}
|
||||
|
||||
source := os.Args[1]
|
||||
format := os.Args[2]
|
||||
output := os.Args[3]
|
||||
source := args[1]
|
||||
format := args[2]
|
||||
output := args[3]
|
||||
|
||||
var err error
|
||||
|
||||
@ -48,10 +54,12 @@ func main() {
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("Error processing course: %v", err)
|
||||
log.Printf("Error processing course: %v", err)
|
||||
return 1
|
||||
}
|
||||
|
||||
fmt.Printf("Successfully exported course to %s\n", output)
|
||||
return 0
|
||||
}
|
||||
|
||||
// isURI checks if a string is a URI by looking for http:// or https:// prefixes.
|
||||
|
||||
303
main_test.go
303
main_test.go
@ -2,6 +2,12 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@ -173,3 +179,300 @@ func BenchmarkJoinStrings(b *testing.B) {
|
||||
joinStrings(strs, separator)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRunWithInsufficientArgs tests the run function with insufficient command-line arguments.
|
||||
func TestRunWithInsufficientArgs(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
args []string
|
||||
}{
|
||||
{
|
||||
name: "no arguments",
|
||||
args: []string{"articulate-parser"},
|
||||
},
|
||||
{
|
||||
name: "one argument",
|
||||
args: []string{"articulate-parser", "source"},
|
||||
},
|
||||
{
|
||||
name: "two arguments",
|
||||
args: []string{"articulate-parser", "source", "format"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Capture stdout
|
||||
oldStdout := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
// Run the function
|
||||
exitCode := run(tt.args)
|
||||
|
||||
// Restore stdout
|
||||
w.Close()
|
||||
os.Stdout = oldStdout
|
||||
|
||||
// Read captured output
|
||||
var buf bytes.Buffer
|
||||
io.Copy(&buf, r)
|
||||
output := buf.String()
|
||||
|
||||
// Verify exit code
|
||||
if exitCode != 1 {
|
||||
t.Errorf("Expected exit code 1, got %d", exitCode)
|
||||
}
|
||||
|
||||
// Verify usage message is displayed
|
||||
if !strings.Contains(output, "Usage:") {
|
||||
t.Errorf("Expected usage message in output, got: %s", output)
|
||||
}
|
||||
|
||||
if !strings.Contains(output, "export format") {
|
||||
t.Errorf("Expected format information in output, got: %s", output)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestRunWithInvalidFile tests the run function with a non-existent file.
|
||||
func TestRunWithInvalidFile(t *testing.T) {
|
||||
// Capture stdout and stderr
|
||||
oldStdout := os.Stdout
|
||||
oldStderr := os.Stderr
|
||||
|
||||
stdoutR, stdoutW, _ := os.Pipe()
|
||||
stderrR, stderrW, _ := os.Pipe()
|
||||
|
||||
os.Stdout = stdoutW
|
||||
os.Stderr = stderrW
|
||||
|
||||
// Also need to redirect log output
|
||||
oldLogOutput := log.Writer()
|
||||
log.SetOutput(stderrW)
|
||||
|
||||
// Run with non-existent file
|
||||
args := []string{"articulate-parser", "nonexistent-file.json", "markdown", "output.md"}
|
||||
exitCode := run(args)
|
||||
|
||||
// Restore stdout/stderr and log output
|
||||
stdoutW.Close()
|
||||
stderrW.Close()
|
||||
os.Stdout = oldStdout
|
||||
os.Stderr = oldStderr
|
||||
log.SetOutput(oldLogOutput)
|
||||
|
||||
// Read captured output
|
||||
var stdoutBuf, stderrBuf bytes.Buffer
|
||||
io.Copy(&stdoutBuf, stdoutR)
|
||||
io.Copy(&stderrBuf, stderrR)
|
||||
|
||||
stdoutR.Close()
|
||||
stderrR.Close()
|
||||
|
||||
// Verify exit code
|
||||
if exitCode != 1 {
|
||||
t.Errorf("Expected exit code 1 for non-existent file, got %d", exitCode)
|
||||
}
|
||||
|
||||
// Should have error output
|
||||
errorOutput := stderrBuf.String()
|
||||
if !strings.Contains(errorOutput, "Error processing course") {
|
||||
t.Errorf("Expected error message about processing course, got: %s", errorOutput)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRunWithInvalidURI tests the run function with an invalid URI.
|
||||
func TestRunWithInvalidURI(t *testing.T) {
|
||||
// Capture stdout and stderr
|
||||
oldStdout := os.Stdout
|
||||
oldStderr := os.Stderr
|
||||
|
||||
stdoutR, stdoutW, _ := os.Pipe()
|
||||
stderrR, stderrW, _ := os.Pipe()
|
||||
|
||||
os.Stdout = stdoutW
|
||||
os.Stderr = stderrW
|
||||
|
||||
// Also need to redirect log output
|
||||
oldLogOutput := log.Writer()
|
||||
log.SetOutput(stderrW)
|
||||
|
||||
// Run with invalid URI (will fail because we can't actually fetch)
|
||||
args := []string{"articulate-parser", "https://example.com/invalid", "markdown", "output.md"}
|
||||
exitCode := run(args)
|
||||
|
||||
// Restore stdout/stderr and log output
|
||||
stdoutW.Close()
|
||||
stderrW.Close()
|
||||
os.Stdout = oldStdout
|
||||
os.Stderr = oldStderr
|
||||
log.SetOutput(oldLogOutput)
|
||||
|
||||
// Read captured output
|
||||
var stdoutBuf, stderrBuf bytes.Buffer
|
||||
io.Copy(&stdoutBuf, stdoutR)
|
||||
io.Copy(&stderrBuf, stderrR)
|
||||
|
||||
stdoutR.Close()
|
||||
stderrR.Close()
|
||||
|
||||
// Should fail because the URI is invalid/unreachable
|
||||
if exitCode != 1 {
|
||||
t.Errorf("Expected failure (exit code 1) for invalid URI, got %d", exitCode)
|
||||
}
|
||||
|
||||
// Should have error output
|
||||
errorOutput := stderrBuf.String()
|
||||
if !strings.Contains(errorOutput, "Error processing course") {
|
||||
t.Errorf("Expected error message about processing course, got: %s", errorOutput)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRunWithValidJSONFile tests the run function with a valid JSON file.
|
||||
func TestRunWithValidJSONFile(t *testing.T) {
|
||||
// Create a temporary test JSON file
|
||||
testContent := `{
|
||||
"title": "Test Course",
|
||||
"lessons": [
|
||||
{
|
||||
"id": "lesson1",
|
||||
"title": "Test Lesson",
|
||||
"blocks": [
|
||||
{
|
||||
"type": "text",
|
||||
"id": "block1",
|
||||
"data": {
|
||||
"text": "Test content"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}`
|
||||
|
||||
tmpFile, err := os.CreateTemp("", "test-course-*.json")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp file: %v", err)
|
||||
}
|
||||
defer os.Remove(tmpFile.Name())
|
||||
|
||||
if _, err := tmpFile.WriteString(testContent); err != nil {
|
||||
t.Fatalf("Failed to write test content: %v", err)
|
||||
}
|
||||
tmpFile.Close()
|
||||
|
||||
// Test successful run with valid file
|
||||
outputFile := "test-output.md"
|
||||
defer os.Remove(outputFile)
|
||||
|
||||
// Save original stdout
|
||||
originalStdout := os.Stdout
|
||||
defer func() { os.Stdout = originalStdout }()
|
||||
|
||||
// Capture stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
args := []string{"articulate-parser", tmpFile.Name(), "markdown", outputFile}
|
||||
exitCode := run(args)
|
||||
|
||||
// Close write end and restore stdout
|
||||
w.Close()
|
||||
os.Stdout = originalStdout
|
||||
|
||||
// Read captured output
|
||||
var buf bytes.Buffer
|
||||
io.Copy(&buf, r)
|
||||
output := buf.String()
|
||||
|
||||
// Verify successful execution
|
||||
if exitCode != 0 {
|
||||
t.Errorf("Expected successful execution (exit code 0), got %d", exitCode)
|
||||
}
|
||||
|
||||
// Verify success message
|
||||
expectedMsg := fmt.Sprintf("Successfully exported course to %s", outputFile)
|
||||
if !strings.Contains(output, expectedMsg) {
|
||||
t.Errorf("Expected success message '%s' in output, got: %s", expectedMsg, output)
|
||||
}
|
||||
|
||||
// Verify output file was created
|
||||
if _, err := os.Stat(outputFile); os.IsNotExist(err) {
|
||||
t.Errorf("Expected output file %s to be created", outputFile)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRunIntegration tests the run function with different output formats using sample file.
|
||||
func TestRunIntegration(t *testing.T) {
|
||||
// Skip if sample file doesn't exist
|
||||
if _, err := os.Stat("articulate-sample.json"); os.IsNotExist(err) {
|
||||
t.Skip("Skipping integration test: articulate-sample.json not found")
|
||||
}
|
||||
|
||||
formats := []struct {
|
||||
format string
|
||||
output string
|
||||
}{
|
||||
{"markdown", "test-output.md"},
|
||||
{"html", "test-output.html"},
|
||||
{"docx", "test-output.docx"},
|
||||
}
|
||||
|
||||
for _, format := range formats {
|
||||
t.Run("format_"+format.format, func(t *testing.T) {
|
||||
// Capture stdout
|
||||
oldStdout := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
// Run the function
|
||||
args := []string{"articulate-parser", "articulate-sample.json", format.format, format.output}
|
||||
exitCode := run(args)
|
||||
|
||||
// Restore stdout
|
||||
w.Close()
|
||||
os.Stdout = oldStdout
|
||||
|
||||
// Read captured output
|
||||
var buf bytes.Buffer
|
||||
io.Copy(&buf, r)
|
||||
output := buf.String()
|
||||
|
||||
// Clean up test file
|
||||
defer os.Remove(format.output)
|
||||
|
||||
// Verify successful execution
|
||||
if exitCode != 0 {
|
||||
t.Errorf("Expected successful execution (exit code 0), got %d", exitCode)
|
||||
}
|
||||
|
||||
// Verify success message
|
||||
expectedMsg := "Successfully exported course to " + format.output
|
||||
if !strings.Contains(output, expectedMsg) {
|
||||
t.Errorf("Expected success message '%s' in output, got: %s", expectedMsg, output)
|
||||
}
|
||||
|
||||
// Verify output file was created
|
||||
if _, err := os.Stat(format.output); os.IsNotExist(err) {
|
||||
t.Errorf("Expected output file %s to be created", format.output)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestMainFunction tests that the main function exists and is properly structured.
|
||||
// We can't test os.Exit behavior directly, but we can verify the main function
|
||||
// calls the run function correctly by testing run function behavior.
|
||||
func TestMainFunction(t *testing.T) {
|
||||
// Test that insufficient args return exit code 1
|
||||
exitCode := run([]string{"program"})
|
||||
if exitCode != 1 {
|
||||
t.Errorf("Expected run to return exit code 1 for insufficient args, got %d", exitCode)
|
||||
}
|
||||
|
||||
// Test that main function exists (this is mainly for coverage)
|
||||
// The main function just calls os.Exit(run(os.Args)), which we can't test directly
|
||||
// but we've tested the run function thoroughly above.
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user