From eed5432206aab2d8599d42c0c751f2d0817c6478 Mon Sep 17 00:00:00 2001 From: Gautam Dey Date: Thu, 8 Aug 2019 22:35:54 -0400 Subject: [PATCH 1/2] [debugger] Added the debugger Added a way to record what is going on inside the algorithims and visulize it in qGIS. --- encoding/gpkg/binary_header.go | 22 +- internal/debugger/constants.go | 62 ++++++ internal/debugger/debugger.go | 203 ++++++++++++++++++ internal/debugger/doc.go | 104 +++++++++ internal/debugger/funcfileline.go | 19 ++ internal/debugger/record_test.go | 67 ++++++ internal/debugger/recorder.go | 145 +++++++++++++ .../debugger/recorder/funcfileline_test.go | 48 +++++ internal/debugger/recorder/gpkg/gpkg.go | 143 ++++++++++++ internal/debugger/recorder/recorder.go | 48 +++++ 10 files changed, 856 insertions(+), 5 deletions(-) create mode 100644 internal/debugger/constants.go create mode 100644 internal/debugger/debugger.go create mode 100644 internal/debugger/doc.go create mode 100644 internal/debugger/funcfileline.go create mode 100644 internal/debugger/record_test.go create mode 100644 internal/debugger/recorder.go create mode 100644 internal/debugger/recorder/funcfileline_test.go create mode 100644 internal/debugger/recorder/gpkg/gpkg.go create mode 100644 internal/debugger/recorder/recorder.go diff --git a/encoding/gpkg/binary_header.go b/encoding/gpkg/binary_header.go index 5e934a46..f8129292 100644 --- a/encoding/gpkg/binary_header.go +++ b/encoding/gpkg/binary_header.go @@ -332,20 +332,32 @@ func (sb StandardBinary) Encode() ([]byte, error) { } func NewBinary(srs int32, geo geom.Geometry) (*StandardBinary, error) { - extent, err := geom.NewExtentFromGeometry(geo) - if err != nil { - return nil, err + + var ( + emptyGeo = geom.IsEmpty(geo) + err error + extent = []float64{nan, nan, nan, nan} + h *BinaryHeader + ) + + if !emptyGeo { + ext, err := geom.NewExtentFromGeometry(geo) + if err != nil { + return nil, err + } + extent = ext[:] } - h, err := NewBinaryHeader(binary.LittleEndian, srs, extent[:], EnvelopeTypeXY, false, false) + + h, err = NewBinaryHeader(binary.LittleEndian, srs, extent, EnvelopeTypeXY, false, emptyGeo) if err != nil { return nil, err } + return &StandardBinary{ Header: h, SRSID: srs, Geometry: geo, }, nil - } func (sb *StandardBinary) Extent() *geom.Extent { diff --git a/internal/debugger/constants.go b/internal/debugger/constants.go new file mode 100644 index 00000000..b7e752b7 --- /dev/null +++ b/internal/debugger/constants.go @@ -0,0 +1,62 @@ +package debugger + +import ( + "fmt" + "strings" + "unicode/utf8" +) + +const ( + + // CategoryGot is for got values of a testcase + CategoryGot = "got" + // CategoryExpected is the expected values of a testcase + CategoryExpected = "expected" + // CategoryInput is the input values of a testcase + CategoryInput = "input" +) + +// CategoryFormatter is a helper category that will build a category +// made up of a set of values given with the With function. +// The category should be a template of where the With values will go. +// For example CategoryFormatter("%v_triangle_%v").With(1,10) will result +// in a string of `1_triangle_10` this can be used to create common +// category names. +type CategoryFormatter string + +func (f CategoryFormatter) With(data ...interface{}) string { return fmt.Sprintf(string(f), data...) } +func (f CategoryFormatter) String() string { return string(f) } + +// CategoryJoiner is a helper category that will build a category +// made up of a set of values given with the With function seperated +// the the last character of the category. +// For example CategoryJoiner("triangle:").With(1,10) will result +// in a string of `triangle:1:10` this can be used to create common +// category names. +type CategoryJoiner string + +func (f CategoryJoiner) With(data ...interface{}) string { + + var ( + s strings.Builder + addc bool + join string + ) + + if len(string(f)) > 0 { + r, _ := utf8.DecodeLastRune([]byte(f)) + join = string([]rune{r}) + } + + s.WriteString(string(f)) + for _, v := range data { + if addc { + s.WriteString(join) + } + fmt.Fprintf(&s, "%v", v) + addc = true + } + return s.String() +} + +func (f CategoryJoiner) String() string { return string(f) } diff --git a/internal/debugger/debugger.go b/internal/debugger/debugger.go new file mode 100644 index 00000000..7b1176de --- /dev/null +++ b/internal/debugger/debugger.go @@ -0,0 +1,203 @@ +package debugger + +import ( + "context" + "fmt" + "log" + "os" + "path/filepath" + "strings" + "sync" + "unicode" + + "github.com/go-spatial/geom/internal/debugger/recorder/gpkg" + "github.com/pborman/uuid" +) + +const ( + // ContextRecorderKey the key to store the recorder in the context + ContextRecorderKey = "debugger_recorder_key" + + // ContextRecorderKey the key to store the testname in the context + ContextRecorderTestnameKey = "debugger_recorder_testname_key" +) + +// DefaultOutputDir is where the system will write the debugging db/files +// By default this will use os.TempDir() to write to the system temp directory +// set this in an init function to wite elsewhere. +var DefaultOutputDir = os.TempDir() + +// AsString will create string contains the stringified items seperated by a ':' +func AsString(vs ...interface{}) string { + var s strings.Builder + var addc bool + for _, v := range vs { + if addc { + s.WriteString(":") + } + fmt.Fprintf(&s, "%v", v) + addc = true + } + return s.String() +} + +// GetRecorderFromContext will return the recoder that is +// in the context. If there isn't a recorder, then an invalid +// recorder will be returned. This can be checked with the +// IsValid() function on the recorder. +func GetRecorderFromContext(ctx context.Context) Recorder { + + r, _ := ctx.Value(ContextRecorderKey).(Recorder) + + if name, ok := ctx.Value(ContextRecorderTestnameKey).(string); ok { + r.Desc.Name = name + } + + return r +} + +var lck sync.Mutex +var recrds = make(map[string]struct { + fn string + rcrd *recorder +}) + +func cleanupFilename(fn string) string { + const replaceValues = ` []{}"'^%*&\,;?!()` + var nfn strings.Builder + for _, r := range strings.TrimSpace(strings.ToLower(fn)) { + switch { + case !unicode.IsPrint(r) || unicode.IsSpace(r): + // no opt + case strings.ContainsRune(replaceValues, r): + nfn.WriteRune('_') + default: + nfn.WriteRune(r) + } + } + return nfn.String() +} +func getFilenameDir(initialFilename string) (dir, filename string) { + + initialFilename = cleanupFilename(initialFilename) + fullFilename := filepath.Clean(filepath.Join(DefaultOutputDir, initialFilename)) + dir = filepath.Dir(fullFilename) + filename = filepath.Base(fullFilename) + return dir, filename +} + +// AugmentRecorder is will create and configure a new recorder (if needed) to +// be used to track the debugging entries +// A Close call on the recoder should be supplied along with the +// AugmentRecorder call, this is usually done using a defer +// If the testFilename is "", then the function name of the calling function +// will be used as the filename for the database file. +func AugmentRecorder(rec Recorder, testFilename string) (Recorder, bool) { + + if rec.IsValid() { + rec.IncrementCount() + return rec, false + } + + if testFilename == "" { + testFilename = funcFileLine().Func + } + dir, filename := getFilenameDir(testFilename) + + err := os.MkdirAll(dir, os.ModePerm) + if err != nil { + panic(fmt.Sprintf("Failed to created dir %v:%v", dir, err)) + } + + lck.Lock() + defer lck.Unlock() + rcrd, ok := recrds[testFilename] + if !ok { + rcd, filename, err := gpkg.New(dir, filename, 0) + if err != nil { + panic(fmt.Sprintf("Failed to created gpkg db: %v", err)) + os.Exit(1) + } + rcrd = struct { + fn string + rcrd *recorder + }{ + fn: filename, + rcrd: &recorder{Interface: rcd}, + } + recrds[testFilename] = rcrd + + } + rcrd.rcrd.IncrementCount() + log.Println("Writing debugger output to", rcrd.fn) + + return Recorder{ + recorder: rcrd.rcrd, + Desc: TestDescription{ + Name: uuid.NewRandom().String(), + }, + }, true + +} + +// AugmentContext is will add and configure the recorder used to track the +// debugging entries into the context. +// A Close call should be supplied along with the AugmentContext call, this +// is usually done using a defer +// If the testFilename is "", then the function name of the calling function +// will be used as the filename for the database file. +func AugmentContext(ctx context.Context, testFilename string) context.Context { + if testFilename == "" { + testFilename = funcFileLine().Func + } + + if rec, newRec := AugmentRecorder(GetRecorderFromContext(ctx), testFilename); newRec { + return context.WithValue(ctx, ContextRecorderKey, rec) + } + return ctx +} + +// Close allows the recorder to release any resources it as, each +// AugmentContext call should have a mirroring Close call that is +// called at the end of the function. +func Close(ctx context.Context) { + GetRecorderFromContext(ctx).Close() +} + +func CloseWait(ctx context.Context) { + GetRecorderFromContext(ctx).CloseWait() +} +func SetTestName(ctx context.Context, name string) context.Context { + return context.WithValue( + ctx, + ContextRecorderTestnameKey, + name, + ) +} + +// Record records the geom and descriptive attributes into the debugging system +func Record(ctx context.Context, geom interface{}, category string, descriptionFormat string, data ...interface{}) { + RecordFFLOn(GetRecorderFromContext(ctx), FFL(1), geom, category, descriptionFormat, data...) +} + +// RecordOn records the geom and descriptive attributes into the debugging system +func RecordOn(rec Recorder, geom interface{}, category string, descriptionFormat string, data ...interface{}) { + RecordFFLOn(rec, FFL(1), geom, category, descriptionFormat, data...) +} + +// RecordFFLOn records the geom and descriptive attributes into the debugging system with the give Func File Line values +func RecordFFLOn(rec Recorder, ffl FuncFileLineType, geom interface{}, category string, descriptionFormat string, data ...interface{}) { + if !rec.IsValid() { + return + } + description := fmt.Sprintf(descriptionFormat, data...) + + rec.Record( + geom, + ffl, + TestDescription{ + Category: category, + Description: description, + }, + ) +} diff --git a/internal/debugger/doc.go b/internal/debugger/doc.go new file mode 100644 index 00000000..0341ad91 --- /dev/null +++ b/internal/debugger/doc.go @@ -0,0 +1,104 @@ +/* + Package debugger provides a way for us to capture partial +geometries during geometry processing. The geometries are +stored in a `spatailite` database. + + The general way to use the package is with a `context.Context` + variable. The package uses context as a way to pass around + the recorders, that can easily be disabled. + + An example of how this would be used in a function doing work: + + + func Foo(ctx context.Context, ... ) (...) { + // At the top of the package we usually + // will enhance the context + if debug { + ctx = debugger.AugmentContext(ctx, "") + defer debugger.Close(ctx) + } + + ... // Do things till you want to record the state for + + if debug { + for i, seg := ranage segments { + debugger.Record(ctx, + seg, + "A Category", + "Description Format %v", i, + ) + } + } + + ... // more work + + } + + +To use the package in a test, one would do something similar. +In your test function, call the `debugger.SetTestName`, with +the test name. The best way to do this is via the `t.Name()` +function in a `testing.T` object. The `context.Context` variable +will need to be Augmented as well -- the first Augment call +in a chain takes precedent, so in each of your functions you +can call this with no worries. + +See the following example: + + + func TestFoo(t *testing.T){ + type tcase struct { + ... + } + + fn := func(ctx context.Context, tc tcase) func(*testing.T){ + return func(t *testing.T){ + + if debug { + debugger.SetTestName(ctx, t.Name()) + } + + ... + ... = Foo(...) + ... + + if got != tc.expected { + // record the inputs + if debug { + debugger.Record(ctx, + got, + debugger.CategoryGot, + "got segments", + ) + debugger.Record(ctx, + tc.expected, + debugger.CategoryExpected, + "expected segments", + ) + debugger.Record(ctx, + tc.input, + debugger.CategoryInput, + "input polygon", + ) + } + } + } + } + + tests := [...]tcase{ ... } + + ctx := context.Background() + + if debug { + ctx = debugger.AugmentContext(ctx, "") + defer debugger.Close(ctx) + } + + for _, tc := range tests { + t.Run(tc.name, fn(ctx, tc)) + } + } + + +*/ +package debugger diff --git a/internal/debugger/funcfileline.go b/internal/debugger/funcfileline.go new file mode 100644 index 00000000..e2d545fc --- /dev/null +++ b/internal/debugger/funcfileline.go @@ -0,0 +1,19 @@ +package debugger + +import ( + recdr "github.com/go-spatial/geom/internal/debugger/recorder" +) + +type FuncFileLineType = recdr.FuncFileLineType + +// FuncFileLine returns the func file and line number of the the number of callers +// above the caller of this function. Zero returns the immediate caller above the +// caller of the FuncFileLine func. +func FuncFileLine(lvl uint) (string, string, int) { + ffl := recdr.FuncFileLine(lvl + 1) + return ffl.Func, ffl.File, ffl.LineNumber +} + +func FFL(lvl uint) FuncFileLineType { return recdr.FuncFileLine(lvl + 1) } + +func funcFileLine() FuncFileLineType { return recdr.FuncFileLine(1) } diff --git a/internal/debugger/record_test.go b/internal/debugger/record_test.go new file mode 100644 index 00000000..0ac45110 --- /dev/null +++ b/internal/debugger/record_test.go @@ -0,0 +1,67 @@ +package debugger_test + +import ( + "context" + "testing" + + "github.com/go-spatial/geom/internal/debugger" + + "github.com/go-spatial/geom" +) + +func TestRecorder(t *testing.T) { + + type tcase struct { + name string + got geom.MultiLineString + expected geom.MultiLineString + input geom.Polygon + } + + fn := func(ctx context.Context, tc tcase) func(*testing.T) { + return func(t *testing.T) { + + debugger.SetTestName(ctx, t.Name()) + debugger.Record(ctx, + tc.got, + debugger.CategoryGot, + "got segments", + ) + debugger.Record(ctx, + tc.expected, + debugger.CategoryExpected, + "expected segments", + ) + debugger.Record(ctx, + tc.input, + debugger.CategoryInput, + "input polygon", + ) + } + } + + tests := []tcase{ + { + name: "test1", + got: geom.MultiLineString{ + {{1, 1}, {1, 2}}, + {{1, 2}, {2, 2}}, + {{1, 1}, {2, 2}}, + }, + expected: geom.MultiLineString{ + {{1, 1}, {1, 2}}, + {{1, 2}, {2, 2}}, + {{1, 1}, {2, 2}}, + }, + }, + } + + ctx := context.Background() + + ctx = debugger.AugmentContext(ctx, "") + defer debugger.Close(ctx) + + for _, tc := range tests { + t.Run(tc.name, fn(ctx, tc)) + } +} diff --git a/internal/debugger/recorder.go b/internal/debugger/recorder.go new file mode 100644 index 00000000..0d4dc6e4 --- /dev/null +++ b/internal/debugger/recorder.go @@ -0,0 +1,145 @@ +package debugger + +import ( + "log" + "sync" + + recdr "github.com/go-spatial/geom/internal/debugger/recorder" +) + +type TestDescription = recdr.TestDescription + +type recorder struct { + recdr.Interface + + wg sync.WaitGroup + + clck sync.Mutex + // Number of times the DB connection has been "initilized", and expect + // the same number of close statements, but only want to close on the + // last close() statement. + count uint + closed bool +} + +// IncrementCount used for reference counting for when to release +// resources; each thing holding a copy of this resource should +// call this if it intends to call Close() +func (rec *recorder) IncrementCount() { + if rec == nil { + return + } + rec.clck.Lock() + rec.count++ + rec.clck.Unlock() +} + +// Close will allows the recorder to free up any held resources +func (rec *recorder) Close() error { + if rec == nil { + return nil + } + rec.clck.Lock() + rec.count-- + c := rec.count + if !rec.closed && c <= 0 { + rec.closed = true + rec.clck.Unlock() + log.Println("Waiting for things to finish") + rec.wg.Wait() + log.Println("Done waiting for things to finish") + return rec.Interface.Close() + } + rec.clck.Unlock() + return nil +} + +// Close will allows the recorder to free up any held resources +func (rec *recorder) CloseWait() error { + if rec == nil { + return nil + } + rec.clck.Lock() + rec.count-- + c := rec.count + if !rec.closed && c <= 0 { + rec.closed = true + rec.clck.Unlock() + log.Println("Waiting for things to finish") + rec.wg.Wait() + log.Println("Done waiting for things to finish") + return rec.Interface.Close() + } + rec.clck.Unlock() + log.Println("Waiting for things to finish") + rec.wg.Wait() + return nil +} + +// Closed will report if the database is available for writing +func (rec *recorder) Closed() bool { + if rec == nil { + return true + } + rec.clck.Lock() + defer rec.clck.Unlock() + return rec.closed +} + +// Recorder is used to record entries into the debugging database +type Recorder struct { + *recorder + + // Desc is the template for the description to use when recording a + // test. + Desc TestDescription +} + +// IsValid is the given Recorder valid +func (rec Recorder) IsValid() bool { return !rec.recorder.Closed() } + +// Record will record an entry into the debugging Database. Zero values in the desc will be +// replaced by their corrosponding values in the Recorder.Desc +func (rec Recorder) Record(geom interface{}, ffl FuncFileLineType, desc TestDescription) error { + if !rec.IsValid() { + return nil + } + tstDesc := rec.Desc + if desc.Name != "" { + tstDesc.Name = desc.Name + } + if desc.Category != "" { + tstDesc.Category = desc.Category + } + if desc.Description != "" { + tstDesc.Description = desc.Description + } + return rec.recorder.Record(geom, ffl, tstDesc) +} + + +// AsyncRecord will record an entry into the debugging Database asynchronously. Zero values in the desc will be +// replaced by their corrosponding values in the Recorder.Desc +func (rec Recorder) AsyncRecord(geom interface{}, ffl FuncFileLineType, desc TestDescription) { + if !rec.IsValid() { + return + } + tstDesc := rec.Desc + if desc.Name != "" { + tstDesc.Name = desc.Name + } + if desc.Category != "" { + tstDesc.Category = desc.Category + } + if desc.Description != "" { + tstDesc.Description = desc.Description + } + rec.recorder.wg.Add(1) + go func() { + err := rec.recorder.Record(geom, ffl, tstDesc) + rec.recorder.wg.Done() + if err != nil { + log.Println("Got an error running recorder (async):", err) + } + }() +} diff --git a/internal/debugger/recorder/funcfileline_test.go b/internal/debugger/recorder/funcfileline_test.go new file mode 100644 index 00000000..deeb48d0 --- /dev/null +++ b/internal/debugger/recorder/funcfileline_test.go @@ -0,0 +1,48 @@ +package recorder_test + +import ( + "path/filepath" + "strings" + "testing" + + "github.com/go-spatial/geom/internal/debugger/recorder" +) + +func levelTwoCall(lvl uint) recorder.FuncFileLineType { + return levelOneCall(lvl) +} + +func levelOneCall(lvl uint) recorder.FuncFileLineType { + return recorder.FuncFileLine(lvl) +} + +func TestFuncFileLine(t *testing.T) { + /* + It's is important not to move this bit of + code as it will change the result of the + the test. + */ + testFFL(t, levelOneCall(0), "TestFuncFileLine", "funcfileline_test.go", 25) + testFFL(t, levelTwoCall(1), "TestFuncFileLine", "funcfileline_test.go", 26) + + testFFL(t, levelTwoCall(0), "levelTwoCall", "funcfileline_test.go", 12) + + testFFL(t, levelOneCall(1), "tRunner", "testing.go", -1) +} + +func testFFL(t *testing.T, ffl recorder.FuncFileLineType, funcName, filename string, ln int) { + funcNameStrs := strings.Split(ffl.Func, ".") + gotFuncName := funcNameStrs[len(funcNameStrs)-1] + if gotFuncName != funcName { + t.Errorf("Func, expected %v got %v", funcName, gotFuncName) + } + if ln != -1 { + if ffl.LineNumber != ln { + t.Errorf("LineNumber, expected %v got %v", ln, ffl.LineNumber) + } + } + file := filepath.Base(ffl.File) + if file != filename { + t.Errorf("LineNumber, expected %v got %v", filename, file) + } +} diff --git a/internal/debugger/recorder/gpkg/gpkg.go b/internal/debugger/recorder/gpkg/gpkg.go new file mode 100644 index 00000000..4e5bc3a8 --- /dev/null +++ b/internal/debugger/recorder/gpkg/gpkg.go @@ -0,0 +1,143 @@ +package gpkg + +import ( + "database/sql" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/go-spatial/geom/encoding/gpkg" + + "github.com/go-spatial/geom/internal/debugger/recorder" +) + +const ( + createTableSQLFmt = ` + DROP TABLE IF EXISTS %[1]s; + CREATE TABLE "%[1]s" ( + id INTEGER PRIMARY KEY AUTOINCREMENT + , function_name TEXT + , filename TEXT + , line INTEGER + , name TEXT + , description TEXT + , category TEXT + , geometry %[2]s + ) + ` + insertTableSQLFmt = ` + INSERT INTO "%[1]s" ( + function_name + , filename + , line + , name + , description + , category + , geometry + ) VALUES (?,?,?,?,?,?,?); + ` +) + +type DB struct { + *gpkg.Handle + + srsid int32 + tblPrefix string + filename string + statements map[gpkg.GeometryType]*sql.Stmt +} + +func New(outputDir, filename string, srsid int32) (*DB, string, error) { + + dbFilename := filepath.Join(outputDir, filename+".gpkg") + os.Remove(dbFilename) + + h, err := gpkg.Open(dbFilename) + if err != nil { + return nil, dbFilename, fmt.Errorf("dbfile: %v err: %v", dbFilename, err) + } + db := &DB{ + Handle: h, + srsid: srsid, + tblPrefix: "test_", + statements: make(map[gpkg.GeometryType]*sql.Stmt, 6), + } + + for _, gType := range []gpkg.GeometryType{ + gpkg.Point, gpkg.MultiPoint, + gpkg.Linestring, gpkg.MultiLinestring, + gpkg.Polygon, gpkg.MultiPolygon, + } { + lgType := strings.ToLower(gType.String()) + tblName := db.TableName(gType) + _, err := db.Exec(fmt.Sprintf(createTableSQLFmt, tblName, gType)) + if err != nil { + return nil, dbFilename, err + } + err = db.AddGeometryTable(gpkg.TableDescription{ + Name: tblName, + ShortName: fmt.Sprintf("test table for %v geometries", lgType), + Description: fmt.Sprintf("Table containting %v type entries.", lgType), + GeometryField: "geometry", + GeometryType: gType, + SRS: db.srsid, + Z: gpkg.Prohibited, + M: gpkg.Prohibited, + }) + if err != nil { + return nil, dbFilename, err + } + db.statements[gType], err = db.Prepare(fmt.Sprintf(insertTableSQLFmt, tblName)) + if err != nil { + return nil, dbFilename, err + } + } + + return db, dbFilename, nil +} + +func (db *DB) TableName(gType gpkg.GeometryType) string { + lgType := strings.ToLower(gType.String()) + return db.tblPrefix + lgType +} + +func (db *DB) Record(geo interface{}, ffl recorder.FuncFileLineType, tblTest recorder.TestDescription) error { + if db == nil { + return nil + } + + gtype := gpkg.TypeForGeometry(geo) + if gtype == gpkg.Geometry { + return fmt.Errorf("error unknown geometry type %t", geo) + } + + stm, ok := db.statements[gtype] + if !ok { + return fmt.Errorf("error unsupported geom %s", gtype) + } + + sb, err := gpkg.NewBinary(db.srsid, geo) + if err != nil { + return fmt.Errorf("error unsupported geometry %s : %v", gtype, err) + } + + _, err = stm.Exec( + + ffl.Func, + ffl.File, + ffl.LineNumber, + + tblTest.Name, + tblTest.Description, + tblTest.Category, + + sb, + ) + if err != nil { + return err + } + // update extent + db.UpdateGeometryExtent(db.TableName(gtype), sb.Extent()) + return nil +} diff --git a/internal/debugger/recorder/recorder.go b/internal/debugger/recorder/recorder.go new file mode 100644 index 00000000..b02abda0 --- /dev/null +++ b/internal/debugger/recorder/recorder.go @@ -0,0 +1,48 @@ +package recorder + +import ( + "runtime" + "strings" +) + +type TestDescription struct { + Name string + Category string + Description string +} + +type Interface interface { + Close() error + Record(geom interface{}, FFL FuncFileLineType, Description TestDescription) error +} + +type FuncFileLineType struct { + Func string + File string + LineNumber int +} + +// FuncFileLine returns the func file and line number of the the number of callers +// above the caller of this function. Zero returns the immediate caller above the +// caller of the FuncFileLine func. +func FuncFileLine(lvl uint) FuncFileLineType { + fnName := "unknown" + file := "unknown" + lineNo := -1 + + pc, _, _, ok := runtime.Caller(int(lvl) + 2) + details := runtime.FuncForPC(pc) + if ok && details != nil { + fnName = details.Name() + file, lineNo = details.FileLine(pc) + } + + vs := strings.Split(fnName, "/") + fnName = vs[len(vs)-1] + + return FuncFileLineType{ + Func: fnName, + File: file, + LineNumber: lineNo, + } +} From 17445338f18d2de5fbd6c9564640716a82c64fe0 Mon Sep 17 00:00:00 2001 From: Gautam Dey Date: Thu, 8 Aug 2019 23:03:56 -0400 Subject: [PATCH 2/2] Only run in cgo --- internal/debugger/create_recorder.go | 12 ++++++++++++ internal/debugger/create_recorder_cgo.go | 18 ++++++++++++++++++ internal/debugger/debugger.go | 7 ++----- internal/debugger/doc.go | 2 +- internal/debugger/record_test.go | 2 ++ internal/debugger/recorder/gpkg/gpkg.go | 4 ++++ 6 files changed, 39 insertions(+), 6 deletions(-) create mode 100644 internal/debugger/create_recorder.go create mode 100644 internal/debugger/create_recorder_cgo.go diff --git a/internal/debugger/create_recorder.go b/internal/debugger/create_recorder.go new file mode 100644 index 00000000..3ba749d5 --- /dev/null +++ b/internal/debugger/create_recorder.go @@ -0,0 +1,12 @@ +// +build !cgo + +package debugger + +import ( + "github.com/gdey/errors" + rcdr "github.com/go-spatial/geom/internal/debugger/recorder" +) + +func NewRecorder(_, _ string) (rcdr.Interface, string, error) { + return nil, "", errors.String("only supported in cgo") +} diff --git a/internal/debugger/create_recorder_cgo.go b/internal/debugger/create_recorder_cgo.go new file mode 100644 index 00000000..afffe864 --- /dev/null +++ b/internal/debugger/create_recorder_cgo.go @@ -0,0 +1,18 @@ +// +build cgo + +package debugger + +import ( + "fmt" + + rcdr "github.com/go-spatial/geom/internal/debugger/recorder" + "github.com/go-spatial/geom/internal/debugger/recorder/gpkg" +) + +func NewRecorder(dir, filename string) (rcdr.Interface, string, error) { + r, fn, err := gpkg.New(dir, filename, 0) + if err != nil { + return nil, fn, fmt.Errorf("gpkg error: %v", err) + } + return r, fn, nil +} diff --git a/internal/debugger/debugger.go b/internal/debugger/debugger.go index 7b1176de..eb17e6c9 100644 --- a/internal/debugger/debugger.go +++ b/internal/debugger/debugger.go @@ -10,7 +10,6 @@ import ( "sync" "unicode" - "github.com/go-spatial/geom/internal/debugger/recorder/gpkg" "github.com/pborman/uuid" ) @@ -113,10 +112,9 @@ func AugmentRecorder(rec Recorder, testFilename string) (Recorder, bool) { defer lck.Unlock() rcrd, ok := recrds[testFilename] if !ok { - rcd, filename, err := gpkg.New(dir, filename, 0) + rcd, filename, err := NewRecorder(dir, filename) if err != nil { - panic(fmt.Sprintf("Failed to created gpkg db: %v", err)) - os.Exit(1) + panic(err) } rcrd = struct { fn string @@ -126,7 +124,6 @@ func AugmentRecorder(rec Recorder, testFilename string) (Recorder, bool) { rcrd: &recorder{Interface: rcd}, } recrds[testFilename] = rcrd - } rcrd.rcrd.IncrementCount() log.Println("Writing debugger output to", rcrd.fn) diff --git a/internal/debugger/doc.go b/internal/debugger/doc.go index 0341ad91..403d669d 100644 --- a/internal/debugger/doc.go +++ b/internal/debugger/doc.go @@ -1,5 +1,5 @@ /* - Package debugger provides a way for us to capture partial +Package debugger provides a way for us to capture partial geometries during geometry processing. The geometries are stored in a `spatailite` database. diff --git a/internal/debugger/record_test.go b/internal/debugger/record_test.go index 0ac45110..8ecae746 100644 --- a/internal/debugger/record_test.go +++ b/internal/debugger/record_test.go @@ -1,3 +1,5 @@ +// +build cgo + package debugger_test import ( diff --git a/internal/debugger/recorder/gpkg/gpkg.go b/internal/debugger/recorder/gpkg/gpkg.go index 4e5bc3a8..b4cb45f1 100644 --- a/internal/debugger/recorder/gpkg/gpkg.go +++ b/internal/debugger/recorder/gpkg/gpkg.go @@ -1,3 +1,5 @@ +// +build cgo + package gpkg import ( @@ -48,6 +50,8 @@ type DB struct { statements map[gpkg.GeometryType]*sql.Stmt } +// New returns a new recorder, the file where the recrods are recorded to and +// any errors. func New(outputDir, filename string, srsid int32) (*DB, string, error) { dbFilename := filepath.Join(outputDir, filename+".gpkg")