Publish grouped warning and diagnostic files
This commit is contained in:
@@ -25,6 +25,11 @@ const chunkMapFileName = "chunk-map.json"
|
||||
|
||||
const evidenceContextFileName = "evidence-context.json"
|
||||
|
||||
const (
|
||||
warningsSchemaVersion = "notarius.warnings.v2"
|
||||
diagnosticsSchemaVersion = "notarius.diagnostics.v1"
|
||||
)
|
||||
|
||||
var safeOutputFileChar = regexp.MustCompile(`[^A-Za-z0-9._-]`)
|
||||
|
||||
var _ contracts.OutputEncoder = (*Encoder)(nil)
|
||||
@@ -231,6 +236,7 @@ type indexFile struct {
|
||||
OutputFiles []outputFileIndex `json:"output_files"`
|
||||
RejectedFile string `json:"rejected_file"`
|
||||
WarningsFile string `json:"warnings_file"`
|
||||
DiagnosticsFile string `json:"diagnostics_file"`
|
||||
ChunkMap *artifactIndex `json:"chunk_map,omitempty"`
|
||||
EvidenceContext *artifactIndex `json:"evidence_context,omitempty"`
|
||||
}
|
||||
@@ -259,7 +265,19 @@ type rejectedFile struct {
|
||||
}
|
||||
|
||||
type warningsFile struct {
|
||||
Groups []contracts.DiagnosticGroup `json:"groups"`
|
||||
SchemaVersion string `json:"schema_version"`
|
||||
GroupCount int `json:"group_count"`
|
||||
OccurrenceCount int `json:"occurrence_count"`
|
||||
Groups []contracts.DiagnosticGroup `json:"groups"`
|
||||
}
|
||||
|
||||
type diagnosticsFile struct {
|
||||
SchemaVersion string `json:"schema_version"`
|
||||
GroupCount int `json:"group_count"`
|
||||
OccurrenceCount int `json:"occurrence_count"`
|
||||
Truncated bool `json:"truncated"`
|
||||
UnrepresentedOccurrenceCount int `json:"unrepresented_occurrence_count"`
|
||||
Groups []contracts.DiagnosticGroup `json:"groups"`
|
||||
}
|
||||
|
||||
func logicalFiles(req contracts.OutputRequest, options Options) ([]contracts.OutputFile, error) {
|
||||
@@ -269,7 +287,7 @@ func logicalFiles(req contracts.OutputRequest, options Options) ([]contracts.Out
|
||||
})
|
||||
|
||||
outputIndexes := make([]outputFileIndex, 0, len(outputs))
|
||||
files := make([]contracts.OutputFile, 0, len(outputs)+5)
|
||||
files := make([]contracts.OutputFile, 0, len(outputs)+6)
|
||||
manifestFile, err := jsonFile("manifest.json", req.Manifest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -303,10 +321,11 @@ func logicalFiles(req contracts.OutputRequest, options Options) ([]contracts.Out
|
||||
}
|
||||
|
||||
index := indexFile{
|
||||
ManifestFile: "manifest.json",
|
||||
OutputFiles: outputIndexes,
|
||||
RejectedFile: "rejected.json",
|
||||
WarningsFile: "warnings.json",
|
||||
ManifestFile: "manifest.json",
|
||||
OutputFiles: outputIndexes,
|
||||
RejectedFile: "rejected.json",
|
||||
WarningsFile: "warnings.json",
|
||||
DiagnosticsFile: "diagnostics.json",
|
||||
}
|
||||
if options.IncludeChunkMap && req.ChunkMap != nil {
|
||||
chunkMapOutput, chunkMapDescriptor, err := serializedChunkMapFile(*req.ChunkMap)
|
||||
@@ -332,11 +351,15 @@ func logicalFiles(req contracts.OutputRequest, options Options) ([]contracts.Out
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
warningsOutput, err := jsonFile("warnings.json", warningsFile{Groups: warningGroups(req.Diagnostics)})
|
||||
warningsOutput, err := jsonFile("warnings.json", newWarningsFile(req.Diagnostics))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
files = append(files, indexOutput, rejectedOutput, warningsOutput)
|
||||
diagnosticsOutput, err := jsonFile("diagnostics.json", newDiagnosticsFile(req.Diagnostics))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
files = append(files, indexOutput, rejectedOutput, warningsOutput, diagnosticsOutput)
|
||||
sort.Slice(files, func(i, j int) bool {
|
||||
return files[i].Name < files[j].Name
|
||||
})
|
||||
@@ -487,16 +510,56 @@ func cloneRejected(rejected []contracts.RejectedOutput) []contracts.RejectedOutp
|
||||
return append([]contracts.RejectedOutput(nil), rejected...)
|
||||
}
|
||||
|
||||
func warningGroups(collection contracts.DiagnosticCollection) []contracts.DiagnosticGroup {
|
||||
func newWarningsFile(collection contracts.DiagnosticCollection) warningsFile {
|
||||
groups := diagnosticGroupsByDisposition(collection, contracts.DiagnosticDispositionWarning)
|
||||
return warningsFile{
|
||||
SchemaVersion: warningsSchemaVersion,
|
||||
GroupCount: len(groups),
|
||||
OccurrenceCount: diagnosticOccurrenceCount(groups),
|
||||
Groups: groups,
|
||||
}
|
||||
}
|
||||
|
||||
func newDiagnosticsFile(collection contracts.DiagnosticCollection) diagnosticsFile {
|
||||
groups := diagnosticGroupsExceptDisposition(collection, contracts.DiagnosticDispositionWarning)
|
||||
return diagnosticsFile{
|
||||
SchemaVersion: diagnosticsSchemaVersion,
|
||||
GroupCount: len(groups),
|
||||
OccurrenceCount: diagnosticOccurrenceCount(groups) + collection.UnrepresentedOccurrenceCount,
|
||||
Truncated: collection.Truncated,
|
||||
UnrepresentedOccurrenceCount: collection.UnrepresentedOccurrenceCount,
|
||||
Groups: groups,
|
||||
}
|
||||
}
|
||||
|
||||
func diagnosticGroupsByDisposition(collection contracts.DiagnosticCollection, disposition contracts.DiagnosticDisposition) []contracts.DiagnosticGroup {
|
||||
groups := make([]contracts.DiagnosticGroup, 0)
|
||||
for _, group := range collection.Groups {
|
||||
if group.Disposition == contracts.DiagnosticDispositionWarning {
|
||||
if group.Disposition == disposition {
|
||||
groups = append(groups, group)
|
||||
}
|
||||
}
|
||||
return contracts.CloneDiagnosticCollection(contracts.DiagnosticCollection{Groups: groups}).Groups
|
||||
}
|
||||
|
||||
func diagnosticGroupsExceptDisposition(collection contracts.DiagnosticCollection, disposition contracts.DiagnosticDisposition) []contracts.DiagnosticGroup {
|
||||
groups := make([]contracts.DiagnosticGroup, 0)
|
||||
for _, group := range collection.Groups {
|
||||
if group.Disposition != disposition {
|
||||
groups = append(groups, group)
|
||||
}
|
||||
}
|
||||
return contracts.CloneDiagnosticCollection(contracts.DiagnosticCollection{Groups: groups}).Groups
|
||||
}
|
||||
|
||||
func diagnosticOccurrenceCount(groups []contracts.DiagnosticGroup) int {
|
||||
count := 0
|
||||
for _, group := range groups {
|
||||
count += group.OccurrenceCount
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
func encoderErrorf(format string, args ...any) error {
|
||||
return fmt.Errorf("json output encoder: "+format, args...)
|
||||
}
|
||||
|
||||
@@ -155,6 +155,7 @@ func TestEncodeReturnsLogicalFilesForNormalizedOutputs(t *testing.T) {
|
||||
}
|
||||
|
||||
wantNames := []string{
|
||||
"diagnostics.json",
|
||||
"index.json",
|
||||
"lanes/notes_items.json",
|
||||
"lanes/spells.json",
|
||||
@@ -216,6 +217,49 @@ func TestEncodeIncludesRejectedAndWarningsWhenEmpty(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncodePublishesVersionedDiagnosticPartitions(t *testing.T) {
|
||||
diagnostics := contracts.DiagnosticCollection{
|
||||
Groups: []contracts.DiagnosticGroup{
|
||||
{
|
||||
Disposition: contracts.DiagnosticDispositionWarning,
|
||||
Category: contracts.DiagnosticCategoryDegradation,
|
||||
ReasonCode: "degraded",
|
||||
Origin: contracts.DiagnosticOrigin{Stage: contracts.DiagnosticOriginStageChunk},
|
||||
OccurrenceCount: 2,
|
||||
Samples: []contracts.DiagnosticSample{{Scope: "chunk", Message: "degraded"}},
|
||||
OmittedSampleCount: 1,
|
||||
},
|
||||
{
|
||||
Disposition: contracts.DiagnosticDispositionAdvisory,
|
||||
Category: contracts.DiagnosticCategoryDataQuality,
|
||||
ReasonCode: "unrelated",
|
||||
Origin: contracts.DiagnosticOrigin{Stage: contracts.DiagnosticOriginStageExtract},
|
||||
OccurrenceCount: 3,
|
||||
Samples: []contracts.DiagnosticSample{{Scope: "items[0]", Message: "unrelated"}},
|
||||
OmittedSampleCount: 2,
|
||||
},
|
||||
},
|
||||
Truncated: true,
|
||||
UnrepresentedOccurrenceCount: 5,
|
||||
}
|
||||
result, err := New().Encode(context.Background(), contracts.OutputRequest{Diagnostics: diagnostics})
|
||||
if err != nil {
|
||||
t.Fatalf("Encode() error = %v", err)
|
||||
}
|
||||
warnings := decodeObject(t, fileBytes(t, result.Files, "warnings.json"))
|
||||
if warnings["schema_version"] != warningsSchemaVersion || warnings["group_count"] != float64(1) || warnings["occurrence_count"] != float64(2) || len(warnings["groups"].([]any)) != 1 {
|
||||
t.Fatalf("warnings file = %#v", warnings)
|
||||
}
|
||||
publishedDiagnostics := decodeObject(t, fileBytes(t, result.Files, "diagnostics.json"))
|
||||
if publishedDiagnostics["schema_version"] != diagnosticsSchemaVersion || publishedDiagnostics["group_count"] != float64(1) || publishedDiagnostics["occurrence_count"] != float64(8) || publishedDiagnostics["truncated"] != true || publishedDiagnostics["unrepresented_occurrence_count"] != float64(5) || len(publishedDiagnostics["groups"].([]any)) != 1 {
|
||||
t.Fatalf("diagnostics file = %#v", publishedDiagnostics)
|
||||
}
|
||||
index := decodeObject(t, fileBytes(t, result.Files, "index.json"))
|
||||
if index["warnings_file"] != "warnings.json" || index["diagnostics_file"] != "diagnostics.json" {
|
||||
t.Fatalf("index = %#v", index)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncodeChunkMapExportIsOptIn(t *testing.T) {
|
||||
artifact := acceptedChunkMapArtifact(t)
|
||||
request := contracts.OutputRequest{
|
||||
@@ -234,7 +278,7 @@ func TestEncodeChunkMapExportIsOptIn(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("Encode() error = %v, want nil", err)
|
||||
}
|
||||
if got, want := outputFileNames(result.Files), []string{"index.json", "manifest.json", "rejected.json", "warnings.json"}; !reflect.DeepEqual(got, want) {
|
||||
if got, want := outputFileNames(result.Files), []string{"diagnostics.json", "index.json", "manifest.json", "rejected.json", "warnings.json"}; !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("file names = %#v, want %#v", got, want)
|
||||
}
|
||||
index := decodeObject(t, fileBytes(t, result.Files, "index.json"))
|
||||
@@ -257,7 +301,7 @@ func TestEncodeIncludesValidatedChunkMap(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("Encode() error = %v, want nil", err)
|
||||
}
|
||||
if got, want := outputFileNames(result.Files), []string{"chunk-map.json", "index.json", "lanes/spells.json", "manifest.json", "rejected.json", "warnings.json"}; !reflect.DeepEqual(got, want) {
|
||||
if got, want := outputFileNames(result.Files), []string{"chunk-map.json", "diagnostics.json", "index.json", "lanes/spells.json", "manifest.json", "rejected.json", "warnings.json"}; !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("file names = %#v, want %#v", got, want)
|
||||
}
|
||||
chunkMapFile := fileBytes(t, result.Files, chunkMapFileName)
|
||||
|
||||
Reference in New Issue
Block a user