@@ -17,13 +17,57 @@ import (
1717 "github.com/google/pprof/profile"
1818 "github.com/klauspost/compress/zstd"
1919 "github.com/pierrec/lz4/v4"
20+ "github.com/xeipuuv/gojsonschema"
2021)
2122
2223var (
2324 _ json.Unmarshaler = (* Optional [int64 ])(nil )
2425 _ json.Marshaler = (* Optional [int64 ])(nil )
2526)
2627
28+ // JSON Schema for validating expected profile JSON files.
29+ // Basic structure validation, complex rules validated in Go code.
30+ var expectedProfileSchema = `{
31+ "$schema": "https://json-schema.org/draft-07/schema#",
32+ "type": "object",
33+ "required": ["stacks"],
34+ "properties": {
35+ "test_name": { "type": "string" },
36+ "note": { "type": "string" },
37+ "scale_by_duration": { "type": "boolean" },
38+ "pprof-regex": { "type": "string" },
39+ "allow_first_profile_failure": { "type": "boolean" },
40+ "stacks": {
41+ "type": "array",
42+ "items": {
43+ "type": "object",
44+ "required": ["profile-type", "stack-content"],
45+ "properties": {
46+ "profile-type": { "type": "string", "minLength": 1 },
47+ "pprof-regex": { "type": "string" },
48+ "stack-content": {
49+ "type": "array",
50+ "minItems": 1,
51+ "items": {
52+ "type": "object",
53+ "required": ["regular_expression"],
54+ "properties": {
55+ "regular_expression": { "type": "string", "minLength": 1 },
56+ "value": { "type": "integer" },
57+ "percent": { "type": "integer" },
58+ "error_margin": { "type": "integer" },
59+ "labels": { "type": "array" }
60+ }
61+ }
62+ },
63+ "error-margin": { "type": "integer" },
64+ "value-matching-sum": { "type": "integer" }
65+ }
66+ }
67+ }
68+ }
69+ }`
70+
2771type Optional [T any ] struct {
2872 value * T
2973}
@@ -94,12 +138,37 @@ type TypedStacks struct {
94138
95139type StackTestData struct {
96140 TestName string `json:"test_name"`
141+ Note string `json:"note,omitempty"`
97142 ScaleByDuration bool `json:"scale_by_duration"`
98143 PprofRegex string `json:"pprof-regex"`
99144 AllowFirstProfileFailure bool `json:"allow_first_profile_failure,omitempty"`
100145 Stacks []TypedStacks `json:"stacks"`
101146}
102147
148+ // Validate rules that JSON Schema can't express
149+ func (s * StackTestData ) Validate () error {
150+ // Stacks must be non-empty unless note is present
151+ if len (s .Stacks ) == 0 && s .Note == "" {
152+ return fmt .Errorf ("'stacks' must have at least one entry (or provide a 'note' explaining why it's empty)" )
153+ }
154+
155+ // If no value-matching-sum, require value or percent in stack-content
156+ for i , stack := range s .Stacks {
157+ if _ , hasValueMatchingSum := stack .ValueMatchingSum .Value (); hasValueMatchingSum {
158+ continue
159+ }
160+ for j , content := range stack .StackContent {
161+ _ , hasValue := content .Value .Value ()
162+ _ , hasPercent := content .Percent .Value ()
163+ if ! hasValue && ! hasPercent {
164+ return fmt .Errorf ("stacks[%d].stack-content[%d]: must have 'value' or 'percent' (or parent must have 'value-matching-sum')" , i , j )
165+ }
166+ }
167+ }
168+
169+ return nil
170+ }
171+
103172// Custom unmarshaller for Labels to ensure exactly one of Values and ValueRegex is defined
104173func (l * Labels ) UnmarshalJSON (data []byte ) error {
105174 type labels Labels
@@ -442,21 +511,41 @@ func writeToJSONFile(data StackTestData, filePath string) error {
442511
443512func readJSONFile (filePath string ) (StackTestData , error ) {
444513 var data StackTestData
445- jsonFile , err := os .Open (filePath )
514+ byteValue , err := os .ReadFile (filePath )
446515 if err != nil {
447516 return data , err
448517 }
449- defer jsonFile .Close ()
450- byteValue , err := io .ReadAll (jsonFile )
518+
519+ // Step 1: Validate JSON syntax
520+ if ! json .Valid (byteValue ) {
521+ return data , fmt .Errorf ("invalid JSON syntax in %s" , filePath )
522+ }
523+
524+ // Step 2: Validate against schema
525+ schemaLoader := gojsonschema .NewStringLoader (expectedProfileSchema )
526+ documentLoader := gojsonschema .NewBytesLoader (byteValue )
527+ result , err := gojsonschema .Validate (schemaLoader , documentLoader )
451528 if err != nil {
452- return data , err
529+ return data , fmt . Errorf ( "schema validation error for %s: %v" , filePath , err )
453530 }
454- if ! json .Valid (byteValue ) {
455- return data , fmt .Errorf ("Invalid json data %s" , filePath )
531+ if ! result .Valid () {
532+ var errs []string
533+ for _ , desc := range result .Errors () {
534+ errs = append (errs , desc .String ())
535+ }
536+ return data , fmt .Errorf ("JSON schema validation failed for %s:\n - %s" , filePath , strings .Join (errs , "\n - " ))
456537 }
538+
539+ // Step 3: Unmarshal validated JSON
457540 if err := json .Unmarshal (byteValue , & data ); err != nil {
458541 return data , err
459542 }
543+
544+ // Step 4: Validate rules
545+ if err := data .Validate (); err != nil {
546+ return data , fmt .Errorf ("validation failed for %s: %v" , filePath , err )
547+ }
548+
460549 return data , nil
461550}
462551
0 commit comments