2

I'd like to ensure with a test, that for each APIErrorCode constant defined as below, the map APIErrorCodeMessages contains an entry. How can I enumerate all constants of a certain type in Go?

// APIErrorCode represents the API error code
type APIErrorCode int

const (
    // APIErrorCodeAuthentication represents an authentication error and corresponds with HTTP 401
    APIErrorCodeAuthentication APIErrorCode = 1000
    // APIErrorCodeInternalError represents an unknown internal error and corresponds with HTTP 500
    APIErrorCodeInternalError APIErrorCode = 1001
)

// APIErrorCodeMessages holds all error messages for APIErrorCodes
var APIErrorCodeMessages = map[APIErrorCode]string{
    APIErrorCodeInternalError: "Internal Error",
}

I've looked into reflect and go/importer and tried tools/cmd/stringer without success.

fhe
  • 6,099
  • 1
  • 41
  • 44
  • Isnt this what you want? `for k, v := range APIErrorCodeMessages{ fmt.Printf("%T %T",k, v) }` – Minty Apr 24 '17 at 15:31

2 Answers2

3

Basic concept

The reflect package does not provide access to exported identifiers, as there is no guarantee they will be linked to the executable binary (and thus available at runtime); more on this: Splitting client/server code; and How to remove unused code at compile time?

This is a source-code level checking. What I would do is write a test that checks if the number of error code constants matches the map length. The solution below will only check the map length. An improved version (see below) may also check if the keys in the map match the values of the constant declarations too.

You may use the go/parser to parse the Go file containing the error code constants, which gives you an ast.File describing the file, containing the constant declarations. You just need to walk through it, and count the error code constant declarations.

Let's say your original file is named "errcodes.go", write a test file named "errcodes_test.go".

This is how the test function could look like:

func TestMap(t *testing.T) {
    fset := token.NewFileSet()
    f, err := parser.ParseFile(fset, "errcodes.go", nil, 0)
    if err != nil {
        t.Errorf("Failed to parse file: %v", err)
        return
    }

    errCodeCount := 0
    // Range through declarations:
    for _, dd := range f.Decls {
        if gd, ok := dd.(*ast.GenDecl); ok {
            // Find constant declrations:
            if gd.Tok == token.CONST {
                for _, sp := range gd.Specs {
                    if valSp, ok := sp.(*ast.ValueSpec); ok {
                        for _, name := range valSp.Names {
                            // Count those that start with "APIErrorCode"
                            if strings.HasPrefix(name.Name, "APIErrorCode") {
                                errCodeCount++
                            }
                        }
                    }
                }
            }
        }
    }
    if exp, got := errCodeCount, len(APIErrorCodeMessages); exp != got {
        t.Errorf("Expected %d err codes, got: %d", exp, got)
    }
}

Running go test will result in:

--- FAIL: TestMap (0.00s)
    errcodes_test.go:39: Expected 2 err codes, got: 1

The test properly reveals that there are 2 constant error code declarations, but the APIErrorCodeMessages map contains only 1 entry.

If we now "complete" the map:

var APIErrorCodeMessages = map[APIErrorCode]string{
    APIErrorCodeInternalError:  "Internal Error",
    APIErrorCodeAuthentication: "asdf",
}

And run go test again:

PASS

Note: it's a matter of style, but the big loop may be written this way to decrease nesting level:

// Range through declarations:
for _, dd := range f.Decls {
    gd, ok := dd.(*ast.GenDecl)
    if !ok {
        continue
    }
    // Find constant declrations:
    if gd.Tok != token.CONST {
        continue
    }
    for _, sp := range gd.Specs {
        valSp, ok := sp.(*ast.ValueSpec)
        if !ok {
            continue
        }
        for _, name := range valSp.Names {
            // Count those that start with "APIErrorCode"
            if strings.HasPrefix(name.Name, "APIErrorCode") {
                errCodeCount++
            }
        }
    }
}

Full, improved detection

This time we will check the exact type of the constants, not their names. We will also gather all the constant values, and in the end we will check each if that exact constant value is in the map. If something is missing, we will print the exact values of the missing codes.

So here it is:

func TestMap(t *testing.T) {
    fset := token.NewFileSet()
    f, err := parser.ParseFile(fset, "errcodes.go", nil, 0)
    if err != nil {
        t.Errorf("Failed to parse file: %v", err)
        return
    }

    var keys []APIErrorCode
    // Range through declarations:
    for _, dd := range f.Decls {
        gd, ok := dd.(*ast.GenDecl)
        if !ok {
            continue
        }
        // Find constant declrations:
        if gd.Tok != token.CONST {
            continue
        }
        for _, sp := range gd.Specs {
            // Filter by APIErrorCode type:
            valSp, ok := sp.(*ast.ValueSpec)
            if !ok {
                continue
            }
            if id, ok2 := valSp.Type.(*ast.Ident); !ok2 ||
                id.Name != "APIErrorCode" {
                continue
            }
            // And gather the constant values in keys:
            for _, value := range valSp.Values {
                bslit, ok := value.(*ast.BasicLit)
                if !ok {
                    continue
                }
                keyValue, err := strconv.Atoi(bslit.Value)
                if err != nil {
                    t.Errorf("Could not parse value from %v: %v",
                        bslit.Value, err)
                }
                keys = append(keys, APIErrorCode(keyValue))
            }
        }
    }

    for _, key := range keys {
        if _, found := APIErrorCodeMessages[key]; !found {
            t.Errorf("Could not found key in map: %v", key)
        }
    }
}

Running go test with an "incomplete" APIErrorCodeMessages map, we get the following output:

--- FAIL: TestMap (0.00s)
    errcodes_test.go:58: Could not found key in map: 1000
Community
  • 1
  • 1
icza
  • 389,944
  • 63
  • 907
  • 827
  • Thanks, @icza – this was I was looking for! – fhe Apr 24 '17 at 14:43
  • Just FYI, the full detection based on the type name requires constants to be specified with the type – and not using the short-hand notation where you can omit the type after the first declaration. The basic detection doesn't require this as it looks at the name only. – fhe Apr 25 '17 at 10:01
  • @fhe If you omit the type from the `const` declaration as in `APIErrorCodeInternalError = 1001`, it will not be of type `APIErrorCode` but an untyped integer constant, that's why it's not picked by by the full version (because it checks for constants of type `APIErrorCode`). See example to verify: [Go Playground](https://play.golang.org/p/gHWTVCVG9-) – icza Apr 25 '17 at 10:14
0

Short of static code analysis, which generates your tests, you can't.

You'll just need to maintain a list of known types somewhere. The most obvious place is probably in your test:

func TestAPICodes(t *testing.T) {
    for _, code := range []APIErrorCode{APIErrorCodeAuthentication, ...} {
        // Do your test here
    }
}

If you want the list defined closer to the code definitions, you could also put it in your main package:

// APIErrorCode represents the API error code
type APIErrorCode int

const (
    // APIErrorCodeAuthentication represents an authentication error and corresponds with HTTP 401
    APIErrorCodeAuthentication APIErrorCode = 1000
    // APIErrorCodeInternalError represents an unknown internal error and corresponds with HTTP 500
    APIErrorCodeInternalError APIErrorCode = 1001
)

var allCodes = []APIErrorCode{APIErrorCodeAuthentication, ...}

Or, if you're confident that your APIErrorCodeMessages map will be kept up-to-date, then you already have the solution. Just loop over that map in your test:

func TestAPICodes(t *testing.T) {
    for code := range APIErrorCodeMessages {
        // Do your tests...
    }
}
Jonathan Hall
  • 75,165
  • 16
  • 143
  • 189
  • Ideally, the known list would be the definition of the individual const values so that no redundant information must be kept. Regarding your last comment, the whole idea of the test would be to ensure that `APIErrorCodeMessages` will be up-to-date :) – fhe Apr 24 '17 at 09:43
  • @fhe: I understand the desire! Sadly, there's no way to do that, short of source-code analysis. (Although there are a ton of Go source analysis tools, so it's not outside the realm of possibility to create a tool to do that. It would have to be a pre-compilation step, though) – Jonathan Hall Apr 24 '17 at 09:45