Skip to content
This repository has been archived by the owner on Jan 20, 2025. It is now read-only.

Commit

Permalink
Fix [Golang] Command (#23)
Browse files Browse the repository at this point in the history
* Fix & Feat [Golang] [Package] Exporter & Filesystem

- [+] feat(session.go): add constants for different format options and output format
- [+] fix(session.go): fix switch cases to use constants instead of hard-coded values
- [+] fix(session.go): fix session data generation for different format options
- [+] feat(file_system_mock.go): add FileExists method to check if a file exists in the mock file system

* Fix [Golang] [Module] Main Command

- [+] fix(main.go): replace hard-coded prompt messages with constant variables
- [+] test(main_test.go): update test case for processCSVOption to match changes in prompt messages and expected output

* Feat [Golang] [Package] File System

- [+] feat(file_system.go): add FileExists method to the FileSystem interface
- [+] feat(file_system.go): implement FileExists method in the RealFileSystem struct
- [+] feat(file_system_mock.go): add FileExistsCalled and FileExistsErr fields to the MockFileSystem struct
  • Loading branch information
H0llyW00dzZ authored Dec 9, 2023
1 parent f4d3de9 commit 418f0af
Show file tree
Hide file tree
Showing 5 changed files with 106 additions and 57 deletions.
28 changes: 21 additions & 7 deletions exporter/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,20 @@ import (
"strings"
)

const (
// FormatOptionInline specifies the format where messages are displayed inline.
FormatOptionInline = iota + 1

// FormatOptionPerLine specifies the format where each message is on a separate line.
FormatOptionPerLine

// FormatOptionJSON specifies the format where messages are encoded as JSON.
FormatOptionJSON

// OutputFormatSeparateCSVFiles specifies the option to create separate CSV files for sessions and messages.
OutputFormatSeparateCSVFiles
)

// StringOrInt is a custom type to handle JSON values that can be either strings or integers (Magic Golang 🎩 🪄).
//
// It implements the Unmarshaler interface to handle this mixed type when unmarshaling JSON data.
Expand Down Expand Up @@ -216,11 +230,11 @@ func ConvertSessionsToCSV(ctx context.Context, sessions []Session, formatOption
// Define headers based on the formatOption
var headers []string
switch formatOption {
case 1: // Inline Formatting
case FormatOptionInline: // Inline Formatting
headers = []string{"id", "topic", "memoryPrompt", "messages"}
case 2: // One Message Per Line
case FormatOptionPerLine: // One Message Per Line
headers = []string{"session_id", "message_id", "date", "role", "content", "memoryPrompt"}
case 4: // JSON String in CSV
case FormatOptionJSON: // JSON String in CSV
headers = []string{"id", "topic", "memoryPrompt", "messages"}
default:
return fmt.Errorf("invalid format option")
Expand All @@ -244,29 +258,29 @@ func ConvertSessionsToCSV(ctx context.Context, sessions []Session, formatOption

var sessionData []string
switch formatOption {
case 1: // Inline Formatting
case FormatOptionInline: // Inline Formatting
var messageContents []string
for _, message := range session.Messages {
messageContents = append(messageContents, fmt.Sprintf("[%s, %s] \"%s\"", message.Role, message.Date, message.Content))
}
sessionData = []string{session.ID, session.Topic, session.MemoryPrompt, strings.Join(messageContents, "; ")}
case 2: // One Message Per Line
case FormatOptionPerLine: // One Message Per Line
for _, message := range session.Messages {
sessionData = []string{session.ID, message.ID, message.Date, message.Role, message.Content, session.MemoryPrompt}
if err := csvWriter.Write(sessionData); err != nil {
return fmt.Errorf("failed to write session data to CSV: %w", err)
}
}
continue // Skip the default write for this format option
case 4: // JSON String in CSV
case FormatOptionJSON: // JSON String in CSV
messagesJSON, err := json.Marshal(session.Messages)
if err != nil {
return fmt.Errorf("failed to marshal messages to JSON: %w", err)
}
sessionData = []string{session.ID, session.Topic, session.MemoryPrompt, string(messagesJSON)}
}
// Write the session data to the CSV
if formatOption != 2 {
if formatOption != FormatOptionPerLine {
if err := csvWriter.Write(sessionData); err != nil {
return fmt.Errorf("failed to write session data to CSV: %w", err)
}
Expand Down
7 changes: 7 additions & 0 deletions filesystem/file_system.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ type FileSystem interface {
WriteFile(name string, data []byte, perm fs.FileMode) error
ReadFile(name string) ([]byte, error) // Added ReadFile method
Stat(name string) (os.FileInfo, error)
FileExists(name string) bool // Added FileExists method to the interface
}

// RealFileSystem implements the FileSystem interface by wrapping the os package functions,
Expand Down Expand Up @@ -48,3 +49,9 @@ func (rfs RealFileSystem) ReadFile(name string) ([]byte, error) {
func (rfs RealFileSystem) Stat(name string) (os.FileInfo, error) {
return os.Stat(name)
}

// FileExists checks if a file exists in the file system at the given path.
func (rfs RealFileSystem) FileExists(name string) bool {
_, err := os.Stat(name)
return !os.IsNotExist(err)
}
26 changes: 17 additions & 9 deletions filesystem/file_system_mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,17 @@ var _ FileSystem = (*MockFileSystem)(nil)
// It uses a map to store file names and associated data, allowing for the simulation of file creation,
// reading, and writing without actual file system interaction.
type MockFileSystem struct {
FilesCreated map[string]*bytes.Buffer // FilesCreated maps file names to buffers holding file contents.
WriteFileCalled bool // Add this field to track if WriteFile has been called.
WriteFilePath string // Track the path provided to WriteFile.
WriteFileData []byte // Optionally track the data provided to WriteFile.
WriteFilePerm fs.FileMode // Optionally track the file permissions provided to WriteFile.
Files map[string][]byte // Files maps file names to file contents.
ReadFileCalled bool // this field to track if ReadFile has been caled.
ReadFileData []byte // Optionally track the data provided to ReadFile.
ReadFileErr error // Optionally track the error provider to ReadFile.
FilesCreated map[string]*bytes.Buffer // FilesCreated maps file names to buffers holding file contents.
WriteFileCalled bool // Add this field to track if WriteFile has been called.
WriteFilePath string // Track the path provided to WriteFile.
WriteFileData []byte // Optionally track the data provided to WriteFile.
WriteFilePerm fs.FileMode // Optionally track the file permissions provided to WriteFile.
Files map[string][]byte // Files maps file names to file contents.
ReadFileCalled bool // this field to track if ReadFile has been caled.
ReadFileData []byte // Optionally track the data provided to ReadFile.
ReadFileErr error // Optionally track the error provider to ReadFile.
FileExistsCalled bool // Optionally track the result of FileExists.
FileExistsErr error // Optionally track the error provider to FileExists.
}

// MockExporter is a mock implementation of the exporter.Exporter interface for testing purposes.
Expand Down Expand Up @@ -109,3 +111,9 @@ func (m mockFileInfo) Mode() fs.FileMode { return 0 } // Dummy value
func (m mockFileInfo) ModTime() time.Time { return time.Time{} } // Dummy value for modification time.
func (m mockFileInfo) IsDir() bool { return false } // Dummy value, always false.
func (m mockFileInfo) Sys() interface{} { return nil } // No system-specific information.

// FileExists checks if the given file name exists in the mock file system.
func (m *MockFileSystem) FileExists(name string) bool {
_, exists := m.FilesCreated[name]
return exists
}
67 changes: 44 additions & 23 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,30 @@ import (
"github.com/H0llyW00dzZ/ChatGPT-Next-Web-Session-Exporter/repairdata"
)

const (
// Output format options
OutputFormatCSV = 1
OutputFormatDataset = 2
OutputFormatInline = 1
OutputFormatPerLine = 2
OutputFormatSeparateCSV = 3
OutputFormatJSONInCSV = 4

// File type
FileTypeDataset = "dataset"

// Prompt messages
PromptEnterJSONFilePath = "Enter the path to the JSON file: "
PromptRepairData = "Do you want to repair data? (yes/no): "
PromptSelectOutputFormat = "Select the output format:\n1) CSV\n2) Hugging Face Dataset\n"
PromptSelectCSVOutputFormat = "Select the message output format:\n1) Inline Formatting\n2) One Message Per Line\n3) Separate Files for Sessions and Messages\n4) JSON String in CSV\n"
PromptEnterCSVFileName = "Enter the name of the CSV file to save: "
PromptEnterSessionsCSVFileName = "Enter the name of the sessions CSV file to save: "
PromptEnterMessagesCSVFileName = "Enter the name of the messages CSV file to save: "
PromptSaveOutputToFile = "Do you want to save the output to a file? (yes/no)\n"
PromptEnterFileName = "Enter the name of the %s file to save: "
)

// main initializes the application, setting up context for cancellation and
// starting the user interaction flow for data processing and exporting.
func main() {
Expand All @@ -35,14 +59,14 @@ func main() {
reader := bufio.NewReader(os.Stdin)

// Collect the JSON file path from the user.
jsonFilePath, err := promptForInput(ctx, reader, "Enter the path to the JSON file: ")
jsonFilePath, err := promptForInput(ctx, reader, PromptEnterJSONFilePath)
if err != nil {
handleInputError(err)
return
}

// Offer the user an option to repair the data before processing.
repairData, err := promptForInput(ctx, reader, "Do you want to repair data? (yes/no): ")
repairData, err := promptForInput(ctx, reader, PromptRepairData)
if err != nil {
handleInputError(err)
return
Expand All @@ -69,7 +93,7 @@ func main() {
}

// Query the user for the preferred output format and process accordingly.
outputOption, err := promptForInput(ctx, reader, "Select the output format:\n1) CSV\n2) Hugging Face Dataset\n")
outputOption, err := promptForInput(ctx, reader, PromptSelectOutputFormat)
if err != nil {
handleInputError(err)
return
Expand Down Expand Up @@ -115,35 +139,32 @@ func setupSignalHandling(cancel context.CancelFunc) {
// It supports context cancellation, which can interrupt the blocking read operation.
func promptForInput(ctx context.Context, reader *bufio.Reader, prompt string) (string, error) {
fmt.Print(prompt)
inputChan := make(chan string)
errorChan := make(chan error)
type result struct {
input string
err error
}
resultChan := make(chan result)

go func() {
input, err := reader.ReadString('\n')
if err != nil {
errorChan <- err
} else {
inputChan <- input
}
resultChan <- result{input: input, err: err}
}()

select {
case <-ctx.Done():
return "", ctx.Err()
case err := <-errorChan:
return "", err
case input := <-inputChan:
return strings.TrimSpace(input), nil
case res := <-resultChan:
return strings.TrimSpace(res.input), res.err
}
}

// processOutputOption directs the processing flow based on the user's choice of output format.
// It now respects the context for cancellation, ensuring long-running operations can be interrupted.
func processOutputOption(fs filesystem.FileSystem, ctx context.Context, reader *bufio.Reader, outputOption string, sessions []exporter.Session) {
switch outputOption {
case "1":
case `1`:
processCSVOption(fs, ctx, reader, sessions)
case "2":
case `2`:
processDatasetOption(fs, ctx, reader, sessions)
default:
fmt.Println("Invalid output option.")
Expand All @@ -157,7 +178,7 @@ func processOutputOption(fs filesystem.FileSystem, ctx context.Context, reader *
// It prints the output file names or error messages accordingly.
func processCSVOption(fs filesystem.FileSystem, ctx context.Context, reader *bufio.Reader, sessions []exporter.Session) {
// Prompt the user for the CSV format option
formatOptionStr, err := promptForInput(ctx, reader, "Select the message output format:\n1) Inline Formatting\n2) One Message Per Line\n3) Separate Files for Sessions and Messages\n4) JSON String in CSV\n")
formatOptionStr, err := promptForInput(ctx, reader, PromptSelectCSVOutputFormat)
if err != nil {
if err == context.Canceled || err == io.EOF {
// If the error is context.Canceled or io.EOF, exit gracefully.
Expand Down Expand Up @@ -202,7 +223,7 @@ func processDatasetOption(fs filesystem.FileSystem, ctx context.Context, reader
// saveToFile prompts the user to save the provided content to a file of the specified type.
// This function now also accepts a context, allowing file operations to be cancelable.
func saveToFile(fs filesystem.FileSystem, ctx context.Context, reader *bufio.Reader, content string, fileType string) {
saveOutput, err := promptForInput(ctx, reader, fmt.Sprintf("Do you want to save the output to a file? (yes/no)\n"))
saveOutput, err := promptForInput(ctx, reader, PromptSaveOutputToFile)
if err != nil {
if err == context.Canceled || err == io.EOF {
// If the error is context.Canceled or io.EOF, exit gracefully.
Expand Down Expand Up @@ -260,7 +281,7 @@ func executeCSVConversion(fs filesystem.FileSystem, ctx context.Context, formatO
var csvFileName string
var err error

if formatOption != 3 {
if formatOption != OutputFormatSeparateCSV {
csvFileName, err = promptForInput(ctx, reader, "Enter the name of the CSV file to save: ")
if err != nil {
if err == context.Canceled || err == io.EOF {
Expand All @@ -276,7 +297,7 @@ func executeCSVConversion(fs filesystem.FileSystem, ctx context.Context, formatO
}

switch formatOption {
case 3:
case OutputFormatSeparateCSV:
// If the user chooses to create separate files, prompt for file names and execute accordingly.
// Pass the FileSystem to createSeparateCSVFiles
createSeparateCSVFiles(fs, ctx, reader, sessions)
Expand All @@ -290,7 +311,7 @@ func executeCSVConversion(fs filesystem.FileSystem, ctx context.Context, formatO
// createSeparateCSVFiles prompts the user for file names and creates separate CSV files for sessions and messages.
// This function is context-aware and supports cancellation during the prompt for input.
func createSeparateCSVFiles(fs filesystem.FileSystem, ctx context.Context, reader *bufio.Reader, sessions []exporter.Session) {
sessionsFileName, err := promptForInput(ctx, reader, "Enter the name of the sessions CSV file to save: ")
sessionsFileName, err := promptForInput(ctx, reader, PromptEnterSessionsCSVFileName)
if err != nil {
if err == context.Canceled || err == io.EOF {
// If the error is context.Canceled or io.EOF, exit gracefully.
Expand All @@ -303,7 +324,7 @@ func createSeparateCSVFiles(fs filesystem.FileSystem, ctx context.Context, reade
}
}

messagesFileName, err := promptForInput(ctx, reader, "Enter the name of the messages CSV file to save: ")
messagesFileName, err := promptForInput(ctx, reader, PromptEnterMessagesCSVFileName)
if err != nil {
if err == context.Canceled || err == io.EOF {
// If the error is context.Canceled or io.EOF, exit gracefully.
Expand Down Expand Up @@ -351,7 +372,7 @@ func convertToSingleCSV(fs filesystem.FileSystem, ctx context.Context, sessions
// writeContentToFile collects a file name from the user and writes the provided content to the specified file.
// It now includes context support to handle potential cancellation during file writing.
func writeContentToFile(fs filesystem.FileSystem, ctx context.Context, reader *bufio.Reader, content string, fileType string) error {
fileName, err := promptForInput(ctx, reader, fmt.Sprintf("Enter the name of the %s file to save: ", fileType))
fileName, err := promptForInput(ctx, reader, fmt.Sprintf(PromptEnterFileName, fileType))
if err != nil {
return err
}
Expand Down
35 changes: 17 additions & 18 deletions main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"os"
"strings"
Expand Down Expand Up @@ -50,7 +51,8 @@ func TestProcessCSVOption(t *testing.T) {
}

// Simulate user input by creating a reader that will return the input as if typed by a user.
input := "4\noutput.csv\n" // User selects option 4 and specifies "output.csv" as the file name.
// Assuming that the option for OutputFormatSeparateCSVFiles is 4 as per the constants defined in session.go.
input := fmt.Sprintf("%d\noutput_sessions.csv\noutput_messages.csv\n", 3)
reader := bufio.NewReader(strings.NewReader(input))

// Create a cancellable context to allow for timeout or cancellation of the process.
Expand All @@ -60,40 +62,37 @@ func TestProcessCSVOption(t *testing.T) {
// Create an instance of the mock file system
mockFS := filesystem.NewMockFileSystem()

// Redirect stdout to a pipe where we can capture the output of the function.
r, w, _ := os.Pipe()
// Capture the output of the function by redirecting stdout.
oldStdout := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w

// Invoke the processCSVOption function, which should process the input and generate a CSV file.
// Invoke the processCSVOption function, which should process the input and generate CSV files.
processCSVOption(mockFS, ctx, reader, store.ChatNextWebStore.Sessions)

// Close the write-end of the pipe to finish capturing the output.
w.Close()
os.Stdout = oldStdout // Restore the original stdout.

// Read the captured output from the read-end of the pipe into a buffer for assertion.
var buf bytes.Buffer
io.Copy(&buf, r)

// Restore the original stdout.
os.Stdout = oldStdout

// Convert the captured output into a string for easy comparison.
outputStr := buf.String()

// Assert that the "output.csv" file was created by the function.
_, err = os.Stat("output.csv")
if os.IsNotExist(err) {
t.Errorf("Expected file 'output.csv' was not created")
// Check that the captured output contains the expected success messages.
expectedOutputSession := "Sessions data saved to output_sessions.csv\n"
expectedOutputMessage := "Messages data saved to output_messages.csv\n"
if !strings.Contains(outputStr, expectedOutputSession) {
t.Errorf("Expected output to contain: %s, got: %s", expectedOutputSession, outputStr)
}
// Clean up by removing the test output files.
defer os.Remove("output_sessions.csv")
defer os.Remove("output_messages.csv")

// Clean up by removing the test output file.
defer os.Remove("output.csv")

// Check that the captured output contains the expected success message.
expectedOutput := "CSV output saved to output.csv\n"
if !strings.Contains(outputStr, expectedOutput) {
t.Errorf("Expected output to contain: %s, got: %s", expectedOutput, outputStr)
if !strings.Contains(outputStr, expectedOutputMessage) {
t.Errorf("Expected output to contain: %s, got: %s", expectedOutputMessage, outputStr)
}
}

Expand Down

0 comments on commit 418f0af

Please sign in to comment.