Add cross-office NWS forecast discussion coverage
This commit is contained in:
@@ -135,6 +135,85 @@ func TestForecastDiscussionNormalizerSupportsMixedHeadingFormats(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestForecastDiscussionNormalizerSupportsCrossOfficeLayout(t *testing.T) {
|
||||||
|
in := event.Event{
|
||||||
|
ID: "evt-discussion-bou",
|
||||||
|
Kind: event.Kind(standards.KindForecastDiscussion),
|
||||||
|
Source: "nws-discussion-bou-test",
|
||||||
|
EmittedAt: time.Date(2026, 4, 7, 19, 1, 0, 0, time.UTC),
|
||||||
|
Schema: standards.SchemaRawNWSForecastDiscussionV1,
|
||||||
|
Payload: loadForecastDiscussionBOUSampleHTML(t),
|
||||||
|
}
|
||||||
|
|
||||||
|
out, err := (ForecastDiscussionNormalizer{}).Normalize(nil, in)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Normalize() error = %v", err)
|
||||||
|
}
|
||||||
|
if out == nil {
|
||||||
|
t.Fatalf("Normalize() returned nil output")
|
||||||
|
}
|
||||||
|
if out.ID != in.ID || out.Source != in.Source || !out.EmittedAt.Equal(in.EmittedAt) {
|
||||||
|
t.Fatalf("envelope = %#v, want ID/source/emittedAt from input", out)
|
||||||
|
}
|
||||||
|
if out.Kind != event.Kind(standards.KindForecastDiscussion) {
|
||||||
|
t.Fatalf("Kind = %q, want forecast_discussion", out.Kind)
|
||||||
|
}
|
||||||
|
if out.Schema != standards.SchemaWeatherForecastDiscussionV1 {
|
||||||
|
t.Fatalf("Schema = %q, want %q", out.Schema, standards.SchemaWeatherForecastDiscussionV1)
|
||||||
|
}
|
||||||
|
wantEffectiveAt := time.Date(2026, 4, 7, 19, 0, 0, 0, time.UTC)
|
||||||
|
if out.EffectiveAt == nil || !out.EffectiveAt.Equal(wantEffectiveAt) {
|
||||||
|
t.Fatalf("EffectiveAt = %v, want %s", out.EffectiveAt, wantEffectiveAt.Format(time.RFC3339))
|
||||||
|
}
|
||||||
|
|
||||||
|
payload, ok := out.Payload.(model.WeatherForecastDiscussion)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("Payload type = %T, want model.WeatherForecastDiscussion", out.Payload)
|
||||||
|
}
|
||||||
|
if payload.OfficeID != "BOU" || payload.OfficeName != "National Weather Service Denver CO" {
|
||||||
|
t.Fatalf("OfficeID=%q OfficeName=%q", payload.OfficeID, payload.OfficeName)
|
||||||
|
}
|
||||||
|
wantMessages := []string{
|
||||||
|
"Strong winds are expected along the Front Range this evening.",
|
||||||
|
"Cooler temperatures arrive on Wednesday.",
|
||||||
|
}
|
||||||
|
if len(payload.KeyMessages) != len(wantMessages) {
|
||||||
|
t.Fatalf("KeyMessages = %#v, want %#v", payload.KeyMessages, wantMessages)
|
||||||
|
}
|
||||||
|
for i := range wantMessages {
|
||||||
|
if payload.KeyMessages[i] != wantMessages[i] {
|
||||||
|
t.Fatalf("KeyMessages[%d] = %q, want %q", i, payload.KeyMessages[i], wantMessages[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if payload.ShortTerm == nil || payload.LongTerm == nil {
|
||||||
|
t.Fatalf("ShortTerm=%v LongTerm=%v, want both populated", payload.ShortTerm, payload.LongTerm)
|
||||||
|
}
|
||||||
|
if payload.ShortTerm.Qualifier != "(Tonight through Wednesday)" || payload.ShortTerm.Text != "Gusty west winds will continue through the evening before decreasing overnight." {
|
||||||
|
t.Fatalf("ShortTerm = %#v", payload.ShortTerm)
|
||||||
|
}
|
||||||
|
if payload.LongTerm.Qualifier != "(Thursday through Saturday)" || payload.LongTerm.Text != "Warmer and drier conditions return Thursday, followed by a chance of showers Friday." {
|
||||||
|
t.Fatalf("LongTerm = %#v", payload.LongTerm)
|
||||||
|
}
|
||||||
|
if payload.ShortTerm.IssuedAt == nil || payload.LongTerm.IssuedAt == nil ||
|
||||||
|
!payload.ShortTerm.IssuedAt.Equal(wantEffectiveAt) || !payload.LongTerm.IssuedAt.Equal(wantEffectiveAt) {
|
||||||
|
t.Fatalf("section issue times = short %v long %v, want %s", payload.ShortTerm.IssuedAt, payload.LongTerm.IssuedAt, wantEffectiveAt.Format(time.RFC3339))
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := json.Marshal(out.Payload)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("json.Marshal(payload) error = %v", err)
|
||||||
|
}
|
||||||
|
var fields map[string]any
|
||||||
|
if err := json.Unmarshal(b, &fields); err != nil {
|
||||||
|
t.Fatalf("json.Unmarshal(payload) error = %v", err)
|
||||||
|
}
|
||||||
|
for _, key := range []string{"aviation", "discussion", "sections"} {
|
||||||
|
if _, ok := fields[key]; ok {
|
||||||
|
t.Fatalf("unexpected key %q in canonical payload", key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestForecastDiscussionNormalizerRejectsMissingIssueTime(t *testing.T) {
|
func TestForecastDiscussionNormalizerRejectsMissingIssueTime(t *testing.T) {
|
||||||
_, err := (ForecastDiscussionNormalizer{}).Normalize(nil, event.Event{
|
_, err := (ForecastDiscussionNormalizer{}).Normalize(nil, event.Event{
|
||||||
ID: "evt-discussion-bad",
|
ID: "evt-discussion-bad",
|
||||||
@@ -193,6 +272,17 @@ func loadForecastDiscussionSampleHTML(t *testing.T) string {
|
|||||||
return string(b)
|
return string(b)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func loadForecastDiscussionBOUSampleHTML(t *testing.T) string {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
path := filepath.Join("..", "..", "providers", "nws", "testdata", "forecast_discussion_bou_sample.html")
|
||||||
|
b, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("os.ReadFile(%q) error = %v", path, err)
|
||||||
|
}
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
|
||||||
func loadMixedFormatForecastDiscussionSampleHTML(t *testing.T) string {
|
func loadMixedFormatForecastDiscussionSampleHTML(t *testing.T) string {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
|
|||||||
@@ -553,6 +553,60 @@ func TestParseForecastDiscussionTextSectionKeepsArbitraryDashedProse(t *testing.
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParseForecastDiscussionHTMLParsesCrossOfficeLayout(t *testing.T) {
|
||||||
|
got, err := ParseForecastDiscussionHTML(loadForecastDiscussionBOUSampleHTML(t))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ParseForecastDiscussionHTML() error = %v", err)
|
||||||
|
}
|
||||||
|
if got.OfficeID != "BOU" || got.OfficeName != "National Weather Service Denver CO" {
|
||||||
|
t.Fatalf("OfficeID=%q OfficeName=%q", got.OfficeID, got.OfficeName)
|
||||||
|
}
|
||||||
|
wantIssuedAt := time.Date(2026, 4, 7, 19, 0, 0, 0, time.UTC)
|
||||||
|
if !got.IssuedAt.Equal(wantIssuedAt) {
|
||||||
|
t.Fatalf("IssuedAt = %s, want %s", got.IssuedAt.Format(time.RFC3339), wantIssuedAt.Format(time.RFC3339))
|
||||||
|
}
|
||||||
|
wantMessages := []string{
|
||||||
|
"Strong winds are expected along the Front Range this evening.",
|
||||||
|
"Cooler temperatures arrive on Wednesday.",
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(got.KeyMessages, wantMessages) {
|
||||||
|
t.Fatalf("KeyMessages = %#v, want %#v", got.KeyMessages, wantMessages)
|
||||||
|
}
|
||||||
|
|
||||||
|
if got.ShortTerm == nil {
|
||||||
|
t.Fatalf("ShortTerm is nil")
|
||||||
|
}
|
||||||
|
if got.ShortTerm.Qualifier != "(Tonight through Wednesday)" {
|
||||||
|
t.Fatalf("ShortTerm.Qualifier = %q", got.ShortTerm.Qualifier)
|
||||||
|
}
|
||||||
|
if got.ShortTerm.IssuedAt == nil || !got.ShortTerm.IssuedAt.Equal(wantIssuedAt) {
|
||||||
|
t.Fatalf("ShortTerm.IssuedAt = %v, want %s", got.ShortTerm.IssuedAt, wantIssuedAt.Format(time.RFC3339))
|
||||||
|
}
|
||||||
|
if got.ShortTerm.Text != "Gusty west winds will continue through the evening before decreasing overnight." {
|
||||||
|
t.Fatalf("ShortTerm.Text = %q", got.ShortTerm.Text)
|
||||||
|
}
|
||||||
|
|
||||||
|
if got.LongTerm == nil {
|
||||||
|
t.Fatalf("LongTerm is nil")
|
||||||
|
}
|
||||||
|
if got.LongTerm.Qualifier != "(Thursday through Saturday)" {
|
||||||
|
t.Fatalf("LongTerm.Qualifier = %q", got.LongTerm.Qualifier)
|
||||||
|
}
|
||||||
|
if got.LongTerm.IssuedAt == nil || !got.LongTerm.IssuedAt.Equal(wantIssuedAt) {
|
||||||
|
t.Fatalf("LongTerm.IssuedAt = %v, want %s", got.LongTerm.IssuedAt, wantIssuedAt.Format(time.RFC3339))
|
||||||
|
}
|
||||||
|
if got.LongTerm.Text != "Warmer and drier conditions return Thursday, followed by a chance of showers Friday." {
|
||||||
|
t.Fatalf("LongTerm.Text = %q", got.LongTerm.Text)
|
||||||
|
}
|
||||||
|
|
||||||
|
allText := strings.Join(append(append([]string(nil), got.KeyMessages...), got.ShortTerm.Text, got.LongTerm.Text), "\n")
|
||||||
|
for _, unwanted := range []string{"Changed Discussion", "Updated at", "Issued at", "ISSUED AT", "AVIATION", "VFR conditions"} {
|
||||||
|
if strings.Contains(allText, unwanted) {
|
||||||
|
t.Fatalf("parsed content includes %q: %q", unwanted, allText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestParseForecastDiscussionHTMLParsesExpectedFields(t *testing.T) {
|
func TestParseForecastDiscussionHTMLParsesExpectedFields(t *testing.T) {
|
||||||
raw := loadForecastDiscussionSampleHTML(t)
|
raw := loadForecastDiscussionSampleHTML(t)
|
||||||
|
|
||||||
@@ -707,6 +761,17 @@ func loadForecastDiscussionSampleHTML(t *testing.T) string {
|
|||||||
return string(b)
|
return string(b)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func loadForecastDiscussionBOUSampleHTML(t *testing.T) string {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
path := filepath.Join("testdata", "forecast_discussion_bou_sample.html")
|
||||||
|
b, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("os.ReadFile(%q) error = %v", path, err)
|
||||||
|
}
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
|
||||||
func loadMixedFormatForecastDiscussionSampleHTML(t *testing.T) string {
|
func loadMixedFormatForecastDiscussionSampleHTML(t *testing.T) string {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
|
|||||||
48
internal/providers/nws/testdata/forecast_discussion_bou_sample.html
vendored
Normal file
48
internal/providers/nws/testdata/forecast_discussion_bou_sample.html
vendored
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<pre class="glossaryProduct">
|
||||||
|
FXUS65 KBOU 071900
|
||||||
|
AFDBOU
|
||||||
|
|
||||||
|
Area Forecast Discussion
|
||||||
|
National Weather Service Denver CO
|
||||||
|
100 PM MDT Tue Apr 7 2026
|
||||||
|
|
||||||
|
.KEY MESSAGES...
|
||||||
|
-- Changed Discussion --
|
||||||
|
Updated at 100 PM MDT Tue Apr 7 2026
|
||||||
|
- Strong winds are expected along the Front Range this evening.
|
||||||
|
- Cooler temperatures arrive on Wednesday.
|
||||||
|
-- End Changed Discussion --
|
||||||
|
|
||||||
|
&&
|
||||||
|
|
||||||
|
.SHORT TERM...
|
||||||
|
(Tonight through Wednesday)
|
||||||
|
Issued at 100 PM MDT Tue Apr 7 2026
|
||||||
|
|
||||||
|
Gusty west winds will continue through the evening before decreasing overnight.
|
||||||
|
|
||||||
|
&&
|
||||||
|
|
||||||
|
.LONG TERM...
|
||||||
|
(Thursday through Saturday)
|
||||||
|
ISSUED AT 100 PM MDT Tue Apr 7 2026
|
||||||
|
|
||||||
|
Warmer and drier conditions return Thursday, followed by a chance of showers Friday.
|
||||||
|
|
||||||
|
&&
|
||||||
|
|
||||||
|
.AVIATION...
|
||||||
|
|
||||||
|
VFR conditions are expected at Denver-area terminals through Wednesday morning.
|
||||||
|
|
||||||
|
&&
|
||||||
|
|
||||||
|
$$
|
||||||
|
|
||||||
|
WFO BOU
|
||||||
|
</pre>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user