302 lines
8.5 KiB
Go
302 lines
8.5 KiB
Go
package reporttemplate
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestTemplateLookup(t *testing.T) {
|
|
source, err := Template("hourly")
|
|
if err != nil {
|
|
t.Fatalf("Template() error = %v", err)
|
|
}
|
|
for _, want := range []string{"# {{ .Report.Title }}", "## Summary", "## Hourly Forecast", "## Weather Story"} {
|
|
if !strings.Contains(source, want) {
|
|
t.Fatalf("template missing %q:\n%s", want, source)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSchemaLookup(t *testing.T) {
|
|
data, err := Schema("hourly")
|
|
if err != nil {
|
|
t.Fatalf("Schema() error = %v", err)
|
|
}
|
|
var schema struct {
|
|
Type string `json:"type"`
|
|
AdditionalProperties bool `json:"additionalProperties"`
|
|
Required []string `json:"required"`
|
|
Properties map[string]any `json:"properties"`
|
|
}
|
|
if err := json.Unmarshal(data, &schema); err != nil {
|
|
t.Fatalf("schema is invalid JSON: %v", err)
|
|
}
|
|
if schema.Type != "object" {
|
|
t.Fatalf("schema type = %q, want object", schema.Type)
|
|
}
|
|
if schema.AdditionalProperties {
|
|
t.Fatal("additionalProperties = true, want false")
|
|
}
|
|
if strings.Join(schema.Required, ",") != "summary,timing,impacts" {
|
|
t.Fatalf("required = %#v, want summary/timing/impacts", schema.Required)
|
|
}
|
|
for _, field := range []string{"summary", "timing", "impacts", "confidence"} {
|
|
property, ok := schema.Properties[field].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("schema property %q missing or invalid", field)
|
|
}
|
|
if property["type"] != "string" {
|
|
t.Fatalf("schema property %q type = %v, want string", field, property["type"])
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRenderHourly(t *testing.T) {
|
|
rendered, err := Render("hourly", testRenderContext{
|
|
Report: testReportContext{
|
|
Title: "Hourly Report",
|
|
LocationName: "Brentwood",
|
|
ValidPeriodLabel: "May 29, 8:30 AM to 2:30 PM",
|
|
GeneratedAtLabel: "May 29, 8:30 AM",
|
|
},
|
|
GeneratedText: testGeneratedText{
|
|
Summary: "Storm chances increase through late morning.",
|
|
Timing: "The main window is 10 AM to noon.",
|
|
Impacts: "Brief downpours may slow travel.",
|
|
Confidence: "Medium confidence in timing.",
|
|
},
|
|
Modules: testModules{
|
|
CurrentConditions: &testCurrentConditions{
|
|
ConditionText: "Partly cloudy",
|
|
TemperatureF: floatPtr(74),
|
|
ApparentTemperatureF: floatPtr(76),
|
|
RelativeHumidityPercent: floatPtr(71),
|
|
WindDirection: "S",
|
|
WindSpeedMph: floatPtr(8),
|
|
},
|
|
HourlyForecast: &testHourlyForecast{
|
|
Periods: []testHourlyPeriod{
|
|
{PeriodBegins: "9 AM", TextDescription: "Cloudy", TemperatureF: floatPtr(74), ProbabilityOfPrecipitationPercent: floatPtr(30), WindDirection: "S", WindSpeedMph: floatPtr(8)},
|
|
{PeriodBegins: "10 AM", TextDescription: "Showers", TemperatureF: floatPtr(75), ProbabilityOfPrecipitationPercent: floatPtr(70), WindDirection: "S", WindSpeedMph: floatPtr(10)},
|
|
},
|
|
},
|
|
PrecipTiming: &testPrecipTiming{
|
|
MaxPopPercent: intPtr(70),
|
|
MaxPopTime: "10 AM",
|
|
PrecipitationWindows: []testPrecipWindow{
|
|
{PeriodBegins: "10 AM", PeriodEnds: "12 PM", MaxPopPercent: intPtr(70), MaxPopTime: "10 AM"},
|
|
},
|
|
},
|
|
AlertDigest: &testAlertDigest{
|
|
Relevant: []testAlert{{Event: "Flood Watch", Headline: "Flood Watch until 2:30 PM", Severity: "Moderate"}},
|
|
},
|
|
SPCConvectiveOutlooks: &testSPCOutlooks{
|
|
Outlooks: []testSPCOutlook{{LabelText: "Slight Risk", PeriodBegins: "8 AM", PeriodEnds: "2 PM"}},
|
|
},
|
|
SPCConvectiveDiscussion: &testSPCDiscussion{
|
|
Discussions: []testSPCDiscussionRecord{{Summary: "Strong storms may develop late morning."}},
|
|
},
|
|
AreaForecastDiscussion: &testForecastDiscussion{
|
|
KeyMessages: []string{"Storms are most likely late morning."},
|
|
ShortTerm: "Short-term discussion favors increasing rain coverage.",
|
|
},
|
|
WeatherStory: &testWeatherStory{
|
|
Available: true,
|
|
Title: "Morning storms",
|
|
Description: "Morning storms remain the main story.",
|
|
},
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Render() error = %v", err)
|
|
}
|
|
text := string(rendered)
|
|
for _, want := range []string{
|
|
"# Hourly Report",
|
|
"Valid: May 29, 8:30 AM to 2:30 PM",
|
|
"Storm chances increase through late morning.",
|
|
"## Confidence",
|
|
"- 10 AM: Showers; 75 F; 70% precipitation; wind S 10 mph",
|
|
"- Flood Watch: Flood Watch until 2:30 PM (Moderate)",
|
|
"Short-term discussion favors increasing rain coverage.",
|
|
} {
|
|
if !strings.Contains(text, want) {
|
|
t.Fatalf("rendered template missing %q:\n%s", want, text)
|
|
}
|
|
}
|
|
assertOrderedText(t, text, []string{
|
|
"# Hourly Report",
|
|
"## Summary",
|
|
"## Timing",
|
|
"## Impacts",
|
|
"## Confidence",
|
|
"## Current Conditions",
|
|
"## Hourly Forecast",
|
|
"## Precipitation Timing",
|
|
"## Alerts",
|
|
"## SPC Outlooks",
|
|
"## Forecast Discussion",
|
|
"## SPC Discussion",
|
|
"## Weather Story",
|
|
})
|
|
}
|
|
|
|
func TestUnknownAssetsReturnActionableErrors(t *testing.T) {
|
|
if _, err := Template("daily"); err == nil || !strings.Contains(err.Error(), `unknown report template "daily"`) {
|
|
t.Fatalf("Template() error = %v, want unknown template", err)
|
|
}
|
|
if _, err := Schema("daily"); err == nil || !strings.Contains(err.Error(), `unknown generated text schema "daily"`) {
|
|
t.Fatalf("Schema() error = %v, want unknown schema", err)
|
|
}
|
|
if _, err := Render("daily", testRenderContext{}); err == nil || !strings.Contains(err.Error(), `unknown report template "daily"`) {
|
|
t.Fatalf("Render() error = %v, want unknown template", err)
|
|
}
|
|
}
|
|
|
|
func TestRenderFailsForMissingContextFields(t *testing.T) {
|
|
_, err := Render("hourly", map[string]any{"Report": map[string]any{"Title": "Hourly Report"}})
|
|
if err == nil {
|
|
t.Fatal("Render() error = nil, want missing field error")
|
|
}
|
|
if !strings.Contains(err.Error(), `render report template "hourly"`) {
|
|
t.Fatalf("Render() error = %v, want render context", err)
|
|
}
|
|
}
|
|
|
|
type testRenderContext struct {
|
|
Report testReportContext
|
|
GeneratedText testGeneratedText
|
|
Modules testModules
|
|
}
|
|
|
|
type testReportContext struct {
|
|
Title string
|
|
LocationName string
|
|
ValidPeriodLabel string
|
|
GeneratedAtLabel string
|
|
}
|
|
|
|
type testGeneratedText struct {
|
|
Summary string
|
|
Timing string
|
|
Impacts string
|
|
Confidence string
|
|
}
|
|
|
|
type testModules struct {
|
|
CurrentConditions *testCurrentConditions
|
|
HourlyForecast *testHourlyForecast
|
|
PrecipTiming *testPrecipTiming
|
|
AlertDigest *testAlertDigest
|
|
SPCConvectiveOutlooks *testSPCOutlooks
|
|
AreaForecastDiscussion *testForecastDiscussion
|
|
SPCConvectiveDiscussion *testSPCDiscussion
|
|
WeatherStory *testWeatherStory
|
|
}
|
|
|
|
type testCurrentConditions struct {
|
|
ConditionText string
|
|
TemperatureF *float64
|
|
ApparentTemperatureF *float64
|
|
RelativeHumidityPercent *float64
|
|
WindSpeedMph *float64
|
|
WindDirection string
|
|
}
|
|
|
|
type testHourlyForecast struct {
|
|
Periods []testHourlyPeriod
|
|
}
|
|
|
|
type testHourlyPeriod struct {
|
|
PeriodBegins string
|
|
Name string
|
|
TextDescription string
|
|
TemperatureF *float64
|
|
WindSpeedMph *float64
|
|
WindGustMph *float64
|
|
WindDirection string
|
|
ProbabilityOfPrecipitationPercent *float64
|
|
}
|
|
|
|
type testPrecipTiming struct {
|
|
MaxPopPercent *int
|
|
MaxPopTime string
|
|
PrecipitationWindows []testPrecipWindow
|
|
ThunderMentioned bool
|
|
}
|
|
|
|
type testPrecipWindow struct {
|
|
PeriodBegins string
|
|
PeriodEnds string
|
|
MaxPopPercent *int
|
|
MaxPopTime string
|
|
}
|
|
|
|
type testAlertDigest struct {
|
|
Missing bool
|
|
Relevant []testAlert
|
|
}
|
|
|
|
type testAlert struct {
|
|
Event string
|
|
Headline string
|
|
Severity string
|
|
}
|
|
|
|
type testSPCOutlooks struct {
|
|
Outlooks []testSPCOutlook
|
|
}
|
|
|
|
type testSPCOutlook struct {
|
|
Label string
|
|
LabelText string
|
|
OutlookType string
|
|
PeriodBegins string
|
|
PeriodEnds string
|
|
}
|
|
|
|
type testForecastDiscussion struct {
|
|
KeyMessages []string
|
|
ShortTerm string
|
|
}
|
|
|
|
type testSPCDiscussion struct {
|
|
Discussions []testSPCDiscussionRecord
|
|
}
|
|
|
|
type testSPCDiscussionRecord struct {
|
|
Headline string
|
|
Summary string
|
|
Discussion string
|
|
}
|
|
|
|
type testWeatherStory struct {
|
|
Available bool
|
|
Title string
|
|
Description string
|
|
}
|
|
|
|
func floatPtr(value float64) *float64 {
|
|
return &value
|
|
}
|
|
|
|
func intPtr(value int) *int {
|
|
return &value
|
|
}
|
|
|
|
func assertOrderedText(t *testing.T, text string, ordered []string) {
|
|
t.Helper()
|
|
previousIndex := -1
|
|
for _, want := range ordered {
|
|
index := strings.Index(text, want)
|
|
if index < 0 {
|
|
t.Fatalf("text missing %q:\n%s", want, text)
|
|
}
|
|
if index <= previousIndex {
|
|
t.Fatalf("%q appears out of order in:\n%s", want, text)
|
|
}
|
|
previousIndex = index
|
|
}
|
|
}
|