openpredictionmarkets / socialpredict

Easy to Deploy Prediction Market Platform
https://github.com/openpredictionmarkets/socialpredict
MIT License
65 stars 11 forks source link

Use handler/helper package across all handlers to deal with error reporting. #328

Open pwdel opened 2 months ago

pwdel commented 2 months ago

We should decide:

pwdel commented 5 days ago

https://softwareengineering.stackexchange.com/questions/421756/what-is-a-good-approach-to-handling-exceptions

pwdel commented 5 days ago

I'm starting this off as I'm working through an MR and thinking about this function:

// singleShareYesNoAllocator determines the outcome of a single bet.
// Logs a fatal error and exits if the input condition (len(bets) == 1) is not met.
func singleShareYesNoAllocator(bets []models.Bet) string {
    if len(bets) != 1 {
        log.Fatalf("singleShareYesNoAllocator: expected len(bets) = 1, got %d", len(bets))
    }

    // Return the outcome of the single bet
    return bets[0].Outcome
}

Basically, singleShareYesNoAllocator() should never have been called if there wasn't a scenario where there wasn't a single share, so this represents a fatal error because it's logically inconsistent.

pwdel commented 5 days ago

From the standpoint of testing conditions which are going to have the FatalF function, I added additional logging;

// Logger interface for flexible logging.
type Logger interface {
    Fatalf(format string, args ...interface{})
    Printf(format string, args ...interface{}) // Add other log levels as needed.
}

// DefaultLogger provides default implementations for Logger methods.
type DefaultLogger struct{}

func (DefaultLogger) Fatalf(format string, args ...interface{}) {
    log.Fatalf(format, args...)
}

func (DefaultLogger) Printf(format string, args ...interface{}) {
    log.Printf(format, args...)
}

Which can be mocked in the same package with:


// MockLogger is a mock implementation of Logger for testing.
type MockLogger struct {
    CalledFatalf  bool
    MessageFatalf string
    CalledPrintf  bool
    MessagePrintf string
}

func (m *MockLogger) Fatalf(format string, args ...interface{}) {
    m.CalledFatalf = true
    m.MessageFatalf = fmt.Sprintf(format, args...)
    panic(m.MessageFatalf) // Simulate Fatalf by panicking
}

func (m *MockLogger) Printf(format string, args ...interface{}) {
    m.CalledPrintf = true
    m.MessagePrintf = fmt.Sprintf(format, args...)
}

This means that for example, we can do:

singleShareDirection := singleShareYesNoAllocator(bets, logging.DefaultLogger{})

Which is defined as:

// singleShareYesNoAllocator determines the outcome of a single bet.
// Logs a fatal error and exits if the input condition (len(bets) == 1) is not met.
func singleShareYesNoAllocator(bets []models.Bet, logger logging.Logger) string {
    if len(bets) != 1 {
        logger.Fatalf("singleShareYesNoAllocator: expected len(bets) = 1, got %d", len(bets))
    }

    return bets[0].Outcome
}

And then to test fatal conditions, we can do the following, and pass the test if a fatal error was expected:

    for _, tc := range tests {
        t.Run(tc.name, func(t *testing.T) {
            mockLogger := &logging.MockLogger{}

            defer func() {
                if r := recover(); r != nil {
                    if tc.expectFatal {
                        // Test passes if fatal was expected
                        t.Logf("[DEBUG] Test case %q: Expected fatal error occurred. Recovered: %v", tc.name, r)
                    } else {
                        // Test fails if panic was not expected
                        t.Errorf("Unexpected fatal error for case %q: %v", tc.name, r)
                    }
                } else if tc.expectFatal {
                    // Test fails if no fatal occurred but was expected
                    t.Errorf("Expected fatal error for case %q, but none occurred", tc.name)
                }
            }()

            // Call the function
            result := singleShareYesNoAllocator(tc.bets, mockLogger)

            // If no fatal error was expected, check the result
            if !tc.expectFatal {
                if result != tc.expected {
                    t.Errorf("Expected outcome %q, got %q", tc.expected, result)
                }
            }
        })
    }
pwdel commented 5 days ago

Reference this conversation for when and why these features were first added.

https://github.com/openpredictionmarkets/socialpredict/pull/383/files/1f593526b2979d2b0d4eb7f63b0fab53d69c69ae#r1855446793