github.com/qri-io/qri@v0.10.1-0.20220104210721-c771715036cb/lib/diff_test.go (about) 1 package lib 2 3 import ( 4 "testing" 5 6 "github.com/google/go-cmp/cmp" 7 "github.com/qri-io/qri/dsref" 8 ) 9 10 func TestDatasetMethodsDiff(t *testing.T) { 11 tr := newTestRunner(t) 12 defer tr.Delete() 13 14 req := tr.Instance.Diff() 15 ds := tr.Instance.Dataset() 16 jobsOnePath := tr.MustWriteTmpFile(t, "jobs_by_automation_1.csv", jobsByAutomationData1) 17 jobsTwoPath := tr.MustWriteTmpFile(t, "jobs_by_automation_2.csv", jobsByAutomationData2) 18 19 djsOnePath := tr.MustWriteTmpFile(t, "djs_1.json", `{ "dj dj booth": { "rating": 1, "uses_soundcloud": true } }`) 20 djsTwoPath := tr.MustWriteTmpFile(t, "djs_2.json", `{ "DJ dj booth": { "rating": 1, "uses_soundcloud": true } }`) 21 22 initParams := &SaveParams{ 23 Ref: "me/jobs_ranked_by_automation_prob", 24 BodyPath: jobsOnePath, 25 } 26 ds1, err := ds.Save(tr.Ctx, initParams) 27 if err != nil { 28 t.Fatalf("couldn't save: %s", err.Error()) 29 } 30 31 initParams = &SaveParams{ 32 Ref: "me/jobs_ranked_by_automation_prob", 33 BodyPath: jobsTwoPath, 34 } 35 ds2, err := ds.Save(tr.Ctx, initParams) 36 if err != nil { 37 t.Fatalf("couldn't save second revision: %s", err.Error()) 38 } 39 40 dsRef1 := dsref.ConvertDatasetToVersionInfo(ds1).SimpleRef() 41 dsRef2 := dsref.ConvertDatasetToVersionInfo(ds2).SimpleRef() 42 43 good := []struct { 44 description string 45 Left, Right string 46 Selector string 47 Stat *DiffStat 48 DeltaLen int 49 }{ 50 {"two fully qualified references", 51 dsRef1.String(), dsRef2.String(), 52 "", 53 &DiffStat{Left: 209, Right: 209, LeftWeight: 5017, RightWeight: 5017, Inserts: 0, Updates: 0, Deletes: 0}, 54 8, 55 }, 56 {"fill left path from history", 57 dsRef2.Alias(), dsRef2.Alias(), 58 "", 59 &DiffStat{Left: 205, Right: 209, LeftWeight: 4920, RightWeight: 5017, Inserts: 19, Updates: 0, Deletes: 13}, 60 10, 61 }, 62 {"two local file paths", 63 "testdata/jobs_by_automation/body.csv", "testdata/jobs_by_automation_2/body.csv", 64 "", 65 &DiffStat{Left: 151, Right: 151, LeftWeight: 3757, RightWeight: 3757, Inserts: 1, Updates: 0, Deletes: 1}, 66 30, 67 }, 68 {"diff local csv & json file", 69 "testdata/now_tf/input.dataset.json", "testdata/jobs_by_automation/body.csv", 70 "", 71 &DiffStat{Left: 10, Right: 151, LeftWeight: 162, RightWeight: 3757, Inserts: 1, Updates: 0, Deletes: 1}, 72 2, 73 }, 74 {"case-sensitive key change", 75 djsOnePath, djsTwoPath, 76 "", 77 &DiffStat{Left: 4, Right: 4, LeftWeight: 18, RightWeight: 18, Inserts: 1, Updates: 0, Deletes: 1}, 78 2, 79 }, 80 } 81 82 // execute 83 for i, c := range good { 84 t.Run(c.description, func(t *testing.T) { 85 p := &DiffParams{ 86 LeftSide: c.Left, 87 RightSide: c.Right, 88 Selector: c.Selector, 89 } 90 // If test has same two paths, we want the previous version compared to head 91 if p.LeftSide == p.RightSide { 92 p.UseLeftPrevVersion = true 93 p.RightSide = "" 94 } 95 res, err := req.Diff(tr.Ctx, p) 96 if err != nil { 97 t.Errorf("%d: \"%s\" error: %s", i, c.description, err.Error()) 98 return 99 } 100 101 if diff := cmp.Diff(c.Stat, res.Stat); diff != "" { 102 t.Errorf("result mismatch (-want +got):\n%s", diff) 103 } 104 105 if len(res.Diff) != c.DeltaLen { 106 t.Errorf("%d: \"%s\" delta length mismatch. want: %d got: %d", i, c.description, c.DeltaLen, len(res.Diff)) 107 } 108 }) 109 } 110 } 111 112 const jobsByAutomationData1 = ` 113 rank,probability_of_automation,soc_code,job_title 114 702,"0.99","41-9041","Telemarketers" 115 701,"0.99","23-2093","Title Examiners, Abstractors, and Searchers" 116 700,"0.99","51-6051","Sewers, Hand" 117 699,"0.99","15-2091","Mathematical Technicians" 118 698,"0.99","13-2053","Insurance Underwriters" 119 697,"0.99","49-9064","Watch Repairers" 120 696,"0.99","43-5011","Cargo and Freight Agents" 121 695,"0.99","13-2082","Tax Preparers" 122 694,"0.99","51-9151","Photographic Process Workers and Processing Machine Operators" 123 693,"0.99","43-4141","New Accounts Clerks" 124 692,"0.99","25-4031","Library Technicians" 125 691,"0.99","43-9021","Data Entry Keyers" 126 690,"0.98","51-2093","Timing Device Assemblers and Adjusters" 127 689,"0.98","43-9041","Insurance Claims and Policy Processing Clerks" 128 688,"0.98","43-4011","Brokerage Clerks" 129 687,"0.98","43-4151","Order Clerks" 130 686,"0.98","13-2072","Loan Officers" 131 685,"0.98","13-1032","Insurance Appraisers, Auto Damage" 132 684,"0.98","27-2023","Umpires, Referees, and Other Sports Officials" 133 683,"0.98","43-3071","Tellers" 134 682,"0.98","51-9194","Etchers and Engravers" 135 681,"0.98","51-9111","Packaging and Filling Machine Operators and Tenders" 136 680,"0.98","43-3061","Procurement Clerks" 137 679,"0.98","43-5071","Shipping, Receiving, and Traffic Clerks" 138 678,"0.98","51-4035","Milling and Planing Machine Setters, Operators, and Tenders, Metal and Plastic" 139 677,"0.98","13-2041","Credit Analysts" 140 676,"0.98","41-2022","Parts Salespersons" 141 675,"0.98","13-1031","Claims Adjusters, Examiners, and Investigators" 142 674,"0.98","53-3031","Driver/Sales Workers" 143 673,"0.98","27-4013","Radio Operators" 144 ` 145 146 const jobsByAutomationData2 = ` 147 rank,probability_of_automation,industry_code,job_name 148 702,"0.99","41-9041","Telemarketers" 149 701,"0.99","23-2093","Title Examiners, Abstractors, and Searchers" 150 700,"0.99","51-6051","Sewers, Hand" 151 699,"0.99","15-2091","Mathematical Technicians" 152 698,"0.88","13-2053","Insurance Underwriters" 153 697,"0.99","49-9064","Watch Repairers" 154 696,"0.99","43-5011","Cargo and Freight Agents" 155 695,"0.99","13-2082","Tax Preparers" 156 694,"0.99","51-9151","Photographic Process Workers and Processing Machine Operators" 157 693,"0.99","43-4141","New Accounts Clerks" 158 692,"0.99","25-4031","Library Technicians" 159 691,"0.99","43-9021","Data Entry Keyers" 160 690,"0.98","51-2093","Timing Device Assemblers and Adjusters" 161 689,"0.98","43-9041","Insurance Claims and Policy Processing Clerks" 162 688,"0.98","43-4011","Brokerage Clerks" 163 687,"0.98","43-4151","Order Clerks" 164 686,"0.98","13-2072","Loan Officers" 165 685,"0.98","13-1032","Insurance Appraisers, Auto Damage" 166 684,"0.98","27-2023","Umpires, Referees, and Other Sports Officials" 167 683,"0.98","43-3071","Tellers" 168 682,"0.98","51-9194","Etchers and Engravers" 169 681,"0.98","51-9111","Packaging and Filling Machine Operators and Tenders" 170 680,"0.98","43-3061","Procurement Clerks" 171 679,"0.98","43-5071","Shipping, Receiving, and Traffic Clerks" 172 678,"0.98","51-4035","Milling and Planing Machine Setters, Operators, and Tenders, Metal and Plastic" 173 677,"0.98","13-2041","Credit Analysts" 174 676,"0.98","41-2022","Parts Salespersons" 175 675,"0.98","13-1031","Claims Adjusters, Examiners, and Investigators" 176 674,"0.98","53-3031","Driver/Sales Workers" 177 673,"0.98","27-4013","Radio Operators" 178 ` 179 180 // Test that we can compare bodies of different dataset revisions. 181 func TestDiffPrevRevision(t *testing.T) { 182 run := newTestRunner(t) 183 defer run.Delete() 184 185 // Save three versions, then diff the last head against its previous version 186 run.MustSaveFromBody(t, "test_cities", "testdata/cities_2/body.csv") 187 run.MustSaveFromBody(t, "test_cities", "testdata/cities_2/body_more.csv") 188 run.MustSaveFromBody(t, "test_cities", "testdata/cities_2/body_even_more.csv") 189 190 output, err := run.Diff("me/test_cities", "", "body") 191 if err != nil { 192 t.Fatal(err) 193 } 194 195 // TODO(dustmop): Come up with a better way to represent this diff, that still looks nice when 196 // compared with cmp.Diff. 197 expect := `{"stat":{"leftNodes":36,"rightNodes":46,"leftWeight":510,"rightWeight":637,"inserts":4,"deletes":2},"diff":[[" ",0,["toronto",50000000,55.5,false]],[" ",1,["new york",8500000,44.4,true]],[" ",2,["los angeles",3990000,42.7,true]],["-",3,["chicago",300000,44.4,true]],["+",3,["dallas",1340000,30,true]],[" ",4,["chatham",35000,65.25,true]],[" ",5,null,[[" ",0,"mexico city"],["-",1,70000000],["+",1,80000000],[" ",2,28.6],[" ",3,false]]],[" ",6,["raleigh",250000,50.65,true]],["+",7,["paris",2100000,41.1,false]],["+",8,["london",8900000,36.5,false]]]}` 198 199 if diff := cmp.Diff(expect, output); diff != "" { 200 t.Errorf("output mismatch (-want +got):\n%s", diff) 201 } 202 } 203 204 // Test that we can compare two different datasets 205 func TestDiff(t *testing.T) { 206 run := newTestRunner(t) 207 defer run.Delete() 208 209 // Save a dataset with one version 210 run.MustSaveFromBody(t, "test_cities", "testdata/cities_2/body.csv") 211 // Save a different dataset with one version 212 run.MustSaveFromBody(t, "test_more", "testdata/cities_2/body_more.csv") 213 214 // Diff the heads 215 output, err := run.Diff("me/test_cities", "me/test_more", "") 216 if err != nil { 217 t.Fatal(err) 218 } 219 220 // TODO(dustmop): Come up with a better way to represent this diff, that still looks nice when 221 // compared with cmp.Diff. 222 // TODO(dustmop): Would be better if Diff only returned the changes, instead of things that 223 // stay the same, since the delta in this case is pretty small. 224 expect := `{"stat":{"leftNodes":103,"rightNodes":113,"leftWeight":3918,"rightWeight":4274,"inserts":52,"deletes":26},"diff":[["-","bodyPath","/mem/Qmc7AoCfFVW5xe8qhyjNYewSgBHFubp6yLM3mfBzQp7iTr"],["+","bodyPath","/mem/QmYuVj1JvALB9Au5DNcVxGLMcWCDBUfbKCN3QbpvissSC4"],[" ","commit",null,[[" ","author",{"id":"QmeL2mdVka1eahKENjehK6tBxkkpk5dNQ1qMcgWi7Hrb4B"}],["-","message","created dataset from body.csv"],["+","message","created dataset from body_more.csv"],["-","path","/mem/QmTKLXhW5CHD9a8o4UwWo6EJ4X7Y2h6h2jsDRKw5tU54jS"],["+","path","/mem/QmQRf5y4L6Hn37a1bf1nkWYDh9wUeUcs6XKoi5v2v2zKjV"],[" ","qri","cm:0"],["-","signature","fYfAWIzpiArVi5g+Ls9dA2KLdcSqbqqBQTF/QVFAVA4IZ01T3gJVwDot6scb7twaM6tX2uihynMz6n1xsZi/x7TZFg7VZlACV/RRG93iyW2OcaKlvXgzqgW4HpNvEdaUvN5l59nRtf10lfoL8jN1SVKI6vlhtZtT0ETLliNA83JEfBCwrWk0ftf/lKFBcbLUv0sI0x8km2gV7fTvAHOeTjiCO5Ya+2ID9AI8TN8AMEAltRkyEQMDankrHg8wEnOwadipZM2/qn0wsZae7b/n2T1JNJPs78tVjC95/7AW+JEqrlhtGpwqX2d5t4vbSMvC1vB/gf8nRwh9PVIaz9ZoTg=="],["+","signature","ibX3sNj08nbcKES2aGXkVk+ZMKgmZEdtnB8Q5be4v4y9DhRrRWUbR2R3XwOTs0vfv3wMcsHqp9jY/v7k/HNnNsCqg0PBiYGdXkvEWr2sjqJbG6A+0x5Bkqzkxf2FhrUqzqx4wMvJYZsgwSf7UDWNh/A/1wybt8jR7P2tT7IWLKbwaWJWhG5nk2NvlkL3nQIVLBApJaYCQhjolAp0n2q82jgD9x1dzs2qgV3oSyVqBdWCCg9GY/q9QuqmQHM/wyxJajXo8gGV0LDgPh39sZfqRqQjJ8koszCTiYl1b9np41GBFkKBn0KRIvSGJrE62NS87YkIi4wOib66XjoY8jaA9w=="],["-","timestamp","2001-01-01T01:01:01.000000001Z"],["+","timestamp","2001-01-01T01:02:01.000000001Z"],["-","title","created dataset from body.csv"],["+","title","created dataset from body_more.csv"]]],["-","id","xtxdndmgp56itafwfssw6nqrgy5fopj77loggh5vrr3cvok7yueq"],["+","id","gr3rhewayftulvahrh4kkahthz55jh545nxgh2lxikqi3psi3gla"],["-","path","/mem/QmbTMgT154t4NnP4H2FXBPcec7D85HrJKnBmZKWvRsYCtS"],["+","path","/mem/QmXQ6L58wzoG57QkLGp6iQtd24PGbnnRk9kPvaE1QPXzcq"],[" ","qri","ds:0"],[" ","stats",null,[["-","path","/mem/QmVvv9vBHLsYbDYzG1G931Pi58gTPdVE4hcUsxb6rAU8S9"],["+","path","/mem/QmcLRdnni9jpvhACHN9LmUsDuT2iiqpEhQ1ZqxdLK5ELr6"],[" ","qri","sa:0"],[" ","stats",null,[[" ",0,null,[["-","count",5],["+","count",7],[" ","frequencies",null,[[" ","chatham",1],[" ","chicago",1],["+","los angeles",1],["+","mexico city",1],[" ","new york",1],[" ","raleigh",1],[" ","toronto",1]]],["-","maxLength",8],["+","maxLength",11],[" ","minLength",7],[" ","type","string"],["-","unique",5],["+","unique",7]]],[" ",1,null,[["-","count",5],["+","count",7],[" ","histogram",null,[[" ","bins",null,[["+",0,35000],[" ",1,250000],["+",2,300000],["+",3,3990000],[" ",4,8500000],["+",5,50000000],["+",6,70000000],["+",7,70000001]]],[" ","frequencies",null,[["+",0,1],["+",1,1],["+",2,1],["+",3,1],["+",4,1],["+",5,1],["+",6,1]]]]],["-","max",50000000],["+","max",70000000],["-","mean",11817000],["+","mean",19010714.285714287],["-","median",300000],["+","median",3990000],[" ","min",35000],[" ","type","numeric"]]],[" ",2,null,[["-","count",5],["+","count",7],[" ","histogram",null,[[" ","bins",null,[["+",0,28.6],["+",1,42.7],["+",2,44.4],["+",3,50.65],[" ",4,55.5],["+",5,65.25],[" ",6,66.25]]],[" ","frequencies",null,[["+",0,1],["+",1,1],["+",2,2],["+",3,1],["+",4,1],["+",5,1]]]]],[" ","max",65.25],["-","mean",52.04],["+","mean",47.357142857142854],[" ","median",50.65],["-","min",44.4],["+","min",28.6],[" ","type","numeric"]]],[" ",3,null,[["-","count",5],["+","count",7],["-","falseCount",1],["+","falseCount",2],["-","trueCount",4],["+","trueCount",5],[" ","type","boolean"]]]]]]],[" ","structure",null,[["-","checksum","/mem/Qmc7AoCfFVW5xe8qhyjNYewSgBHFubp6yLM3mfBzQp7iTr"],["+","checksum","/mem/QmYuVj1JvALB9Au5DNcVxGLMcWCDBUfbKCN3QbpvissSC4"],[" ","depth",2],["-","entries",5],["+","entries",7],[" ","format","csv"],[" ","formatConfig",{"headerRow":true,"lazyQuotes":true}],["-","length",155],["+","length",217],["-","path","/mem/QmX3HjmvFGYXavQiPqpJvAZZ14J1DNPjCCGEzEy9NgZq2J"],["+","path","/mem/QmNpWHSFo8xwNQyamsaYAqUKu7Nzd3J1a4MukyNVf4J1xt"],[" ","qri","st:0"],[" ","schema",{"items":{"items":[{"title":"city","type":"string"},{"title":"pop","type":"integer"},{"title":"avg_age","type":"number"},{"title":"in_usa","type":"boolean"}],"type":"array"},"type":"array"}]]]]}` 225 if diff := cmp.Diff(expect, output); diff != "" { 226 t.Errorf("output mismatch (-want +got):\n%s", diff) 227 } 228 229 // Diff the bodies 230 output, err = run.Diff("me/test_cities", "me/test_more", "body") 231 if err != nil { 232 t.Fatal(err) 233 } 234 235 expect = `{"stat":{"leftNodes":26,"rightNodes":36,"leftWeight":344,"rightWeight":510,"inserts":2},"diff":[[" ",0,["toronto",50000000,55.5,false]],[" ",1,["new york",8500000,44.4,true]],["+",2,["los angeles",3990000,42.7,true]],[" ",3,["chicago",300000,44.4,true]],[" ",4,["chatham",35000,65.25,true]],["+",5,["mexico city",70000000,28.6,false]],[" ",6,["raleigh",250000,50.65,true]]]}` 236 if diff := cmp.Diff(expect, output); diff != "" { 237 t.Errorf("output mismatch (-want +got):\n%s", diff) 238 } 239 } 240 241 // Test that diffing a dataset with only one version produces an error 242 func TestDiffOnlyOneRevision(t *testing.T) { 243 run := newTestRunner(t) 244 defer run.Delete() 245 246 run.MustSaveFromBody(t, "test_cities", "testdata/cities_2/body.csv") 247 _, err := run.Diff("me/test_cities", "", "body") 248 if err == nil { 249 t.Fatal("expected error, did not get one") 250 } 251 expect := `dataset has only one version, nothing to diff against` 252 if err.Error() != expect { 253 t.Errorf("expected error: %q, got: %q", expect, err) 254 } 255 } 256 257 // Test that we can compare csv files 258 func TestDiffLocalCsvFiles(t *testing.T) { 259 run := newTestRunner(t) 260 defer run.Delete() 261 262 output, err := run.Diff("testdata/cities_2/body.csv", "testdata/cities_2/body_more.csv", "") 263 if err != nil { 264 t.Fatal(err) 265 } 266 expect := `{"stat":{"leftNodes":26,"rightNodes":36,"leftWeight":344,"rightWeight":510,"inserts":2},"schemaStat":{"leftNodes":5,"rightNodes":5,"leftWeight":41,"rightWeight":41},"schema":[[" ",0,"city"],[" ",1,"pop"],[" ",2,"avg_age"],[" ",3,"in_usa"]],"diff":[[" ",0,["toronto",50000000,55.5,false]],[" ",1,["new york",8500000,44.4,true]],["+",2,["los angeles",3990000,42.7,true]],[" ",3,["chicago",300000,44.4,true]],[" ",4,["chatham",35000,65.25,true]],["+",5,["mexico city",70000000,28.6,false]],[" ",6,["raleigh",250000,50.65,true]]]}` 267 if diff := cmp.Diff(expect, output); diff != "" { 268 t.Errorf("output mismatch (-want +got):\n%s", diff) 269 } 270 } 271 272 // Test that we can compare json files 273 func TestDiffLocalJsonFiles(t *testing.T) { 274 run := newTestRunner(t) 275 defer run.Delete() 276 277 output, err := run.Diff("../cmd/testdata/movies/body_two.json", "../cmd/testdata/movies/body_four.json", "") 278 if err != nil { 279 t.Fatal(err) 280 } 281 expect := `{"stat":{"leftNodes":7,"rightNodes":13,"leftWeight":161,"rightWeight":267,"inserts":2},"schemaStat":{"leftNodes":2,"rightNodes":2,"leftWeight":11,"rightWeight":11},"schema":[[" ","type","array"]],"diff":[[" ",0,["Avatar",178]],[" ",1,["Pirates of the Caribbean: At World's End",169]],["+",2,["Spectre",148]],["+",3,["The Dark Knight Rises",164]]]}` 282 if diff := cmp.Diff(expect, output); diff != "" { 283 t.Errorf("output mismatch (-want +got):\n%s", diff) 284 } 285 } 286 287 func TestDiffErrors(t *testing.T) { 288 run := newTestRunner(t) 289 defer run.Delete() 290 291 // Save a dataset with one version 292 run.MustSaveFromBody(t, "test_cities", "testdata/cities_2/body.csv") 293 294 // Save a different dataset with one version 295 run.MustSaveFromBody(t, "test_more", "testdata/cities_2/body_more.csv") 296 297 // Error to compare a dataset ref to a file 298 _, err := run.Diff("me/test_cities", "testdata/cities_2/body_even_more.csv", "") 299 expectErr := `cannot compare a file to dataset, must compare similar things` 300 if diff := cmp.Diff(expectErr, errorMessage(err)); diff != "" { 301 t.Errorf("output mismatch (-want +got):\n%s", diff) 302 } 303 304 // Error to only set left-side 305 _, err = run.DiffWithParams(&DiffParams{ 306 LeftSide: "me/test_cities", 307 }) 308 expectErr = `invalid parameters to diff` 309 if diff := cmp.Diff(expectErr, errorMessage(err)); diff != "" { 310 t.Errorf("output mismatch (-want +got):\n%s", diff) 311 } 312 313 // Error to set left-side with both WorkingDir and UseLeftPrevVersion 314 _, err = run.DiffWithParams(&DiffParams{ 315 LeftSide: "me/test_cities", 316 WorkingDir: "workdir", 317 UseLeftPrevVersion: true, 318 }) 319 expectErr = `cannot use both previous version and working directory` 320 if diff := cmp.Diff(expectErr, errorMessage(err)); diff != "" { 321 t.Errorf("output mismatch (-want +got):\n%s", diff) 322 } 323 324 // Error to set left-side and right-side with WorkingDir 325 _, err = run.DiffWithParams(&DiffParams{ 326 LeftSide: "me/test_cities", 327 RightSide: "me/test_more", 328 WorkingDir: "workdir", 329 }) 330 expectErr = `cannot use working directory when comparing two sources` 331 if diff := cmp.Diff(expectErr, errorMessage(err)); diff != "" { 332 t.Errorf("output mismatch (-want +got):\n%s", diff) 333 } 334 335 // Error to set left-side and right-side with UseLeftPrevVersion 336 _, err = run.DiffWithParams(&DiffParams{ 337 LeftSide: "me/test_cities", 338 RightSide: "me/test_more", 339 UseLeftPrevVersion: true, 340 }) 341 expectErr = `cannot use previous version when comparing two sources` 342 if diff := cmp.Diff(expectErr, errorMessage(err)); diff != "" { 343 t.Errorf("output mismatch (-want +got):\n%s", diff) 344 } 345 346 // Error to use a selector for a field that doesn't exist 347 _, err = run.Diff("me/test_cities", "me/test_more", "meta") 348 expectErr = `component "meta" not found` 349 if diff := cmp.Diff(expectErr, errorMessage(err)); diff != "" { 350 t.Errorf("output mismatch (-want +got):\n%s", diff) 351 } 352 } 353 354 // TODO(dustmop): Test comparing a dataset in FSI, with a modification in the working directory 355 // TODO(dustmop): Test comparing a dataset in FSI, using selector 356 357 func errorMessage(err error) string { 358 if err == nil { 359 return "" 360 } 361 return err.Error() 362 }