1

I'm using crypto/ssh package and I'm trying to write a unit test for a method that constructs the ClientConfig.

One of the assertions in the unit is that the returned ClientConfig is deeply equal to the expected. The assertion fails because both Auth and HostKeyCallback fields of ClientConfig are not deeply equal. The HostKeyCallback is hardcoded to be ssh.InsecureIgnoreHostKey(). The only authentication method I'm testing right now is with password and I have verified that the password string is picked up correctly.

I tried to mess around in playground (see here) and I don't understand why there is no deep equality in these cases.

package main

import (
    "fmt"
    "reflect"

    "golang.org/x/crypto/ssh"
)

func main() {
    pass := "bar"
    auth := []ssh.AuthMethod{ssh.Password(pass)}
    authLiteral := []ssh.AuthMethod{ssh.Password("bar")}
    if reflect.DeepEqual(authLiteral, auth) {
        fmt.Println("authentication methods are equal")
    } else {
        fmt.Println("authentication methods are not equal")
    }

    callback1 := ssh.InsecureIgnoreHostKey()
    callback2 := ssh.InsecureIgnoreHostKey()
    if reflect.DeepEqual(callback1, callback2) {
        fmt.Println("callbacks are equal")
    } else {
        fmt.Println("callbacks are not equal")
    }
}
authentication methods are not equal
callbacks are not equal

Could someone please explain these results? I would also be grateful if you could suggest how I could unit test this case.

  • 2
    Non-nil functions in Go are not comparable. No matter how "deep" you go. [`reflect.DeepEqual`](https://pkg.go.dev/reflect@go1.18#DeepEqual): *"Func values are deeply equal if both are nil; otherwise they are not deeply equal."* – mkopriva Apr 02 '22 at 10:03
  • @mkopriva thanks a lot for pointing this out, I missed this part. I assume there is no way to make the assertion that I want in ClientConfig then... – Chronopoios Apr 02 '22 at 10:19
  • *One of the assertions in the unit is that the returned `ClientConfig` is deeply equal to the expected.* Why this check? – jub0bs Apr 02 '22 at 10:31
  • I have a method that generates the `ClientConfig` based on some configuration values (e.g. the username and password could come from a yaml file or from env variable etc) so there are multiple paths to test. The method returns `*ClientConfig, error` so I though about checking that the returned values are the expected ones. Am I off of the idiomatic go unit-testing? – Chronopoios Apr 02 '22 at 10:43

1 Answers1

0

As mkopriva notes in a comment, this is formally disallowed. (Still, it might be nice if reflect.DeepEqual, which comes with the Go implementation, did something like I did below. Because it comes with the implementation, the implementor can use special knowledge to write it.)

Ultimately, of course, there's some machine-level implementation for pointer to function taking arguments of types T1, T2, ... Tna and returning values value of types R1, R2, ... Rnr . But we're not told what that representation is. It might be a single unsafe.Pointer value, or it might be some kind of inline expansion of a thunk. So we don't know how many bytes to compare here.

If you're willing to make rather dangerous / non-portable assumptions, and update them if and when they prove inadequate, there is a way to "cheat" here. I made this example on the Go Playground, which produces the desired output. Is this just by chance, or does this really work on your Go implementation?

The text is also below. Use at your own risk!

package main

import (
    "fmt"
    "unsafe"
)

func dummy1() {}
func dummy2() {}

func main() {
    fmt.Println("dummy1 == dummy2 =>", qe(dummy1, dummy2))
    fmt.Println("dummy1 == dummy1 =>", qe(dummy1, dummy1))
}

// "questionably equal", for function pointers
func qe(a, b func()) bool {
    pp1 := (*unsafe.Pointer)(unsafe.Pointer(&a))
    pp2 := (*unsafe.Pointer)(unsafe.Pointer(&b))
    p1 := *pp1
    p2 := *pp2
    return p1 == p2
}
torek
  • 448,244
  • 59
  • 642
  • 775