Files
weatherfeeder/internal/providers/nws/forecast_discussion_test.go

750 lines
23 KiB
Go

package nws
import (
"os"
"path/filepath"
"reflect"
"strings"
"testing"
"time"
)
func TestParseForecastDiscussionSectionHeading(t *testing.T) {
tests := []struct {
name string
line string
wantSection string
wantQualifier string
wantOK bool
}{
{
name: "ellipsis-first key messages",
line: ".KEY MESSAGES... (Through Tonight)",
wantSection: "KEY MESSAGES",
wantQualifier: "(Through Tonight)",
wantOK: true,
},
{
name: "ellipsis-first short term",
line: ".SHORT TERM... (Sunday)",
wantSection: "SHORT TERM",
wantQualifier: "(Sunday)",
wantOK: true,
},
{
name: "ellipsis-first long term",
line: ".LONG TERM... (Monday Through Friday)",
wantSection: "LONG TERM",
wantQualifier: "(Monday Through Friday)",
wantOK: true,
},
{
name: "ellipsis-first aviation without qualifier",
line: ".AVIATION...",
wantSection: "AVIATION",
wantOK: true,
},
{
name: "slash-qualified key messages",
line: ".KEY MESSAGES /Tonight/...",
wantSection: "KEY MESSAGES",
wantQualifier: "Tonight",
wantOK: true,
},
{
name: "slash-qualified short term with outer whitespace",
line: " .SHORT TERM /Through Late Sunday Night/... ",
wantSection: "SHORT TERM",
wantQualifier: "Through Late Sunday Night",
wantOK: true,
},
{
name: "slash-qualified long term",
line: ".LONG TERM /Monday through Next Saturday/...",
wantSection: "LONG TERM",
wantQualifier: "Monday through Next Saturday",
wantOK: true,
},
{
name: "slash-qualified aviation",
line: ".AVIATION /18Z TAFS/...",
wantSection: "AVIATION",
wantQualifier: "18Z TAFS",
wantOK: true,
},
{
name: "generic synopsis",
line: ".SYNOPSIS... Overview",
wantSection: "SYNOPSIS",
wantQualifier: "Overview",
wantOK: true,
},
{
name: "generic update",
line: ".UPDATE...",
wantSection: "UPDATE",
wantOK: true,
},
{
name: "generic marine",
line: ".MARINE...",
wantSection: "MARINE",
wantOK: true,
},
{
name: "generic hydrology",
line: ".HYDROLOGY...",
wantSection: "HYDROLOGY",
wantOK: true,
},
{
name: "office watch advisory heading",
line: ".LSX WATCHES/WARNINGS/ADVISORIES...",
wantSection: "LSX WATCHES/WARNINGS/ADVISORIES",
wantOK: true,
},
{
name: "preliminary point temperatures heading",
line: ".PRELIMINARY POINT TEMPS/POPS ...",
wantSection: "PRELIMINARY POINT TEMPS/POPS",
wantOK: true,
},
{
name: "generic identity punctuation and digits",
line: ".DAY 1 FIRE-WEATHER & HYDROLOGY'S/OUTLOOK...",
wantSection: "DAY 1 FIRE-WEATHER & HYDROLOGY'S/OUTLOOK",
wantOK: true,
},
{
name: "slash qualifier contains slash",
line: ".LONG TERM /Tonight/Sunday/...",
wantSection: "LONG TERM",
wantQualifier: "Tonight/Sunday",
wantOK: true,
},
{
name: "normalized identity whitespace",
line: " .SHORT\t\tTERM \t... (Tonight) ",
wantSection: "SHORT TERM",
wantQualifier: "(Tonight)",
wantOK: true,
},
{
name: "lowercase prose",
line: ".This is ordinary prose...",
wantOK: false,
},
{
name: "empty identity",
line: "....",
wantOK: false,
},
{
name: "punctuation only identity",
line: ".-/&'...",
wantOK: false,
},
{
name: "slash qualifier missing opening separator",
line: ".SHORT TERM Through Tonight/...",
wantOK: false,
},
{
name: "slash qualifier missing required whitespace",
line: ".SHORT TERM/Through Tonight/...",
wantOK: false,
},
{
name: "slash qualifier missing closing terminal",
line: ".SHORT TERM /Through Tonight...",
wantOK: false,
},
{
name: "slash qualifier whitespace only",
line: ".SHORT TERM / /...",
wantOK: false,
},
{
name: "slash qualifier missing final ellipsis",
line: ".SHORT TERM /Through Tonight/..",
wantOK: false,
},
{
name: "slash qualifier has trailing text",
line: ".SHORT TERM /Through Tonight/... more",
wantOK: false,
},
{
name: "missing ellipsis",
line: ".SHORT TERM",
wantOK: false,
},
{
name: "unsupported identity punctuation",
line: ".SHORT TERM:...",
wantOK: false,
},
{
name: "unicode ellipsis",
line: ".SHORT TERM…",
wantOK: false,
},
{
name: "mixed case identity",
line: ".Short Term... (Tonight)",
wantOK: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, ok := parseForecastDiscussionSectionHeading(tt.line)
if ok != tt.wantOK {
t.Fatalf("parseForecastDiscussionSectionHeading(%q) ok = %t, want %t", tt.line, ok, tt.wantOK)
}
if !ok {
return
}
if got.section != tt.wantSection {
t.Fatalf("section = %q, want %q", got.section, tt.wantSection)
}
if got.qualifier != tt.wantQualifier {
t.Fatalf("qualifier = %q, want %q", got.qualifier, tt.wantQualifier)
}
})
}
}
func TestForecastDiscussionSectionRoleHeadingsParse(t *testing.T) {
if len(forecastDiscussionSectionRoles) != 3 {
t.Fatalf("role registry has %d entries, want 3", len(forecastDiscussionSectionRoles))
}
if _, ok := forecastDiscussionSectionRoles["AVIATION"]; ok {
t.Fatalf("AVIATION must remain boundary-only")
}
for section := range forecastDiscussionSectionRoles {
t.Run(section, func(t *testing.T) {
got, ok := parseForecastDiscussionSectionHeading("." + section + "...")
if !ok {
t.Fatalf("parseForecastDiscussionSectionHeading() did not recognize %q", section)
}
if got.section != section {
t.Fatalf("section = %q, want %q", got.section, section)
}
})
}
}
func TestParseForecastDiscussionSectionBlocksRetainsOrderAndEmptyBodies(t *testing.T) {
got := parseForecastDiscussionSectionBlocks([]string{
".SHORT TERM... (Tonight)",
".LONG TERM... (Tomorrow)",
"Long-term prose.",
})
want := []forecastDiscussionSectionBlock{
{heading: forecastDiscussionSectionHeading{section: "SHORT TERM", qualifier: "(Tonight)"}},
{
heading: forecastDiscussionSectionHeading{section: "LONG TERM", qualifier: "(Tomorrow)"},
body: []string{"Long-term prose."},
},
}
if !reflect.DeepEqual(got, want) {
t.Fatalf("blocks = %#v, want %#v", got, want)
}
}
func TestParseForecastDiscussionSectionBlocksStopsAtTerminators(t *testing.T) {
got := parseForecastDiscussionSectionBlocks([]string{
"Preamble is ignored.",
".KEY MESSAGES...",
"- First message.",
"&&",
".SHORT TERM...",
"Short-term prose.",
".LSX WATCHES/WARNINGS/ADVISORIES...",
"Watch text is ignored.",
"$$",
"WFO LSX",
})
want := []forecastDiscussionSectionBlock{
{
heading: forecastDiscussionSectionHeading{section: "KEY MESSAGES"},
body: []string{"- First message."},
},
{
heading: forecastDiscussionSectionHeading{section: "SHORT TERM"},
body: []string{"Short-term prose."},
},
}
if !reflect.DeepEqual(got, want) {
t.Fatalf("blocks = %#v, want %#v", got, want)
}
}
func TestParseForecastDiscussionSectionBlocksUsesGenericHeadingsAsBoundaries(t *testing.T) {
got := parseForecastDiscussionSectionBlocks([]string{
".SHORT TERM...",
"Short-term prose.",
".SYNOPSIS...",
"Synopsis prose.",
".LONG TERM /Tomorrow/...",
"Long-term prose.",
})
want := []forecastDiscussionSectionBlock{
{
heading: forecastDiscussionSectionHeading{section: "SHORT TERM"},
body: []string{"Short-term prose."},
},
{
heading: forecastDiscussionSectionHeading{section: "SYNOPSIS"},
body: []string{"Synopsis prose."},
},
{
heading: forecastDiscussionSectionHeading{section: "LONG TERM", qualifier: "Tomorrow"},
body: []string{"Long-term prose."},
},
}
if !reflect.DeepEqual(got, want) {
t.Fatalf("blocks = %#v, want %#v", got, want)
}
}
func TestParseForecastDiscussionSectionBlocksKeepsMalformedHeadingLikeLines(t *testing.T) {
got := parseForecastDiscussionSectionBlocks([]string{
".SHORT TERM... (Tonight)",
".short term... Lowercase prose stays in the body.",
".LONG TERM /Tomorrow/..",
".LONG TERM /Tomorrow/... more",
"Expected short-term prose.",
".LONG TERM... (Tomorrow)",
"Long-term prose.",
})
want := []forecastDiscussionSectionBlock{
{
heading: forecastDiscussionSectionHeading{section: "SHORT TERM", qualifier: "(Tonight)"},
body: []string{
".short term... Lowercase prose stays in the body.",
".LONG TERM /Tomorrow/..",
".LONG TERM /Tomorrow/... more",
"Expected short-term prose.",
},
},
{
heading: forecastDiscussionSectionHeading{section: "LONG TERM", qualifier: "(Tomorrow)"},
body: []string{"Long-term prose."},
},
}
if !reflect.DeepEqual(got, want) {
t.Fatalf("blocks = %#v, want %#v", got, want)
}
}
func TestParseForecastDiscussionTextKeepsFirstMappedSections(t *testing.T) {
text := strings.Join([]string{
"AFDLSX",
"National Weather Service Saint Louis MO",
"224 PM CDT Sat Mar 28 2026",
"",
".KEY MESSAGES...",
"&&",
".KEY MESSAGES...",
"- Second key message.",
"&&",
".SHORT TERM... (First short term)",
"First short-term prose.",
"&&",
".SHORT TERM... (Second short term)",
"Second short-term prose.",
"&&",
".LONG TERM... (First long term)",
"First long-term prose.",
"&&",
".LONG TERM... (Second long term)",
"Second long-term prose.",
}, "\n")
got, err := ParseForecastDiscussionText(text)
if err != nil {
t.Fatalf("ParseForecastDiscussionText() error = %v", err)
}
if len(got.KeyMessages) != 0 {
t.Fatalf("KeyMessages = %#v, want empty first block", got.KeyMessages)
}
if got.ShortTerm == nil || got.ShortTerm.Qualifier != "(First short term)" || got.ShortTerm.Text != "First short-term prose." {
t.Fatalf("ShortTerm = %#v, want first section", got.ShortTerm)
}
if got.LongTerm == nil || got.LongTerm.Qualifier != "(First long term)" || got.LongTerm.Text != "First long-term prose." {
t.Fatalf("LongTerm = %#v, want first section", got.LongTerm)
}
}
func TestParseForecastDiscussionTextSectionKeepsHeadingQualifiers(t *testing.T) {
tests := []struct {
name string
heading string
wantQualifier string
}{
{
name: "legacy heading",
heading: ".SHORT TERM... (Tonight)",
wantQualifier: "(Tonight)",
},
{
name: "slash-qualified heading",
heading: ".SHORT TERM /Tonight/...",
wantQualifier: "Tonight",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
section, err := parseForecastDiscussionTextSection(forecastDiscussionSectionBlock{
heading: mustParseForecastDiscussionSectionHeading(t, tt.heading),
body: []string{"Section prose."},
})
if err != nil {
t.Fatalf("parseForecastDiscussionTextSection() error = %v", err)
}
if section.Qualifier != tt.wantQualifier {
t.Fatalf("Qualifier = %q, want %q", section.Qualifier, tt.wantQualifier)
}
if section.Text != "Section prose." {
t.Fatalf("Text = %q", section.Text)
}
})
}
}
func TestParseForecastDiscussionTextSectionReadsNextLineQualifierAndIssuedAt(t *testing.T) {
section, err := parseForecastDiscussionTextSection(forecastDiscussionSectionBlock{
heading: mustParseForecastDiscussionSectionHeading(t, ".SHORT TERM..."),
body: []string{
"(Tonight)",
"Issued at 219 PM CDT Sat Mar 28 2026",
"Section prose.",
},
})
if err != nil {
t.Fatalf("parseForecastDiscussionTextSection() error = %v", err)
}
if section.Qualifier != "(Tonight)" {
t.Fatalf("Qualifier = %q", section.Qualifier)
}
wantIssuedAt := time.Date(2026, 3, 28, 19, 19, 0, 0, time.UTC)
if section.IssuedAt == nil || !section.IssuedAt.Equal(wantIssuedAt) {
t.Fatalf("IssuedAt = %v, want %s", section.IssuedAt, wantIssuedAt.Format(time.RFC3339))
}
if section.Text != "Section prose." {
t.Fatalf("Text = %q", section.Text)
}
}
func TestParseForecastDiscussionTextSectionRecognizesUppercaseIssuedAt(t *testing.T) {
section, err := parseForecastDiscussionTextSection(forecastDiscussionSectionBlock{
heading: mustParseForecastDiscussionSectionHeading(t, ".SHORT TERM..."),
body: []string{
"ISSUED AT 219 PM CDT Sat Mar 28 2026",
"Section prose.",
},
})
if err != nil {
t.Fatalf("parseForecastDiscussionTextSection() error = %v", err)
}
wantIssuedAt := time.Date(2026, 3, 28, 19, 19, 0, 0, time.UTC)
if section.IssuedAt == nil || !section.IssuedAt.Equal(wantIssuedAt) {
t.Fatalf("IssuedAt = %v, want %s", section.IssuedAt, wantIssuedAt.Format(time.RFC3339))
}
}
func TestParseForecastDiscussionTextSectionKeepsHeadingQualifierBeforeParentheticalProse(t *testing.T) {
section, err := parseForecastDiscussionTextSection(forecastDiscussionSectionBlock{
heading: mustParseForecastDiscussionSectionHeading(t, ".SHORT TERM... (From Heading)"),
body: []string{"(Parenthetical prose.)"},
})
if err != nil {
t.Fatalf("parseForecastDiscussionTextSection() error = %v", err)
}
if section.Qualifier != "(From Heading)" {
t.Fatalf("Qualifier = %q", section.Qualifier)
}
if section.Text != "(Parenthetical prose.)" {
t.Fatalf("Text = %q", section.Text)
}
}
func TestParseForecastDiscussionTextSectionHandlesEmptyAndInvalidPreambles(t *testing.T) {
t.Run("empty section", func(t *testing.T) {
section, err := parseForecastDiscussionTextSection(forecastDiscussionSectionBlock{
heading: mustParseForecastDiscussionSectionHeading(t, ".SHORT TERM..."),
})
if err != nil {
t.Fatalf("parseForecastDiscussionTextSection() error = %v", err)
}
if section.Qualifier != "" || section.IssuedAt != nil || section.Text != "" {
t.Fatalf("section = %#v, want empty section", section)
}
})
t.Run("invalid issue time", func(t *testing.T) {
_, err := parseForecastDiscussionTextSection(forecastDiscussionSectionBlock{
heading: mustParseForecastDiscussionSectionHeading(t, ".SHORT TERM..."),
body: []string{"ISSUED AT not a timestamp"},
})
if err == nil {
t.Fatalf("parseForecastDiscussionTextSection() error = nil")
}
if !strings.Contains(err.Error(), "parse section issuedAt") {
t.Fatalf("error = %q, want section issue-time context", err)
}
})
}
func TestParseForecastDiscussionTextSectionRemovesPresentationMarkers(t *testing.T) {
section, err := parseForecastDiscussionTextSection(forecastDiscussionSectionBlock{
heading: mustParseForecastDiscussionSectionHeading(t, ".SHORT TERM..."),
body: []string{
"-- Changed Discussion --",
"(Tonight)",
"Issued at 219 PM CDT Sat Mar 28 2026",
"Section prose.",
"-- END CHANGED DISCUSSION --",
},
})
if err != nil {
t.Fatalf("parseForecastDiscussionTextSection() error = %v", err)
}
if section.Qualifier != "(Tonight)" || section.Text != "Section prose." {
t.Fatalf("section = %#v", section)
}
if strings.Contains(section.Text, "Changed Discussion") {
t.Fatalf("Text contains presentation marker: %q", section.Text)
}
}
func TestParseForecastDiscussionKeyMessagesRemovesMarkersAndLeadingMetadata(t *testing.T) {
got := parseForecastDiscussionKeyMessages([]string{
"-- changed discussion --",
"Updated at 300 PM CDT Sat Mar 28 2026",
"- First message.",
"Updated at after the first message.",
"-- End Changed Discussion --",
"- Second message.",
})
want := []string{
"First message. Updated at after the first message.",
"Second message.",
}
if !reflect.DeepEqual(got, want) {
t.Fatalf("KeyMessages = %#v, want %#v", got, want)
}
}
func TestParseForecastDiscussionTextSectionKeepsArbitraryDashedProse(t *testing.T) {
section, err := parseForecastDiscussionTextSection(forecastDiscussionSectionBlock{
heading: mustParseForecastDiscussionSectionHeading(t, ".SHORT TERM..."),
body: []string{"-- Changed Discussion -- extra", "Section prose."},
})
if err != nil {
t.Fatalf("parseForecastDiscussionTextSection() error = %v", err)
}
if section.Text != "-- Changed Discussion -- extra Section prose." {
t.Fatalf("Text = %q", section.Text)
}
}
func TestParseForecastDiscussionHTMLParsesExpectedFields(t *testing.T) {
raw := loadForecastDiscussionSampleHTML(t)
got, err := ParseForecastDiscussionHTML(raw)
if err != nil {
t.Fatalf("ParseForecastDiscussionHTML() error = %v", err)
}
if got.OfficeID != "LSX" {
t.Fatalf("OfficeID = %q, want LSX", got.OfficeID)
}
if got.OfficeName != "National Weather Service Saint Louis MO" {
t.Fatalf("OfficeName = %q", got.OfficeName)
}
if got.Product != "afd" {
t.Fatalf("Product = %q, want afd", got.Product)
}
wantIssuedAt := time.Date(2026, 3, 28, 19, 24, 0, 0, time.UTC)
if !got.IssuedAt.Equal(wantIssuedAt) {
t.Fatalf("IssuedAt = %s, want %s", got.IssuedAt.Format(time.RFC3339), wantIssuedAt.Format(time.RFC3339))
}
wantUpdatedAt := time.Date(2026, 3, 28, 20, 29, 47, 0, time.UTC)
if got.UpdatedAt == nil || !got.UpdatedAt.Equal(wantUpdatedAt) {
t.Fatalf("UpdatedAt = %v, want %s", got.UpdatedAt, wantUpdatedAt.Format(time.RFC3339))
}
wantMessages := []string{
"Elevated fire danger conditions are expected across a broad area tomorrow afternoon due to breezy southwest winds and low humidity.",
"Very warm temperatures are expected once again Monday and Tuesday, with highs well into the 80s.",
"A cold front late Tuesday or early Wednesday brings our next chance of thunderstorms, followed by a cooldown and possibly more chances for rain later in the week.",
}
if len(got.KeyMessages) != len(wantMessages) {
t.Fatalf("KeyMessages len = %d, want %d", len(got.KeyMessages), len(wantMessages))
}
for i := range wantMessages {
if got.KeyMessages[i] != wantMessages[i] {
t.Fatalf("KeyMessages[%d] = %q, want %q", i, got.KeyMessages[i], wantMessages[i])
}
}
if got.ShortTerm == nil {
t.Fatalf("ShortTerm is nil")
}
if got.ShortTerm.Qualifier != "(Through Late Sunday Night)" {
t.Fatalf("ShortTerm.Qualifier = %q", got.ShortTerm.Qualifier)
}
if got.ShortTerm.IssuedAt == nil || !got.ShortTerm.IssuedAt.Equal(time.Date(2026, 3, 28, 19, 19, 0, 0, time.UTC)) {
t.Fatalf("ShortTerm.IssuedAt = %v", got.ShortTerm.IssuedAt)
}
if !strings.Contains(got.ShortTerm.Text, "After a chilly morning") {
t.Fatalf("ShortTerm.Text missing expected prose: %q", got.ShortTerm.Text)
}
if strings.Contains(got.ShortTerm.Text, "BRC") {
t.Fatalf("ShortTerm.Text should not include signature: %q", got.ShortTerm.Text)
}
if strings.Contains(got.ShortTerm.Text, "\n\n\n") {
t.Fatalf("ShortTerm.Text contains unexpected paragraph breaks: %q", got.ShortTerm.Text)
}
if got.LongTerm == nil {
t.Fatalf("LongTerm is nil")
}
if got.LongTerm.Qualifier != "(Monday through Next Saturday)" {
t.Fatalf("LongTerm.Qualifier = %q", got.LongTerm.Qualifier)
}
if got.LongTerm.IssuedAt == nil || !got.LongTerm.IssuedAt.Equal(time.Date(2026, 3, 28, 19, 19, 0, 0, time.UTC)) {
t.Fatalf("LongTerm.IssuedAt = %v", got.LongTerm.IssuedAt)
}
if !strings.Contains(got.LongTerm.Text, "The peak of the warmth arrives Monday and Tuesday") {
t.Fatalf("LongTerm.Text missing expected prose: %q", got.LongTerm.Text)
}
if strings.Contains(got.LongTerm.Text, "AVIATION") || strings.Contains(got.LongTerm.Text, "WATCHES/WARNINGS/ADVISORIES") {
t.Fatalf("LongTerm.Text includes content from other sections: %q", got.LongTerm.Text)
}
}
func TestParseForecastDiscussionHTMLSupportsMixedHeadingFormats(t *testing.T) {
legacy, err := ParseForecastDiscussionHTML(loadForecastDiscussionSampleHTML(t))
if err != nil {
t.Fatalf("ParseForecastDiscussionHTML() legacy error = %v", err)
}
got, err := ParseForecastDiscussionHTML(loadMixedFormatForecastDiscussionSampleHTML(t))
if err != nil {
t.Fatalf("ParseForecastDiscussionHTML() mixed-format error = %v", err)
}
if !reflect.DeepEqual(got.KeyMessages, legacy.KeyMessages) {
t.Fatalf("KeyMessages = %#v, want unchanged %#v", got.KeyMessages, legacy.KeyMessages)
}
if got.ShortTerm == nil {
t.Fatalf("ShortTerm is nil")
}
if got.ShortTerm.Qualifier != "Through Late Sunday Night" {
t.Fatalf("ShortTerm.Qualifier = %q", got.ShortTerm.Qualifier)
}
wantIssuedAt := time.Date(2026, 3, 28, 19, 19, 0, 0, time.UTC)
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 !strings.Contains(got.ShortTerm.Text, "After a chilly morning") {
t.Fatalf("ShortTerm.Text missing expected prose: %q", got.ShortTerm.Text)
}
if got.LongTerm == nil {
t.Fatalf("LongTerm is nil")
}
if got.LongTerm.Qualifier != "Monday through Next 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 !strings.Contains(got.LongTerm.Text, "The peak of the warmth arrives Monday and Tuesday") {
t.Fatalf("LongTerm.Text missing expected prose: %q", got.LongTerm.Text)
}
if strings.Contains(got.LongTerm.Text, "AVIATION") || strings.Contains(got.LongTerm.Text, "VFR conditions are expected") {
t.Fatalf("LongTerm.Text includes aviation content: %q", got.LongTerm.Text)
}
}
func TestParseForecastDiscussionHTMLMissingPreBlock(t *testing.T) {
_, err := ParseForecastDiscussionHTML("<html><body><div>no pre block</div></body></html>")
if err == nil {
t.Fatalf("ParseForecastDiscussionHTML() error = nil, want error")
}
if !strings.Contains(err.Error(), "glossaryProduct") {
t.Fatalf("error = %q, want glossaryProduct context", err)
}
}
func TestParseForecastDiscussionTextMissingIssueTime(t *testing.T) {
_, err := ParseForecastDiscussionText("National Weather Service Saint Louis MO\n\n.KEY MESSAGES...\n- Test")
if err == nil {
t.Fatalf("ParseForecastDiscussionText() error = nil, want error")
}
if !strings.Contains(err.Error(), "issue time") {
t.Fatalf("error = %q, want issue time context", err)
}
}
func loadForecastDiscussionSampleHTML(t *testing.T) string {
t.Helper()
path := filepath.Join("testdata", "forecast_discussion_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 {
t.Helper()
raw := loadForecastDiscussionSampleHTML(t)
replacements := []struct {
original string
replacement string
}{
{
original: ".SHORT TERM... (Through Late Sunday Night)",
replacement: ".SHORT TERM /Through Late Sunday Night/...",
},
{
original: ".LONG TERM... (Monday through Next Saturday)",
replacement: ".LONG TERM /Monday through Next Saturday/...",
},
{
original: ".AVIATION... (For the 18z TAFs through 18z Sunday Afternoon)",
replacement: ".AVIATION /For the 18z TAFs through 18z Sunday Afternoon/...",
},
}
for _, replacement := range replacements {
if !strings.Contains(raw, replacement.original) {
t.Fatalf("fixture missing heading %q", replacement.original)
}
raw = strings.Replace(raw, replacement.original, replacement.replacement, 1)
}
return raw
}
func mustParseForecastDiscussionSectionHeading(t *testing.T, line string) forecastDiscussionSectionHeading {
t.Helper()
heading, ok := parseForecastDiscussionSectionHeading(line)
if !ok {
t.Fatalf("parseForecastDiscussionSectionHeading(%q) did not recognize heading", line)
}
return heading
}