|
5 | 5 | "io" |
6 | 6 | "math" |
7 | 7 | "os" |
| 8 | + "path/filepath" |
8 | 9 | "reflect" |
9 | 10 | "sort" |
10 | 11 | "strings" |
@@ -846,3 +847,224 @@ func TestSortStdin(t *testing.T) { |
846 | 847 | }) |
847 | 848 | } |
848 | 849 | } |
| 850 | + |
| 851 | +// TestMainCommand tests the main command functionality |
| 852 | +func TestMainCommand(t *testing.T) { |
| 853 | + // Create temporary directory for test files |
| 854 | + tmpDir, err := os.MkdirTemp("", "phredsort_test_*") |
| 855 | + if err != nil { |
| 856 | + t.Fatal(err) |
| 857 | + } |
| 858 | + defer os.RemoveAll(tmpDir) |
| 859 | + |
| 860 | + // Helper function to create test FASTQ file |
| 861 | + createTestFastq := func(name string) string { |
| 862 | + path := filepath.Join(tmpDir, name) |
| 863 | + f, err := os.Create(path) |
| 864 | + if err != nil { |
| 865 | + t.Fatal(err) |
| 866 | + } |
| 867 | + defer f.Close() |
| 868 | + |
| 869 | + // Write some test FASTQ data |
| 870 | + fmt.Fprintf(f, "@seq1\nACGT\n+\nIIII\n") |
| 871 | + fmt.Fprintf(f, "@seq2\nACGT\n+\n$$$$\n") |
| 872 | + return path |
| 873 | + } |
| 874 | + |
| 875 | + // Helper function to capture stdout/stderr |
| 876 | + captureOutput := func(f func()) (string, string) { |
| 877 | + oldStdout := os.Stdout |
| 878 | + oldStderr := os.Stderr |
| 879 | + rOut, wOut, _ := os.Pipe() |
| 880 | + rErr, wErr, _ := os.Pipe() |
| 881 | + os.Stdout = wOut |
| 882 | + os.Stderr = wErr |
| 883 | + |
| 884 | + f() |
| 885 | + |
| 886 | + wOut.Close() |
| 887 | + wErr.Close() |
| 888 | + os.Stdout = oldStdout |
| 889 | + os.Stderr = oldStderr |
| 890 | + |
| 891 | + stdout, _ := io.ReadAll(rOut) |
| 892 | + stderr, _ := io.ReadAll(rErr) |
| 893 | + return string(stdout), string(stderr) |
| 894 | + } |
| 895 | + |
| 896 | + tests := []struct { |
| 897 | + name string |
| 898 | + args []string |
| 899 | + expectedCode int |
| 900 | + checkStdout bool |
| 901 | + checkStderr bool |
| 902 | + wantStdout string |
| 903 | + wantStderr string |
| 904 | + setupFiles bool |
| 905 | + validateFiles bool |
| 906 | + }{ |
| 907 | + { |
| 908 | + name: "Version flag", |
| 909 | + args: []string{"--version"}, |
| 910 | + expectedCode: 0, |
| 911 | + checkStdout: true, |
| 912 | + wantStdout: fmt.Sprintf("phredsort %s\n", VERSION), |
| 913 | + }, |
| 914 | + { |
| 915 | + name: "Missing required flags", |
| 916 | + args: []string{}, |
| 917 | + expectedCode: 1, |
| 918 | + checkStderr: true, |
| 919 | + wantStderr: red("Error: input and output files are required") + "\n" + |
| 920 | + red("Try 'phredsort --help' for more information") + "\n", |
| 921 | + }, |
| 922 | + { |
| 923 | + name: "Invalid metric", |
| 924 | + args: []string{"--in", "input.fq", "--out", "output.fq", "--metric", "invalid"}, |
| 925 | + expectedCode: 1, |
| 926 | + checkStderr: true, |
| 927 | + wantStderr: red("Error: invalid metric 'invalid'. Must be one of: avgphred, maxee, meep, lqcount, lqpercent"), |
| 928 | + }, |
| 929 | + { |
| 930 | + name: "Invalid compression level", |
| 931 | + args: []string{"--in", "input.fq", "--out", "output.fq", "--compress", "23"}, |
| 932 | + expectedCode: 1, |
| 933 | + checkStderr: true, |
| 934 | + wantStderr: red("Error: compression level must be between 0 and 22") + "\n", |
| 935 | + }, |
| 936 | + { |
| 937 | + name: "Invalid header metrics", |
| 938 | + args: []string{"--in", "input.fq", "--out", "output.fq", "--header", "invalid,metrics"}, |
| 939 | + expectedCode: 1, |
| 940 | + checkStderr: true, |
| 941 | + wantStderr: red("Error: invalid header metric: invalid") + "\n", |
| 942 | + }, |
| 943 | + { |
| 944 | + name: "Basic file processing", |
| 945 | + args: []string{"--in", "input.fq", "--out", "output.fq"}, |
| 946 | + expectedCode: 0, |
| 947 | + setupFiles: true, |
| 948 | + validateFiles: true, |
| 949 | + }, |
| 950 | + { |
| 951 | + name: "Complex command with multiple options", |
| 952 | + args: []string{ |
| 953 | + "--in", "input.fq", |
| 954 | + "--out", "output.fq", |
| 955 | + "--metric", "avgphred", |
| 956 | + "--minphred", "20", |
| 957 | + "--minqual", "15", |
| 958 | + "--maxqual", "40", |
| 959 | + "--header", "avgphred,maxee,length", |
| 960 | + "--ascending", |
| 961 | + "--compress", "1", |
| 962 | + }, |
| 963 | + expectedCode: 0, |
| 964 | + setupFiles: true, |
| 965 | + validateFiles: true, |
| 966 | + }, |
| 967 | + } |
| 968 | + |
| 969 | + for _, tt := range tests { |
| 970 | + t.Run(tt.name, func(t *testing.T) { |
| 971 | + if tt.setupFiles { |
| 972 | + inFile := createTestFastq("input.fq") |
| 973 | + outFile := filepath.Join(tmpDir, "output.fq") |
| 974 | + |
| 975 | + // Update args with actual file paths |
| 976 | + for i, arg := range tt.args { |
| 977 | + switch arg { |
| 978 | + case "input.fq": |
| 979 | + tt.args[i] = inFile |
| 980 | + case "output.fq": |
| 981 | + tt.args[i] = outFile |
| 982 | + } |
| 983 | + } |
| 984 | + } |
| 985 | + |
| 986 | + // Reset os.Args and set test arguments |
| 987 | + oldArgs := os.Args |
| 988 | + os.Args = append([]string{"phredsort"}, tt.args...) |
| 989 | + |
| 990 | + // Capture exit code |
| 991 | + var exitCode int |
| 992 | + oldExit := exitFunc |
| 993 | + exitFunc = func(code int) { |
| 994 | + exitCode = code |
| 995 | + panic(fmt.Sprintf("exit %d", code)) |
| 996 | + } |
| 997 | + defer func() { |
| 998 | + exitFunc = oldExit |
| 999 | + os.Args = oldArgs |
| 1000 | + |
| 1001 | + if r := recover(); r != nil { |
| 1002 | + if exitStr, ok := r.(string); !ok || !strings.HasPrefix(exitStr, "exit ") { |
| 1003 | + t.Errorf("Unexpected panic: %v", r) |
| 1004 | + } |
| 1005 | + } |
| 1006 | + }() |
| 1007 | + |
| 1008 | + // Run main and capture output |
| 1009 | + stdout, stderr := captureOutput(func() { |
| 1010 | + defer func() { |
| 1011 | + if r := recover(); r != nil { |
| 1012 | + if exitStr, ok := r.(string); !ok || !strings.HasPrefix(exitStr, "exit ") { |
| 1013 | + panic(r) |
| 1014 | + } |
| 1015 | + } |
| 1016 | + }() |
| 1017 | + main() |
| 1018 | + }) |
| 1019 | + |
| 1020 | + // Check exit code |
| 1021 | + if exitCode != tt.expectedCode { |
| 1022 | + t.Errorf("Expected exit code %d, got %d", tt.expectedCode, exitCode) |
| 1023 | + } |
| 1024 | + |
| 1025 | + // Check stdout if required |
| 1026 | + if tt.checkStdout && stdout != tt.wantStdout { |
| 1027 | + t.Errorf("Expected stdout:\n%s\nGot:\n%s", tt.wantStdout, stdout) |
| 1028 | + } |
| 1029 | + |
| 1030 | + // Check stderr if required (including non-printable characters) |
| 1031 | + if tt.checkStderr && stderr != tt.wantStderr { |
| 1032 | + t.Errorf("Expected stderr:\n%q\nGot:\n%q", tt.wantStderr, stderr) |
| 1033 | + } |
| 1034 | + |
| 1035 | + // Validate output file if required |
| 1036 | + if tt.validateFiles { |
| 1037 | + outPath := filepath.Join(tmpDir, "output.fq") |
| 1038 | + if _, err := os.Stat(outPath); os.IsNotExist(err) { |
| 1039 | + t.Error("Expected output file was not created") |
| 1040 | + } |
| 1041 | + |
| 1042 | + // Optional: Add more specific file content validation here |
| 1043 | + // For example, check if the file is valid FASTQ format |
| 1044 | + reader, err := fastx.NewReader(seq.DNAredundant, outPath, fastx.DefaultIDRegexp) |
| 1045 | + if err != nil { |
| 1046 | + t.Errorf("Failed to read output file: %v", err) |
| 1047 | + } |
| 1048 | + defer reader.Close() |
| 1049 | + |
| 1050 | + // Try to read at least one record |
| 1051 | + _, err = reader.Read() |
| 1052 | + if err != nil && err != io.EOF { |
| 1053 | + t.Errorf("Failed to read record from output file: %v", err) |
| 1054 | + } |
| 1055 | + } |
| 1056 | + }) |
| 1057 | + } |
| 1058 | +} |
| 1059 | + |
| 1060 | +func TestMain(m *testing.M) { |
| 1061 | + // Store original exit function |
| 1062 | + originalExit := exitFunc |
| 1063 | + // Replace exit function with mock during tests |
| 1064 | + exitFunc = func(code int) { |
| 1065 | + panic(fmt.Sprintf("exit %d", code)) |
| 1066 | + } |
| 1067 | + defer func() { exitFunc = originalExit }() |
| 1068 | + |
| 1069 | + m.Run() |
| 1070 | +} |
0 commit comments