From 3ed599aae82b68aead551b82a0accda4d80caee5 Mon Sep 17 00:00:00 2001
From: Hans Moog <hm@mkjc.net>
Date: Sat, 31 Aug 2019 03:00:35 +0200
Subject: [PATCH] Feat: started adding a stringify package for debugging
 purposes

To make debugging of the heartbeats a bit easier, this commit adds a stringify package which can show even very complex objects in a nice readable format like:

Heartbeat {
    nodeId: "c0a2918db00514d655f2b82199c6aedda234f807"
    mainStatement: OpinionStatement {
        previousStatementHash: <nil>
        nodeId: "c0a2918db00514d655f2b82199c6aedda234f807"
        time: 1567213114
        toggledTransactions: [
            ToggledTransaction {
                transactionId: 0x29f54622b0df47c864ceca1a5bb4ed84fb7dbbc1b4a6d9a029f79572b86f4d3c68bcca30b08953ac9d217a667facac7d4aa0
                initialStatement: true
                finalStatement: false
            },
            ToggledTransaction {
                transactionId: 0x774f74040c249b73ec4c390352e3191438899c166863b86765e1ef8546ea735bd0ff28dea6e35c9b7d66b35a5703b0c7fe63
                initialStatement: true
                finalStatement: false
            },
        ]
        signature: 0x57fa4ba799a2312b4e8561b144d9386703fce7f128c30c9ec2eeec73693878704d0a176cd4563f5afa8f5ceabdd2552f42bd17cd82363fc0bd1c1386edf914d401
    }
    neighborStatements: map{}
    signature: <nil>
}
---
 go.mod                                       |  2 +
 go.sum                                       |  2 +
 packages/ca/heartbeat/heartbeat.go           | 13 ++++-
 packages/ca/heartbeat/opinion_statement.go   | 18 +++----
 packages/ca/heartbeat/toggled_transaction.go | 10 ++++
 packages/ca/heartbeat_manager_test.go        |  6 ++-
 packages/stringify/bool.go                   |  9 ++++
 packages/stringify/constants.go              |  5 ++
 packages/stringify/int.go                    |  9 ++++
 packages/stringify/interface.go              | 50 ++++++++++++++++++++
 packages/stringify/map.go                    | 31 ++++++++++++
 packages/stringify/map_test.go               | 15 ++++++
 packages/stringify/slice.go                  | 41 ++++++++++++++++
 packages/stringify/slice_of_bytes.go         | 13 +++++
 packages/stringify/string.go                 |  5 ++
 packages/stringify/struct.go                 | 31 ++++++++++++
 packages/stringify/struct_field.go           | 17 +++++++
 17 files changed, 266 insertions(+), 11 deletions(-)
 create mode 100644 packages/stringify/bool.go
 create mode 100644 packages/stringify/constants.go
 create mode 100644 packages/stringify/int.go
 create mode 100644 packages/stringify/interface.go
 create mode 100644 packages/stringify/map.go
 create mode 100644 packages/stringify/map_test.go
 create mode 100644 packages/stringify/slice.go
 create mode 100644 packages/stringify/slice_of_bytes.go
 create mode 100644 packages/stringify/string.go
 create mode 100644 packages/stringify/struct.go
 create mode 100644 packages/stringify/struct_field.go

diff --git a/go.mod b/go.mod
index 31b7140a..8e94e41a 100644
--- a/go.mod
+++ b/go.mod
@@ -12,12 +12,14 @@ require (
 	github.com/gorilla/websocket v1.4.0
 	github.com/iotaledger/iota.go v1.0.0-beta.7
 	github.com/kr/pretty v0.1.0 // indirect
+	github.com/kr/text v0.1.0
 	github.com/labstack/echo v3.3.10+incompatible
 	github.com/labstack/gommon v0.2.9 // indirect
 	github.com/pkg/errors v0.8.1
 	github.com/rivo/tview v0.0.0-20190721135419-23dc8a0944e4
 	github.com/rivo/uniseg v0.1.0 // indirect
 	github.com/stretchr/testify v1.3.0
+	github.com/tonnerre/golang-text v0.0.0-20130925195846-048ed3d792f7 // indirect
 	golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4
 	golang.org/x/net v0.0.0-20190724013045-ca1201d0de80
 	golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e // indirect
diff --git a/go.sum b/go.sum
index 3190f6cf..4321f8c8 100644
--- a/go.sum
+++ b/go.sum
@@ -100,6 +100,8 @@ github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoH
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/tonnerre/golang-text v0.0.0-20130925195846-048ed3d792f7 h1:E4pdTAo1tqctAfTr2tkxcX+JkDeJP84bQfjC3ONwoXQ=
+github.com/tonnerre/golang-text v0.0.0-20130925195846-048ed3d792f7/go.mod h1:J5H/d3ZVtRUjmP4Zf87sPSIUSCGFXorzn3hQtdODORQ=
 github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
 github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
 github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
diff --git a/packages/ca/heartbeat/heartbeat.go b/packages/ca/heartbeat/heartbeat.go
index 6087afd8..dd2d3da8 100644
--- a/packages/ca/heartbeat/heartbeat.go
+++ b/packages/ca/heartbeat/heartbeat.go
@@ -3,6 +3,8 @@ package heartbeat
 import (
 	"sync"
 
+	"github.com/iotaledger/goshimmer/packages/stringify"
+
 	"github.com/iotaledger/goshimmer/packages/errors"
 	"github.com/iotaledger/goshimmer/packages/marshaling"
 
@@ -28,7 +30,7 @@ func NewHeartbeat() *Heartbeat {
 
 func (heartbeat *Heartbeat) GetNodeId() string {
 	heartbeat.nodeIdMutex.RLock()
-	defer heartbeat.nodeIdMutex.RLock()
+	defer heartbeat.nodeIdMutex.RUnlock()
 
 	return heartbeat.nodeId
 }
@@ -126,3 +128,12 @@ func (heartbeat *Heartbeat) MarshalBinary() ([]byte, errors.IdentifiableError) {
 func (heartbeat *Heartbeat) UnmarshalBinary(data []byte) (err errors.IdentifiableError) {
 	return marshaling.Unmarshal(heartbeat, data, &heartbeatProto.HeartBeat{})
 }
+
+func (heartbeat *Heartbeat) String() string {
+	return stringify.Struct("Heartbeat",
+		stringify.StructField("nodeId", heartbeat.nodeId),
+		stringify.StructField("mainStatement", heartbeat.mainStatement),
+		stringify.StructField("neighborStatements", heartbeat.neighborStatements),
+		stringify.StructField("signature", heartbeat.signature),
+	)
+}
diff --git a/packages/ca/heartbeat/opinion_statement.go b/packages/ca/heartbeat/opinion_statement.go
index 8071114d..95819a48 100644
--- a/packages/ca/heartbeat/opinion_statement.go
+++ b/packages/ca/heartbeat/opinion_statement.go
@@ -1,10 +1,10 @@
 package heartbeat
 
 import (
-	"encoding/hex"
-	"strconv"
 	"sync"
 
+	"github.com/iotaledger/goshimmer/packages/stringify"
+
 	"github.com/iotaledger/goshimmer/packages/errors"
 	"github.com/iotaledger/goshimmer/packages/marshaling"
 
@@ -157,11 +157,11 @@ func (opinionStatement *OpinionStatement) UnmarshalBinary(data []byte) (err erro
 }
 
 func (opinionStatement *OpinionStatement) String() string {
-	return "OpinionStatement {\n" +
-		"    previousStatementHash: 0x" + hex.EncodeToString(opinionStatement.previousStatementHash) + "\n" +
-		"    nodeId:                " + opinionStatement.nodeId + "\n" +
-		"    time:                  " + strconv.Itoa(int(opinionStatement.time)) + "\n" +
-		"    toggledTransactions:   [" + "" + "]\n" +
-		"    signature:             0x" + hex.EncodeToString(opinionStatement.signature) + "\n" +
-		"}"
+	return stringify.Struct("OpinionStatement",
+		stringify.StructField("previousStatementHash", opinionStatement.previousStatementHash),
+		stringify.StructField("nodeId", opinionStatement.nodeId),
+		stringify.StructField("time", opinionStatement.time),
+		stringify.StructField("toggledTransactions", opinionStatement.toggledTransactions),
+		stringify.StructField("signature", opinionStatement.signature),
+	)
 }
diff --git a/packages/ca/heartbeat/toggled_transaction.go b/packages/ca/heartbeat/toggled_transaction.go
index a6fb4bb9..8d81371c 100644
--- a/packages/ca/heartbeat/toggled_transaction.go
+++ b/packages/ca/heartbeat/toggled_transaction.go
@@ -3,6 +3,8 @@ package heartbeat
 import (
 	"sync"
 
+	"github.com/iotaledger/goshimmer/packages/stringify"
+
 	"github.com/iotaledger/goshimmer/packages/errors"
 	"github.com/iotaledger/goshimmer/packages/marshaling"
 
@@ -89,3 +91,11 @@ func (toggledTransaction *ToggledTransaction) MarshalBinary() ([]byte, errors.Id
 func (toggledTransaction *ToggledTransaction) UnmarshalBinary(data []byte) (err errors.IdentifiableError) {
 	return marshaling.Unmarshal(toggledTransaction, data, &heartbeatProto.ToggledTransaction{})
 }
+
+func (toggledTransaction *ToggledTransaction) String() string {
+	return stringify.Struct("ToggledTransaction",
+		stringify.StructField("transactionId", toggledTransaction.transactionId),
+		stringify.StructField("initialStatement", toggledTransaction.initialStatement),
+		stringify.StructField("finalStatement", toggledTransaction.finalStatement),
+	)
+}
diff --git a/packages/ca/heartbeat_manager_test.go b/packages/ca/heartbeat_manager_test.go
index 9c5bc1a5..42b82105 100644
--- a/packages/ca/heartbeat_manager_test.go
+++ b/packages/ca/heartbeat_manager_test.go
@@ -12,8 +12,12 @@ func TestHeartbeatManager_GenerateHeartbeat(t *testing.T) {
 	transactionId1 := make([]byte, 50)
 	rand.Read(transactionId1)
 
+	transactionId2 := make([]byte, 50)
+	rand.Read(transactionId2)
+
 	heartbeatManager := NewHeartbeatManager(identity.GenerateRandomIdentity())
 	heartbeatManager.SetInitialOpinion(transactionId1)
+	heartbeatManager.SetInitialOpinion(transactionId2)
 
 	result, err := heartbeatManager.GenerateHeartbeat()
 	if err != nil {
@@ -22,5 +26,5 @@ func TestHeartbeatManager_GenerateHeartbeat(t *testing.T) {
 		return
 	}
 
-	fmt.Println(result.GetMainStatement())
+	fmt.Println(result)
 }
diff --git a/packages/stringify/bool.go b/packages/stringify/bool.go
new file mode 100644
index 00000000..0cd7045e
--- /dev/null
+++ b/packages/stringify/bool.go
@@ -0,0 +1,9 @@
+package stringify
+
+func Bool(value bool) string {
+	if value {
+		return "true"
+	} else {
+		return "false"
+	}
+}
diff --git a/packages/stringify/constants.go b/packages/stringify/constants.go
new file mode 100644
index 00000000..798bff37
--- /dev/null
+++ b/packages/stringify/constants.go
@@ -0,0 +1,5 @@
+package stringify
+
+const (
+	INDENTATION_SIZE = 4
+)
diff --git a/packages/stringify/int.go b/packages/stringify/int.go
new file mode 100644
index 00000000..0a98f381
--- /dev/null
+++ b/packages/stringify/int.go
@@ -0,0 +1,9 @@
+package stringify
+
+import (
+	"strconv"
+)
+
+func Int(value int) string {
+	return strconv.Itoa(value)
+}
diff --git a/packages/stringify/interface.go b/packages/stringify/interface.go
new file mode 100644
index 00000000..bbf4f198
--- /dev/null
+++ b/packages/stringify/interface.go
@@ -0,0 +1,50 @@
+package stringify
+
+import (
+	"fmt"
+	"reflect"
+	"strconv"
+)
+
+func Interface(value interface{}) string {
+	switch value.(type) {
+	case bool:
+		return Bool(value.(bool))
+	case string:
+		return String(value.(string))
+	case []byte:
+		return SliceOfBytes(value.([]byte))
+	case int:
+		return Int(value.(int))
+	case uint64:
+		return strconv.FormatUint(value.(uint64), 10)
+	case reflect.Value:
+		typeCastedValue := value.(reflect.Value)
+		switch typeCastedValue.Kind() {
+		case reflect.Slice:
+			return sliceReflect(typeCastedValue)
+		case reflect.String:
+			return String(typeCastedValue.String())
+		case reflect.Int:
+			return Int(int(typeCastedValue.Int()))
+		case reflect.Uint8:
+			return Int(int(typeCastedValue.Uint()))
+		case reflect.Ptr:
+			return Interface(typeCastedValue.Interface())
+		default:
+			panic("undefined reflect type: " + typeCastedValue.Kind().String())
+		}
+	case fmt.Stringer:
+		return value.(fmt.Stringer).String()
+	default:
+		value := reflect.ValueOf(value)
+		switch value.Kind() {
+		case reflect.Slice:
+			return sliceReflect(value)
+		case reflect.Map:
+			return mapReflect(value)
+		default:
+			panic("undefined type: " + value.Kind().String())
+		}
+	}
+}
diff --git a/packages/stringify/map.go b/packages/stringify/map.go
new file mode 100644
index 00000000..b81741e2
--- /dev/null
+++ b/packages/stringify/map.go
@@ -0,0 +1,31 @@
+package stringify
+
+import (
+	"reflect"
+	"strings"
+
+	"github.com/kr/text"
+)
+
+func Map(value interface{}) string {
+	return mapReflect(reflect.ValueOf(value))
+}
+
+func mapReflect(value reflect.Value) (result string) {
+	result = "map{"
+
+	mapKeys := value.MapKeys()
+	if len(mapKeys) >= 1 {
+		result += "\n"
+	}
+
+	for _, mapKey := range mapKeys {
+		item := value.MapIndex(mapKey)
+
+		result += text.Indent("["+Interface(mapKey)+"]: "+Interface(item)+",\n", strings.Repeat(" ", INDENTATION_SIZE))
+	}
+
+	result += "}"
+
+	return
+}
diff --git a/packages/stringify/map_test.go b/packages/stringify/map_test.go
new file mode 100644
index 00000000..b1fb2740
--- /dev/null
+++ b/packages/stringify/map_test.go
@@ -0,0 +1,15 @@
+package stringify
+
+import (
+	"fmt"
+	"testing"
+)
+
+func TestMap(t *testing.T) {
+	testMap := make(map[string][]byte)
+
+	testMap["huhu"] = []byte{1, 2}
+	testMap["haha"] = []byte{1, 2, 3, 4}
+
+	fmt.Println(Map(testMap))
+}
diff --git a/packages/stringify/slice.go b/packages/stringify/slice.go
new file mode 100644
index 00000000..21c5ffb9
--- /dev/null
+++ b/packages/stringify/slice.go
@@ -0,0 +1,41 @@
+package stringify
+
+import (
+	"reflect"
+	"strings"
+
+	"github.com/kr/text"
+)
+
+func Slice(value []interface{}) string {
+	return sliceReflect(reflect.ValueOf(value))
+}
+
+func sliceReflect(value reflect.Value) (result string) {
+	result += "["
+
+	newLineVersion := false
+	for i := 0; i < value.Len(); i++ {
+		item := value.Index(i)
+
+		valueString := Interface(item)
+		if strings.Contains(valueString, "\n") {
+			if !newLineVersion {
+				result += "\n"
+
+				newLineVersion = true
+			}
+			result += text.Indent(Interface(item)+",\n", strings.Repeat(" ", INDENTATION_SIZE))
+		} else {
+			result += Interface(item) + ", "
+		}
+	}
+
+	if !newLineVersion {
+		result = result[:len(result)-2]
+	}
+
+	result += "]"
+
+	return
+}
diff --git a/packages/stringify/slice_of_bytes.go b/packages/stringify/slice_of_bytes.go
new file mode 100644
index 00000000..558a2576
--- /dev/null
+++ b/packages/stringify/slice_of_bytes.go
@@ -0,0 +1,13 @@
+package stringify
+
+import "encoding/hex"
+
+func SliceOfBytes(value []byte) string {
+	if value == nil {
+		return "<nil>"
+	} else if len(value) == 0 {
+		return "<empty>"
+	} else {
+		return "0x" + hex.EncodeToString(value) + ""
+	}
+}
diff --git a/packages/stringify/string.go b/packages/stringify/string.go
new file mode 100644
index 00000000..e25c63f5
--- /dev/null
+++ b/packages/stringify/string.go
@@ -0,0 +1,5 @@
+package stringify
+
+func String(value string) string {
+	return "\"" + value + "\""
+}
diff --git a/packages/stringify/struct.go b/packages/stringify/struct.go
new file mode 100644
index 00000000..9d1a50a6
--- /dev/null
+++ b/packages/stringify/struct.go
@@ -0,0 +1,31 @@
+package stringify
+
+import (
+	"strings"
+
+	"github.com/kr/text"
+)
+
+func Struct(name string, fields ...*structField) string {
+	return structBuilder{
+		name:   name,
+		fields: fields,
+	}.String()
+}
+
+type structBuilder struct {
+	name   string
+	fields []*structField
+}
+
+func (stringifyStruct structBuilder) String() (result string) {
+	result = stringifyStruct.name + " {\n"
+
+	for _, field := range stringifyStruct.fields {
+		result += text.Indent(field.String()+"\n", strings.Repeat(" ", INDENTATION_SIZE))
+	}
+
+	result += "}"
+
+	return result
+}
diff --git a/packages/stringify/struct_field.go b/packages/stringify/struct_field.go
new file mode 100644
index 00000000..d4190b22
--- /dev/null
+++ b/packages/stringify/struct_field.go
@@ -0,0 +1,17 @@
+package stringify
+
+type structField struct {
+	name  string
+	value interface{}
+}
+
+func StructField(name string, value interface{}) *structField {
+	return &structField{
+		name:  name,
+		value: value,
+	}
+}
+
+func (structField *structField) String() (result string) {
+	return structField.name + ": " + Interface(structField.value)
+}
-- 
GitLab