From 4eb6636cb9efc17eff6ebe95994ad49cf93d9f74 Mon Sep 17 00:00:00 2001 From: Nathan Date: Thu, 2 Apr 2026 17:52:31 -0500 Subject: [PATCH] Refactor course-section references to use compound keys --- parser/courseParser.go | 51 ++--- parser/courseParser_test.go | 24 ++- parser/sectionParser.go | 72 +++---- parser/sectionParser_test.go | 33 ++-- parser/testdata/case_000/course.json | 5 +- parser/testdata/case_000/professors.json | 4 +- parser/testdata/case_000/section.json | 2 +- parser/testdata/case_001/course.json | 5 +- parser/testdata/case_001/professors.json | 4 +- parser/testdata/case_001/section.json | 2 +- parser/testdata/case_002/course.json | 5 +- parser/testdata/case_002/professors.json | 2 +- parser/testdata/case_002/section.json | 2 +- parser/testdata/case_003/course.json | 5 +- parser/testdata/case_003/professors.json | 2 +- parser/testdata/case_003/section.json | 2 +- parser/testdata/case_004/course.json | 5 +- parser/testdata/case_004/section.json | 2 +- parser/testdata/case_005/course.json | 5 +- parser/testdata/case_005/section.json | 2 +- parser/testdata/courses.json | 18 ++ parser/testdata/professors.json | 66 +++---- parser/validator.go | 112 ++++++----- parser/validator_test.go | 227 ++++++++++++----------- 24 files changed, 376 insertions(+), 281 deletions(-) diff --git a/parser/courseParser.go b/parser/courseParser.go index b927cf5..911f985 100644 --- a/parser/courseParser.go +++ b/parser/courseParser.go @@ -25,24 +25,25 @@ var ( // provided information. If the associated course is not found in // Courses, it will run getCourse and add the result to Courses. func parseCourse(internalCourseNumber string, session schema.AcademicSession, rowInfo map[string]*goquery.Selection, classInfo map[string]string) *schema.Course { - // Courses are internally keyed by their internal course number and the catalog year they're part of + // Courses are internally keyed by their identifying fields catalogYear := getCatalogYear(session) - courseKey := internalCourseNumber + catalogYear + subjectPrefix, courseNumber := getPrefixAndNumber(classInfo) + courseMapKey := subjectPrefix + courseNumber + catalogYear // Don't recreate the course if it already exists - course, courseExists := Courses[courseKey] + course, courseExists := Courses[courseMapKey] if courseExists { return course } course = getCourse(internalCourseNumber, session, rowInfo, classInfo) - // Get closure for parsing course requisites (god help me) + // Get closure for parsing course requisites enrollmentReqs, hasEnrollmentReqs := rowInfo["Enrollment Reqs:"] ReqParsers[course.Id] = getReqParser(course, hasEnrollmentReqs, enrollmentReqs) - Courses[courseKey] = course - CourseIDMap[course.Id] = courseKey + Courses[courseMapKey] = course + CourseIDMap[course.Id] = courseMapKey return course } @@ -50,13 +51,13 @@ func parseCourse(internalCourseNumber string, session schema.AcademicSession, ro // This function does not modify any global state. // Returns a pointer to the newly created schema.Course object. func getCourse(internalCourseNumber string, session schema.AcademicSession, rowInfo map[string]*goquery.Selection, classInfo map[string]string) *schema.Course { - CoursePrefix, CourseNumber := getPrefixAndNumber(classInfo) + subjectPrefix, courseNumber := getPrefixAndNumber(classInfo) catalogYear := getCatalogYear(session) course := schema.Course{ Id: primitive.NewObjectID(), - Course_number: CourseNumber, - Subject_prefix: CoursePrefix, + Subject_prefix: subjectPrefix, + Course_number: courseNumber, Title: utils.TrimWhitespace(rowInfo["Course Title:"].Text()), Description: utils.TrimWhitespace(rowInfo["Description:"].Text()), School: utils.TrimWhitespace(rowInfo["College:"].Text()), @@ -66,6 +67,7 @@ func getCourse(internalCourseNumber string, session schema.AcademicSession, rowI Grading: classInfo["Grading:"], Internal_course_number: internalCourseNumber, Catalog_year: catalogYear, + Sections: []schema.SectionKey{}, } // Try to get lecture/lab contact hours and offering frequency from course description @@ -82,13 +84,15 @@ func getCourse(internalCourseNumber string, session schema.AcademicSession, rowI // getCatalogYear determines the catalog year from the academic session information. // It assumes the session name starts with a 2-digit year and a semester character ('F', 'S', 'U'). -// Fall (S) and Summer U sessions are associated with the previous calendar year. -// (e.g, 20F = 20, 20S = 19) +// Fall sessions are associated with the same catalog year. +// Spring and Summer sessions are associated with the previous catalog year. +// (e.g. 20F = 20, 20S = 19) func getCatalogYear(session schema.AcademicSession) string { sessionYear, err := strconv.Atoi(session.Name[0:2]) if err != nil { panic(err) } + sessionSemester := session.Name[2] switch sessionSemester { case 'F': @@ -96,21 +100,24 @@ func getCatalogYear(session schema.AcademicSession) string { case 'S', 'U': return strconv.Itoa(sessionYear - 1) default: - panic(fmt.Errorf("encountered invalid session semester '%c!'", sessionSemester)) + panic(fmt.Errorf("encountered invalid session semester '%c'", sessionSemester)) } } // getPrefixAndNumber returns the 2nd and 3rd matched values from a coursePrefixRegexp on -// `ClassInfo["Class Section:"]`. It expects ClassInfo to contain "Class Section:" key. -// If there are no matches, empty strings are returned. +// classInfo["Class Section:"]. It expects classInfo to contain "Class Section:". +// If there are no matches, it panics. func getPrefixAndNumber(classInfo map[string]string) (string, string) { - if sectionId, ok := classInfo["Class Section:"]; ok { - // Get subject prefix and course number by doing a regexp match on the section id - matches := coursePrefixRegexp.FindStringSubmatch(sectionId) - if len(matches) == 3 { - return matches[1], matches[2] - } - panic("failed to course prefix and number") + sectionID, ok := classInfo["Class Section:"] + if !ok { + panic("could not find 'Class Section:' in classInfo") + } + + // Get subject prefix and course number by doing a regexp match on the section id + matches := coursePrefixRegexp.FindStringSubmatch(sectionID) + if len(matches) == 3 { + return matches[1], matches[2] } - panic("could not find 'Class Section:' in ClassInfo") + + panic("failed to parse course prefix and number") } diff --git a/parser/courseParser_test.go b/parser/courseParser_test.go index a72ede8..e9b42d0 100644 --- a/parser/courseParser_test.go +++ b/parser/courseParser_test.go @@ -9,7 +9,7 @@ import ( "github.com/UTDNebula/nebula-api/api/schema" ) -// TestGetCourse checks course parsing from HTML fixtures. +// Test get course func TestGetCourse(t *testing.T) { t.Parallel() @@ -19,17 +19,28 @@ func TestGetCourse(t *testing.T) { output := *getCourse(courseNum, testCase.Section.Academic_session, testCase.RowInfo, testCase.ClassInfo) expected := testCase.Course - diff := cmp.Diff(expected, output, cmpopts.IgnoreFields(schema.Course{}, "Id", "Sections", "Enrollment_reqs", "Prerequisites")) + diff := cmp.Diff( + expected, + output, + cmpopts.IgnoreFields( + schema.Course{}, + "Id", + "Sections", + "Enrollment_reqs", + "Prerequisites", + "Corequisites", + "Co_or_pre_requisites", + ), + ) if diff != "" { - t.Errorf("Failed (-expected +got)\n %s", diff) + t.Errorf("Failed (-expected +got)\n%s", diff) } - }) } } -// TestGetCatalogYear ensures catalog year derivation matches expected academic sessions. +// Test get catalog year func TestGetCatalogYear(t *testing.T) { t.Parallel() @@ -81,7 +92,6 @@ func TestGetCatalogYear(t *testing.T) { } }() - // only call if we *expect* it to succeed output := getCatalogYear(testCase.Session) if !testCase.Panic && output != testCase.Expected { t.Errorf("expected %q, got %q", testCase.Expected, output) @@ -90,7 +100,7 @@ func TestGetCatalogYear(t *testing.T) { } } -// TestGetPrefixAndCourseNum verifies extraction of subject prefixes and course numbers. +// Test get prefix and course num func TestGetPrefixAndCourseNum(t *testing.T) { t.Parallel() diff --git a/parser/sectionParser.go b/parser/sectionParser.go index 58ab0cd..fb439be 100644 --- a/parser/sectionParser.go +++ b/parser/sectionParser.go @@ -22,7 +22,7 @@ var ( // coreRegexp matches any 3-digit number, used for core curriculum codes (e.g., "090"). coreRegexp = regexp.MustCompile(`[0-9]{3}`) - // personRegexp matches any 3 strings (no spaces) seperated by '・', (e.g, Name・Role・Email) + // personRegexp matches any 3 strings (no spaces) separated by '・', (e.g, Name・Role・Email) personRegexp = regexp.MustCompile(`(.+)・(.+)・(.+)`) // meetingDatesRegexp matches a full date in "Month Day, Year" format (e.g., "January 5, 2022") @@ -35,7 +35,7 @@ var ( meetingTimesRegexp = regexp.MustCompile(utils.R_TIME_AM_PM) ) -// parseSection creates a schema.Section from rowInfo and ClassInfo, +// parseSection creates a schema.Section from rowInfo and classInfo, // adds it to Sections, and updates the associated Course and Professors. // Internally calls parseCourse and parseProfessors, which modify global maps. func parseSection(rowInfo map[string]*goquery.Selection, classInfo map[string]string) { @@ -46,16 +46,18 @@ func parseSection(rowInfo map[string]*goquery.Selection, classInfo map[string]st id := primitive.NewObjectID() - // Build compound keys courseKey := schema.CourseKey{ Subject_prefix: courseRef.Subject_prefix, Course_number: courseRef.Course_number, Catalog_year: courseRef.Catalog_year, } - courseSectionKey := schema.CourseSectionKey{ + sectionKey := schema.SectionKey{ + Subject_prefix: courseRef.Subject_prefix, + Course_number: courseRef.Course_number, + Catalog_year: courseRef.Catalog_year, Section_number: sectionNumber, - Term: session.Name, + Term: session.Name, } profSectionKey := schema.ProfSectionKey{ @@ -84,14 +86,14 @@ func parseSection(rowInfo map[string]*goquery.Selection, classInfo map[string]st Sections[section.Id] = §ion // Append new section to course's section listing - courseRef.Sections = append(courseRef.Sections, courseSectionKey) + courseRef.Sections = append(courseRef.Sections, sectionKey) } -// getInternalClassAndCourseNum returns a sections internal course and class number, +// getInternalClassAndCourseNum returns a section's internal class number and internal course number, // both 0-padded, 5-digit numbers as strings. -// It expects ClassInfo to contain "Class/Course Number:" key. +// It expects classInfo to contain "Class/Course Number:" key. // If the key is not found or the value is not in the expected "classNum / courseNum" format, -// it returns empty strings. +// it panics. func getInternalClassAndCourseNum(classInfo map[string]string) (string, string) { if numbers, ok := classInfo["Class/Course Number:"]; ok { classAndCourseNum := strings.Split(numbers, " / ") @@ -100,7 +102,7 @@ func getInternalClassAndCourseNum(classInfo map[string]string) (string, string) } panic("failed to parse internal class number and course number") } - panic("could not find 'Class/Course Number:' in ClassInfo") + panic("could not find 'Class/Course Number:' in classInfo") } // getAcademicSession returns the schema.AcademicSession parsed from the provided rowInfo. @@ -111,7 +113,7 @@ func getAcademicSession(rowInfo map[string]*goquery.Selection) schema.AcademicSe infoNodes := rowInfo["Schedule:"].FindMatcher(goquery.Single("p.courseinfo__sectionterm")).Contents().Nodes for _, node := range infoNodes { if node.DataAtom == atom.B { - //since the key is not a TextElement, the Text is stored in its first child, a TextElement + // since the key is not a TextElement, the text is stored in its first child, a TextElement key := utils.TrimWhitespace(node.FirstChild.Data) value := utils.TrimWhitespace(node.NextSibling.Data) @@ -127,25 +129,24 @@ func getAcademicSession(rowInfo map[string]*goquery.Selection) schema.AcademicSe } if session.Name == "" { - panic("failed to find academic session, session name can not be empty") + panic("failed to find academic session, session name cannot be empty") } return session } // getSectionNumber returns the matched value from a sectionPrefixRegexp on -// `ClassInfo["Class Section:"]`. It expects ClassInfo to contain "Class Section:" key. -// If there is no matches, getSectionNumber will panic as sectionNumber is a required -// field. +// classInfo["Class Section:"]. It expects classInfo to contain "Class Section:" key. +// If there are no matches, getSectionNumber will panic since sectionNumber is a required field. func getSectionNumber(classInfo map[string]string) string { - if syllabus, ok := classInfo["Class Section:"]; ok { - matches := sectionPrefixRegexp.FindStringSubmatch(syllabus) + if classSection, ok := classInfo["Class Section:"]; ok { + matches := sectionPrefixRegexp.FindStringSubmatch(classSection) if len(matches) == 2 { return matches[1] } panic("failed to parse section number") } - panic("could not find 'Class Section:' in ClassInfo") + panic("could not find 'Class Section:' in classInfo") } // getTeachingAssistants parses TA/RA information from rowInfo and returns a list of schema.Assistant. @@ -156,6 +157,7 @@ func getTeachingAssistants(rowInfo map[string]*goquery.Selection) []schema.Assis if !ok { return []schema.Assistant{} } + assistantMatches := personRegexp.FindAllStringSubmatch(utils.TrimWhitespace(taRow.Text()), -1) assistants := make([]schema.Assistant, 0, len(assistantMatches)) @@ -170,11 +172,12 @@ func getTeachingAssistants(rowInfo map[string]*goquery.Selection) []schema.Assis } assistants = append(assistants, assistant) } + return assistants } -// getInstructionMode returns the instruction mode (e.g., in-person, online) from ClassInfo. -// It expects ClassInfo to contain "Instruction Mode:" key. +// getInstructionMode returns the instruction mode (e.g., in-person, online) from classInfo. +// It expects classInfo to contain "Instruction Mode:" key. // If the key is not present, it returns an empty string. func getInstructionMode(classInfo map[string]string) string { if mode, ok := classInfo["Instruction Mode:"]; ok { @@ -189,7 +192,7 @@ func getInstructionMode(classInfo map[string]string) string { // each meeting. Therefore, both an empty slice or a slice containing a meeting // where all its values are empty are perfectly valid. // -// Each meeting is parsed as following: +// Each meeting is parsed as follows: // // Start and End Date // - Accepts 0, 1 or 2 dates matched using meetingDatesRegexp. @@ -209,7 +212,7 @@ func getInstructionMode(classInfo map[string]string) string { // - Skips locations whose text don't match format func getMeetings(rowInfo map[string]*goquery.Selection) []schema.Meeting { meetingItems := rowInfo["Schedule:"].Find("div.courseinfo__meeting-item--multiple") - var meetings = make([]schema.Meeting, 0, meetingItems.Length()) + meetings := make([]schema.Meeting, 0, meetingItems.Length()) meetingItems.Each(func(i int, s *goquery.Selection) { meeting := schema.Meeting{} @@ -228,7 +231,7 @@ func getMeetings(rowInfo map[string]*goquery.Selection) []schema.Meeting { if days != nil { meeting.Meeting_days = days } else { - meeting.Meeting_days = []string{} //avoid null in the json + meeting.Meeting_days = []string{} } times := meetingTimesRegexp.FindAllString(meetingInfo.Text(), -1) @@ -241,23 +244,25 @@ func getMeetings(rowInfo map[string]*goquery.Selection) []schema.Meeting { } if locationInfo := meetingInfo.FindMatcher(goquery.Single("a")); locationInfo != nil { - mapUri := locationInfo.AttrOr("href", "") + mapURI := locationInfo.AttrOr("href", "") - //only add locations for meetings that have actual data, all meetings have a link some are not visible or empty - if mapUri != "" && mapUri != "https://locator.utdallas.edu/" && mapUri != "https://locator.utdallas.edu/ONLINE" { + // only add locations for meetings that have actual data; all meetings have a link but some are not visible or empty + if mapURI != "" && mapURI != "https://locator.utdallas.edu/" && mapURI != "https://locator.utdallas.edu/ONLINE" { splitText := strings.Split(utils.TrimWhitespace(locationInfo.Text()), " ") if len(splitText) == 2 { meeting.Location = schema.Location{ Building: splitText[0], Room: splitText[1], - Map_uri: mapUri, + Map_uri: mapURI, } } } } + meetings = append(meetings, meeting) }) + return meetings } @@ -268,7 +273,6 @@ func getMeetings(rowInfo map[string]*goquery.Selection) []schema.Meeting { func getCoreFlags(rowInfo map[string]*goquery.Selection) []string { if core, ok := rowInfo["Core:"]; ok { flags := coreRegexp.FindAllString(utils.TrimWhitespace(core.Text()), -1) - if flags != nil { return flags } @@ -290,18 +294,18 @@ func getSyllabusUri(rowInfo map[string]*goquery.Selection) string { } // getGradeDistribution returns the grade distribution for the given section. -// It retrieves grade distribution from the global `GradeMap`. +// It retrieves grade distribution from the global GradeMap. // // If GradeMap contains the resulting key it will return the specified slice, -// otherwise it will return an empty slice, `[]int{}`. +// otherwise it will return an empty slice, []int{}. // The key is generated using the following formula: -// key = SubjectPrefix + InternalCourseNumber + InternalSectionNumber. -// Note that the InternalSectionNumber is trimmed of leading '0's +// key = SubjectPrefix + CourseNumber + InternalSectionNumberTrimmed +// Note that the section number is trimmed of leading '0's. func getGradeDistribution(session schema.AcademicSession, sectionNumber string, courseRef *schema.Course) []int { if semesterGrades, ok := GradeMap[session.Name]; ok { // We have to trim leading zeroes from the section number in order to match properly, since the grade data does not use leading zeroes trimmedSectionNumber := strings.TrimLeft(sectionNumber, "0") - // Key into grademap should be uppercased like the grade data + // Key into GradeMap should be uppercased like the grade data gradeKey := strings.ToUpper(courseRef.Subject_prefix + courseRef.Course_number + trimmedSectionNumber) sectionGrades, exists := semesterGrades[gradeKey] if exists { @@ -311,7 +315,7 @@ func getGradeDistribution(session schema.AcademicSession, sectionNumber string, return []int{} } -// parseTimeOrPanic is a simplified version time.ParseInLocation. The layout and +// parseTimeOrPanic is a simplified version of time.ParseInLocation. The layout and // location are constants, timeLayout and timeLocation respectively. If time.ParseInLocation // returns an error, parseTimeOrPanic will panic regardless of the error type. func parseTimeOrPanic(value string) time.Time { diff --git a/parser/sectionParser_test.go b/parser/sectionParser_test.go index a920b4d..12fce79 100644 --- a/parser/sectionParser_test.go +++ b/parser/sectionParser_test.go @@ -8,7 +8,7 @@ import ( "github.com/google/go-cmp/cmp" ) -// TestGetInternalClassAndCourseNum checks parsing of internal course identifiers. +// Test get internal class and course num func TestGetInternalClassAndCourseNum(t *testing.T) { t.Parallel() @@ -25,9 +25,8 @@ func TestGetInternalClassAndCourseNum(t *testing.T) { } if courseNum != expectedCourseNumber { - t.Errorf("Class Number: expected %s got %s", expectedCourseNumber, courseNum) + t.Errorf("Course Number: expected %s got %s", expectedCourseNumber, courseNum) } - }) } @@ -51,16 +50,16 @@ func TestGetInternalClassAndCourseNum(t *testing.T) { defer func() { if r := recover(); r == nil { - t.Errorf("expected panic for input %s but none occurred", fail) + t.Errorf("expected panic for input %v but none occurred", fail) } }() - getInternalClassAndCourseNum(fail) + getInternalClassAndCourseNum(fail) }) } } -// TestGetAcademicSession ensures term metadata is parsed correctly. +// Test get academic session func TestGetAcademicSession(t *testing.T) { t.Parallel() @@ -80,7 +79,7 @@ func TestGetAcademicSession(t *testing.T) { } } -// TestGetSectionNumber validates extraction of section numbers. +// Test get section number func TestGetSectionNumber(t *testing.T) { t.Parallel() @@ -117,16 +116,16 @@ func TestGetSectionNumber(t *testing.T) { defer func() { if r := recover(); r == nil { - t.Errorf("expected panic for input %s but none occurred", fail) + t.Errorf("expected panic for input %v but none occurred", fail) } }() - getSectionNumber(fail) + getSectionNumber(fail) }) - } } +// Test get teaching assistants func TestGetTeachingAssistants(t *testing.T) { t.Parallel() @@ -146,6 +145,7 @@ func TestGetTeachingAssistants(t *testing.T) { } } +// Test get instruction mode func TestGetInstructionMode(t *testing.T) { t.Parallel() @@ -160,10 +160,10 @@ func TestGetInstructionMode(t *testing.T) { t.Errorf("expected %s got %s", expected, output) } }) - } } +// Test get meetings func TestGetMeetings(t *testing.T) { t.Parallel() @@ -183,6 +183,7 @@ func TestGetMeetings(t *testing.T) { } } +// Test get core flags func TestGetCoreFlags(t *testing.T) { t.Parallel() @@ -202,12 +203,14 @@ func TestGetCoreFlags(t *testing.T) { } } +// Test get syllabus uri func TestGetSyllabusUri(t *testing.T) { t.Parallel() for name, testCase := range testData { t.Run(name, func(t *testing.T) { t.Parallel() + output := getSyllabusUri(testCase.RowInfo) expected := testCase.Section.Syllabus_uri @@ -215,10 +218,10 @@ func TestGetSyllabusUri(t *testing.T) { t.Errorf("expected %s got %s", expected, output) } }) - } } +// Test parse time or panic func TestParseTimeOrPanic(t *testing.T) { t.Parallel() @@ -238,15 +241,15 @@ func TestParseTimeOrPanic(t *testing.T) { Panic: false, }, "Case_003": { - Input: "15 March, 2020", // wrong format + Input: "15 March, 2020", Panic: true, }, "Case_004": { - Input: "Not a date", // clearly wrong + Input: "Not a date", Panic: true, }, "Case_005": { - Input: "", // empty input + Input: "", Panic: true, }, } diff --git a/parser/testdata/case_000/course.json b/parser/testdata/case_000/course.json index e401ae5..34dc589 100644 --- a/parser/testdata/case_000/course.json +++ b/parser/testdata/case_000/course.json @@ -1,5 +1,5 @@ { - "_id": "67d07ee0c972c18731e23bd7", + "_id": "69ceeb3ec5bf079cd1334b19", "subject_prefix": "ACCT", "course_number": "2301", "title": "Introductory Financial Accounting", @@ -16,6 +16,9 @@ "co_or_pre_requisites": null, "sections": [ { + "subject_prefix": "ACCT", + "course_number": "2301", + "catalog_year": "24", "section_number": "003", "term": "25S" } diff --git a/parser/testdata/case_000/professors.json b/parser/testdata/case_000/professors.json index 3ca3aa2..8a6ad1d 100644 --- a/parser/testdata/case_000/professors.json +++ b/parser/testdata/case_000/professors.json @@ -1,6 +1,6 @@ [ { - "_id": "67d07ee0c972c18731e23bd9", + "_id": "69ceeb3ec5bf079cd1334b1b", "first_name": "Naim Bugra", "last_name": "Ozel", "titles": [ @@ -26,7 +26,7 @@ ] }, { - "_id": "67d07ee0c972c18731e23bda", + "_id": "69ceeb3ec5bf079cd1334b1c", "first_name": "Jieying", "last_name": "Zhang", "titles": [ diff --git a/parser/testdata/case_000/section.json b/parser/testdata/case_000/section.json index a9a3c4b..3e99265 100644 --- a/parser/testdata/case_000/section.json +++ b/parser/testdata/case_000/section.json @@ -1,5 +1,5 @@ { - "_id": "67d07ee0c972c18731e23bd8", + "_id": "69ceeb3ec5bf079cd1334b1a", "section_number": "003", "course_key": { "subject_prefix": "ACCT", diff --git a/parser/testdata/case_001/course.json b/parser/testdata/case_001/course.json index 22cdf0a..548825c 100644 --- a/parser/testdata/case_001/course.json +++ b/parser/testdata/case_001/course.json @@ -1,5 +1,5 @@ { - "_id": "67d07ee0c972c18731e23bdb", + "_id": "69ceeb3ec5bf079cd1334b1d", "subject_prefix": "ACCT", "course_number": "2301", "title": "Introductory Financial Accounting", @@ -16,6 +16,9 @@ "co_or_pre_requisites": null, "sections": [ { + "subject_prefix": "ACCT", + "course_number": "2301", + "catalog_year": "24", "section_number": "001", "term": "25S" } diff --git a/parser/testdata/case_001/professors.json b/parser/testdata/case_001/professors.json index a3dcd36..510cbb9 100644 --- a/parser/testdata/case_001/professors.json +++ b/parser/testdata/case_001/professors.json @@ -1,6 +1,6 @@ [ { - "_id": "67d07ee0c972c18731e23bdd", + "_id": "69ceeb3ec5bf079cd1334b1f", "first_name": "Jieying", "last_name": "Zhang", "titles": [ @@ -26,7 +26,7 @@ ] }, { - "_id": "67d07ee0c972c18731e23bde", + "_id": "69ceeb3ec5bf079cd1334b20", "first_name": "Naim Bugra", "last_name": "Ozel", "titles": [ diff --git a/parser/testdata/case_001/section.json b/parser/testdata/case_001/section.json index 2dcdec2..bab5234 100644 --- a/parser/testdata/case_001/section.json +++ b/parser/testdata/case_001/section.json @@ -1,5 +1,5 @@ { - "_id": "67d07ee0c972c18731e23bdc", + "_id": "69ceeb3ec5bf079cd1334b1e", "section_number": "001", "course_key": { "subject_prefix": "ACCT", diff --git a/parser/testdata/case_002/course.json b/parser/testdata/case_002/course.json index 47e9cec..4dc2961 100644 --- a/parser/testdata/case_002/course.json +++ b/parser/testdata/case_002/course.json @@ -1,5 +1,5 @@ { - "_id": "67d07ee0c972c18731e23bdf", + "_id": "69ceeb3ec5bf079cd1334b21", "subject_prefix": "BA", "course_number": "1320", "title": "Business in a Global World", @@ -16,6 +16,9 @@ "co_or_pre_requisites": null, "sections": [ { + "subject_prefix": "BA", + "course_number": "1320", + "catalog_year": "24", "section_number": "501", "term": "25S" } diff --git a/parser/testdata/case_002/professors.json b/parser/testdata/case_002/professors.json index 31aae13..ba4ade6 100644 --- a/parser/testdata/case_002/professors.json +++ b/parser/testdata/case_002/professors.json @@ -1,6 +1,6 @@ [ { - "_id": "67d07ee0c972c18731e23be1", + "_id": "69ceeb3ec5bf079cd1334b23", "first_name": "Peter", "last_name": "Lewin", "titles": [ diff --git a/parser/testdata/case_002/section.json b/parser/testdata/case_002/section.json index 1ecfbfd..2433b53 100644 --- a/parser/testdata/case_002/section.json +++ b/parser/testdata/case_002/section.json @@ -1,5 +1,5 @@ { - "_id": "67d07ee0c972c18731e23be0", + "_id": "69ceeb3ec5bf079cd1334b22", "section_number": "501", "course_key": { "subject_prefix": "BA", diff --git a/parser/testdata/case_003/course.json b/parser/testdata/case_003/course.json index 509f336..31664c0 100644 --- a/parser/testdata/case_003/course.json +++ b/parser/testdata/case_003/course.json @@ -1,5 +1,5 @@ { - "_id": "67d07ee0c972c18731e23be2", + "_id": "69ceeb3ec5bf079cd1334b24", "subject_prefix": "BIOL", "course_number": "6111", "title": "Graduate Research Presentation", @@ -16,6 +16,9 @@ "co_or_pre_requisites": null, "sections": [ { + "subject_prefix": "BIOL", + "course_number": "6111", + "catalog_year": "24", "section_number": "016", "term": "25S" } diff --git a/parser/testdata/case_003/professors.json b/parser/testdata/case_003/professors.json index f68298c..817ad11 100644 --- a/parser/testdata/case_003/professors.json +++ b/parser/testdata/case_003/professors.json @@ -1,6 +1,6 @@ [ { - "_id": "67d07ee0c972c18731e23be4", + "_id": "69ceeb3ec5bf079cd1334b26", "first_name": "Tian", "last_name": "Hong", "titles": [ diff --git a/parser/testdata/case_003/section.json b/parser/testdata/case_003/section.json index deacfc1..b0a59a8 100644 --- a/parser/testdata/case_003/section.json +++ b/parser/testdata/case_003/section.json @@ -1,5 +1,5 @@ { - "_id": "67d07ee0c972c18731e23be3", + "_id": "69ceeb3ec5bf079cd1334b25", "section_number": "016", "course_key": { "subject_prefix": "BIOL", diff --git a/parser/testdata/case_004/course.json b/parser/testdata/case_004/course.json index 491f7f5..c617298 100644 --- a/parser/testdata/case_004/course.json +++ b/parser/testdata/case_004/course.json @@ -1,5 +1,5 @@ { - "_id": "67d07ee0c972c18731e23be5", + "_id": "69ceeb3ec5bf079cd1334b27", "subject_prefix": "AERO", "course_number": "3320", "title": "- Recitation", @@ -16,6 +16,9 @@ "co_or_pre_requisites": null, "sections": [ { + "subject_prefix": "AERO", + "course_number": "3320", + "catalog_year": "24", "section_number": "201", "term": "25S" } diff --git a/parser/testdata/case_004/section.json b/parser/testdata/case_004/section.json index f68422f..fa92946 100644 --- a/parser/testdata/case_004/section.json +++ b/parser/testdata/case_004/section.json @@ -1,5 +1,5 @@ { - "_id": "67d07ee0c972c18731e23be6", + "_id": "69ceeb3ec5bf079cd1334b28", "section_number": "201", "course_key": { "subject_prefix": "AERO", diff --git a/parser/testdata/case_005/course.json b/parser/testdata/case_005/course.json index 6a83d19..53f5b88 100644 --- a/parser/testdata/case_005/course.json +++ b/parser/testdata/case_005/course.json @@ -1,5 +1,5 @@ { - "_id": "67d07ee0c972c18731e23be7", + "_id": "69ceeb3ec5bf079cd1334b29", "subject_prefix": "AERO", "course_number": "4320", "title": "- Laboratory", @@ -16,6 +16,9 @@ "co_or_pre_requisites": null, "sections": [ { + "subject_prefix": "AERO", + "course_number": "4320", + "catalog_year": "24", "section_number": "002", "term": "25S" } diff --git a/parser/testdata/case_005/section.json b/parser/testdata/case_005/section.json index f3e5db8..9a3ada3 100644 --- a/parser/testdata/case_005/section.json +++ b/parser/testdata/case_005/section.json @@ -1,5 +1,5 @@ { - "_id": "67d07ee0c972c18731e23be8", + "_id": "69ceeb3ec5bf079cd1334b2a", "section_number": "002", "course_key": { "subject_prefix": "AERO", diff --git a/parser/testdata/courses.json b/parser/testdata/courses.json index 954e3ea..7b73e07 100644 --- a/parser/testdata/courses.json +++ b/parser/testdata/courses.json @@ -17,6 +17,9 @@ "co_or_pre_requisites": null, "sections": [ { + "subject_prefix": "BA", + "course_number": "1320", + "catalog_year": "24", "section_number": "501", "term": "25S" } @@ -45,6 +48,9 @@ "co_or_pre_requisites": null, "sections": [ { + "subject_prefix": "BIOL", + "course_number": "6111", + "catalog_year": "24", "section_number": "016", "term": "25S" } @@ -73,6 +79,9 @@ "co_or_pre_requisites": null, "sections": [ { + "subject_prefix": "AERO", + "course_number": "3320", + "catalog_year": "24", "section_number": "201", "term": "25S" } @@ -101,6 +110,9 @@ "co_or_pre_requisites": null, "sections": [ { + "subject_prefix": "AERO", + "course_number": "4320", + "catalog_year": "24", "section_number": "002", "term": "25S" } @@ -129,10 +141,16 @@ "co_or_pre_requisites": null, "sections": [ { + "subject_prefix": "ACCT", + "course_number": "2301", + "catalog_year": "24", "section_number": "003", "term": "25S" }, { + "subject_prefix": "ACCT", + "course_number": "2301", + "catalog_year": "24", "section_number": "001", "term": "25S" } diff --git a/parser/testdata/professors.json b/parser/testdata/professors.json index 60fc1d2..f100fe8 100644 --- a/parser/testdata/professors.json +++ b/parser/testdata/professors.json @@ -1,12 +1,12 @@ [ { - "_id": "67d07ee0c972c18731e23beb", - "first_name": "Naim Bugra", - "last_name": "Ozel", + "_id": "69ceeb3ec5bf079cd1334b2e", + "first_name": "Jieying", + "last_name": "Zhang", "titles": [ "Primary Instructor (50%)" ], - "email": "nbo150030@utdallas.edu", + "email": "jxz146230@utdallas.edu", "phone_number": "", "office": { "building": "", @@ -32,13 +32,13 @@ ] }, { - "_id": "67d07ee0c972c18731e23bec", - "first_name": "Jieying", - "last_name": "Zhang", + "_id": "69ceeb3ec5bf079cd1334b32", + "first_name": "Peter", + "last_name": "Lewin", "titles": [ - "Primary Instructor (50%)" + "Primary Instructor" ], - "email": "jxz146230@utdallas.edu", + "email": "plewin@utdallas.edu", "phone_number": "", "office": { "building": "", @@ -50,27 +50,21 @@ "office_hours": null, "sections": [ { - "subject_prefix": "ACCT", - "course_number": "2301", - "section_number": "003", - "term": "25S" - }, - { - "subject_prefix": "ACCT", - "course_number": "2301", - "section_number": "001", + "subject_prefix": "BA", + "course_number": "1320", + "section_number": "501", "term": "25S" } ] }, { - "_id": "67d07ee0c972c18731e23bf0", - "first_name": "Peter", - "last_name": "Lewin", + "_id": "69ceeb3ec5bf079cd1334b35", + "first_name": "Tian", + "last_name": "Hong", "titles": [ "Primary Instructor" ], - "email": "plewin@utdallas.edu", + "email": "txh240018@utdallas.edu", "phone_number": "", "office": { "building": "", @@ -82,21 +76,21 @@ "office_hours": null, "sections": [ { - "subject_prefix": "BA", - "course_number": "1320", - "section_number": "501", + "subject_prefix": "BIOL", + "course_number": "6111", + "section_number": "016", "term": "25S" } ] }, { - "_id": "67d07ee0c972c18731e23bf3", - "first_name": "Tian", - "last_name": "Hong", + "_id": "69ceeb3ec5bf079cd1334b2d", + "first_name": "Naim Bugra", + "last_name": "Ozel", "titles": [ - "Primary Instructor" + "Primary Instructor (50%)" ], - "email": "txh240018@utdallas.edu", + "email": "nbo150030@utdallas.edu", "phone_number": "", "office": { "building": "", @@ -108,9 +102,15 @@ "office_hours": null, "sections": [ { - "subject_prefix": "BIOL", - "course_number": "6111", - "section_number": "016", + "subject_prefix": "ACCT", + "course_number": "2301", + "section_number": "003", + "term": "25S" + }, + { + "subject_prefix": "ACCT", + "course_number": "2301", + "section_number": "001", "term": "25S" } ] diff --git a/parser/validator.go b/parser/validator.go index dc03561..16953a9 100644 --- a/parser/validator.go +++ b/parser/validator.go @@ -18,16 +18,19 @@ func validate() { }() // Build maps for quick lookup by key - courseSectionsByKey := make(map[schema.CourseKey]map[schema.CourseSectionKey]*schema.Section) + courseSectionsByKey := make(map[schema.CourseKey]map[schema.SectionKey]*schema.Section) for _, section := range Sections { courseKey := section.Course if courseSectionsByKey[courseKey] == nil { - courseSectionsByKey[courseKey] = make(map[schema.CourseSectionKey]*schema.Section) + courseSectionsByKey[courseKey] = make(map[schema.SectionKey]*schema.Section) } - sectionKey := schema.CourseSectionKey{ + sectionKey := schema.SectionKey{ + Subject_prefix: section.Course.Subject_prefix, + Course_number: section.Course.Course_number, + Catalog_year: section.Course.Catalog_year, Section_number: section.Section_number, - Term: section.Academic_session.Name, + Term: section.Academic_session.Name, } courseSectionsByKey[courseKey][sectionKey] = section } @@ -36,8 +39,8 @@ func validate() { for _, course := range Courses { courseKey := schema.CourseKey{ Subject_prefix: course.Subject_prefix, - Course_number: course.Course_number, - Catalog_year: course.Catalog_year, + Course_number: course.Course_number, + Catalog_year: course.Catalog_year, } courseByKey[courseKey] = course } @@ -46,7 +49,7 @@ func validate() { for _, professor := range Professors { professorKey := schema.ProfessorKey{ First_name: professor.First_name, - Last_name: professor.Last_name, + Last_name: professor.Last_name, } profByKey[professorKey] = professor } @@ -55,11 +58,13 @@ func validate() { courseKeys := utils.GetMapKeys(Courses) for i := range len(courseKeys) { course1 := Courses[courseKeys[i]] + // Check for duplicate courses by comparing course_number, subject_prefix, and catalog_year as a compound key for j := i + 1; j < len(courseKeys); j++ { course2 := Courses[courseKeys[j]] valDuplicateCourses(course1, course2) } + // Make sure course isn't referencing any nonexistent sections, and that course-section references are consistent both ways valCourseReference(course1, courseSectionsByKey) } @@ -70,15 +75,17 @@ func validate() { sectionKeys := utils.GetMapKeys(Sections) for i := range len(sectionKeys) { section1 := Sections[sectionKeys[i]] + // Check for duplicate sections by comparing section_number, course_reference, and academic_session as a compound key for j := i + 1; j < len(sectionKeys); j++ { section2 := Sections[sectionKeys[j]] valDuplicateSections(section1, section2) } + // Make sure section isn't referencing any nonexistent professors, and that section-professor references are consistent both ways valSectionReferenceProf(section1, profByKey) - // Make sure section isn't referencing a nonexistant course + // Make sure section isn't referencing a nonexistent course valSectionReferenceCourse(section1, courseByKey) } sectionKeys = nil @@ -86,7 +93,8 @@ func validate() { log.Printf("Validating professors...") profKeys := utils.GetMapKeys(Professors) - // Check for duplicate professors by comparing first_name, last_name, and sections as a compound key + + // Check for duplicate professors by comparing first_name, last_name, and profile_uri as a compound key for i := range len(profKeys) { prof1 := Professors[profKeys[i]] for j := i + 1; j < len(profKeys); j++ { @@ -109,38 +117,54 @@ func valDuplicateCourses(course1 *schema.Course, course2 *schema.Course) { } // Validate course reference to sections -func valCourseReference(course *schema.Course, courseSections map[schema.CourseKey]map[schema.CourseSectionKey]*schema.Section) { +func valCourseReference(course *schema.Course, courseSections map[schema.CourseKey]map[schema.SectionKey]*schema.Section) { courseKey := schema.CourseKey{ Subject_prefix: course.Subject_prefix, - Course_number: course.Course_number, - Catalog_year: course.Catalog_year, + Course_number: course.Course_number, + Catalog_year: course.Catalog_year, } - if sections, sectionsExist := courseSections[courseKey]; sectionsExist { - for _, sectionKey := range course.Sections { - section, exists := sections[sectionKey] - // validate if course references to some section not in the parsed sections - if !exists { - log.Printf("Nonexistent section reference found for %s%s!", course.Subject_prefix, course.Course_number) - log.Printf("Referenced section key: %s\nCourse key: %s", sectionKey, courseKey) - log.Panic("Courses failed to validate!") - } - - // validate if the ref sections references back to the course - if section.Course != courseKey { - log.Printf("Inconsistent section reference found for %s%s! The course references the section, but not vice-versa!", course.Subject_prefix, course.Course_number) - log.Printf("Referenced CourseSection key: %+v\nCourse key: %+v\nSection's course key: %+v", sectionKey, courseKey, section.Course) - log.Panic("Courses failed to validate!") - } + sections := courseSections[courseKey] + + for _, sectionKey := range course.Sections { + section, exists := sections[sectionKey] + + // validate if course references some section not in the parsed sections + if !exists { + log.Printf("Nonexistent section reference found for %s%s!", course.Subject_prefix, course.Course_number) + log.Printf("Referenced section key: %+v\nCourse key: %+v", sectionKey, courseKey) + log.Panic("Courses failed to validate!") + } + + // validate if the referenced section points back to the same course + if section.Course != courseKey { + log.Printf("Inconsistent section reference found for %s%s! The course references the section, but not vice-versa!", course.Subject_prefix, course.Course_number) + log.Printf("Referenced section key: %+v\nCourse key: %+v\nSection's course key: %+v", sectionKey, courseKey, section.Course) + log.Panic("Courses failed to validate!") + } + + // validate if the referenced section's own full compound key matches the key stored on the course + expectedSectionKey := schema.SectionKey{ + Subject_prefix: section.Course.Subject_prefix, + Course_number: section.Course.Course_number, + Catalog_year: section.Course.Catalog_year, + Section_number: section.Section_number, + Term: section.Academic_session.Name, + } + + if expectedSectionKey != sectionKey { + log.Printf("Mismatched section key found for %s%s!", course.Subject_prefix, course.Course_number) + log.Printf("Course stored section key: %+v\nActual section key: %+v", sectionKey, expectedSectionKey) + log.Panic("Courses failed to validate!") } } } // Validate if the sections are duplicate func valDuplicateSections(section1 *schema.Section, section2 *schema.Section) { - if section1.Section_number == section2.Section_number && - section1.Course == section2.Course && - section1.Academic_session == section2.Academic_session { + if section1.Section_number == section2.Section_number && + section1.Course == section2.Course && + section1.Academic_session == section2.Academic_session { log.Print("Duplicate section found!") log.Printf("Section 1: %v\n\nSection 2: %v", section1, section2) log.Panic("Sections failed to validate!") @@ -152,23 +176,24 @@ func valSectionReferenceProf(section *schema.Section, profs map[schema.Professor for _, profKey := range section.Professors { profSectionKey := schema.ProfSectionKey{ Subject_prefix: section.Course.Subject_prefix, - Course_number: section.Course.Course_number, + Course_number: section.Course.Course_number, Section_number: section.Section_number, - Term: section.Academic_session.Name, + Term: section.Academic_session.Name, } professor, exists := profs[profKey] - // validate if the section references to some prof not in the parsed professors + + // validate if the section references some professor not in the parsed professors if !exists { - log.Printf("Nonexistent professor reference found for section ID %s!", section.Id) - log.Printf("Referenced professor key: %v", profKey) + log.Printf("Nonexistent professor reference found for section ID %s!", section.Id.Hex()) + log.Printf("Referenced professor key: %+v", profKey) log.Panic("Sections failed to validate!") } // validate if the referenced professor references back to section if !slices.Contains(professor.Sections, profSectionKey) { - log.Printf("Inconsistent professor reference found for section ID %s! The section references the professor, but not vice-versa!", section.Id) - log.Printf("Referenced professor key: %v", profKey) + log.Printf("Inconsistent professor reference found for section ID %s! The section references the professor, but not vice-versa!", section.Id.Hex()) + log.Printf("Referenced professor key: %+v", profKey) log.Panic("Sections failed to validate!") } } @@ -177,9 +202,10 @@ func valSectionReferenceProf(section *schema.Section, profs map[schema.Professor // Validate section reference to course func valSectionReferenceCourse(section *schema.Section, coursesByKey map[schema.CourseKey]*schema.Course) { _, exists := coursesByKey[section.Course] - // validate if section reference some course not in parsed courses + + // validate if section references some course not in parsed courses if !exists { - log.Printf("Nonexistent course reference found for section ID %s!", section.Id) + log.Printf("Nonexistent course reference found for section ID %s!", section.Id.Hex()) log.Printf("Referenced course key: %+v", section.Course) log.Panic("Sections failed to validate!") } @@ -187,9 +213,9 @@ func valSectionReferenceCourse(section *schema.Section, coursesByKey map[schema. // Validate if the professors are duplicate func valDuplicateProfs(prof1 *schema.Professor, prof2 *schema.Professor) { - if prof1.First_name == prof2.First_name && - prof1.Last_name == prof2.Last_name && - prof1.Profile_uri == prof2.Profile_uri { + if prof1.First_name == prof2.First_name && + prof1.Last_name == prof2.Last_name && + prof1.Profile_uri == prof2.Profile_uri { log.Printf("Duplicate professor found!") log.Printf("Professor 1: %v\n\nProfessor 2: %v", prof1, prof2) log.Panic("Professors failed to validate!") diff --git a/parser/validator_test.go b/parser/validator_test.go index ddf8c95..16e4283 100644 --- a/parser/validator_test.go +++ b/parser/validator_test.go @@ -56,7 +56,6 @@ func init() { } // Test duplicate courses. Designed for fail cases -// TestDuplicateCoursesFail expects duplicates to trigger validation panic. func TestDuplicateCoursesFail(t *testing.T) { for i := range len(testCourses) { t.Run(fmt.Sprintf("Duplicate course %v", i), func(t *testing.T) { @@ -66,7 +65,6 @@ func TestDuplicateCoursesFail(t *testing.T) { } // Test duplicate sections. Designed for fail cases -// TestDuplicateSectionsFail ensures duplicate sections are rejected. func TestDuplicateSectionsFail(t *testing.T) { for i := range len(testSections) { t.Run(fmt.Sprintf("Duplicate section %v", i), func(t *testing.T) { @@ -76,7 +74,6 @@ func TestDuplicateSectionsFail(t *testing.T) { } // Test duplicate professors . Designed for fail cases -// TestDuplicateProfFail ensures duplicate professors fail validation. func TestDuplicateProfFail(t *testing.T) { for i := range len(testProfessors) { t.Run(fmt.Sprintf("Duplicate professor %v", i), func(t *testing.T) { @@ -86,7 +83,6 @@ func TestDuplicateProfFail(t *testing.T) { } // Test duplicate courses. Designed for pass case -// TestDuplicateCoursesPass confirms unique courses validate successfully. func TestDuplicateCoursesPass(t *testing.T) { for i := range len(testCourses) - 1 { t.Run(fmt.Sprintf("Duplicate courses %v, %v", i, i+1), func(t *testing.T) { @@ -96,7 +92,6 @@ func TestDuplicateCoursesPass(t *testing.T) { } // Test duplicate sections. Designed for pass cases -// TestDuplicateSectionsPass confirms unique sections validate successfully. func TestDuplicateSectionsPass(t *testing.T) { for i := range len(testSections) - 1 { t.Run(fmt.Sprintf("Duplicate sections %v, %v", i, i+1), func(t *testing.T) { @@ -106,7 +101,6 @@ func TestDuplicateSectionsPass(t *testing.T) { } // Test duplicate professors. Designed for pass cases -// TestDuplicateProfPass confirms unique professors validate successfully. func TestDuplicateProfPass(t *testing.T) { for i := range len(testProfessors) - 1 { t.Run(fmt.Sprintf("Duplicate professors %v, %v", i, i+1), func(t *testing.T) { @@ -116,19 +110,21 @@ func TestDuplicateProfPass(t *testing.T) { } // Test if course references to anything nonexistent. Designed for pass case -// TestCourseReferencePass ensures section references to courses succeed. func TestCourseReferencePass(t *testing.T) { - courseSectionMap := make(map[schema.CourseKey]map[schema.CourseSectionKey]*schema.Section) + courseSectionMap := make(map[schema.CourseKey]map[schema.SectionKey]*schema.Section) for _, section := range testSections { courseKey := section.Course if courseSectionMap[courseKey] == nil { - courseSectionMap[courseKey] = make(map[schema.CourseSectionKey]*schema.Section) + courseSectionMap[courseKey] = make(map[schema.SectionKey]*schema.Section) } - sectionKey := schema.CourseSectionKey{ + sectionKey := schema.SectionKey{ + Subject_prefix: section.Course.Subject_prefix, + Course_number: section.Course.Course_number, + Catalog_year: section.Course.Catalog_year, Section_number: section.Section_number, - Term: section.Academic_session.Name, + Term: section.Academic_session.Name, } courseSectionMap[courseKey][sectionKey] = section @@ -159,9 +155,8 @@ func TestCourseReferencePass(t *testing.T) { // 2 types of fail: // - Course references non-existent section // - Section doesn't reference back to same course -// + // This is fail: missing -// TestCourseReferenceFail1 detects missing course references during validation. func TestCourseReferenceFail1(t *testing.T) { for key, value := range sectionCourseMap { t.Run(fmt.Sprintf("Section %v & course %v", key, value), func(t *testing.T) { @@ -171,7 +166,6 @@ func TestCourseReferenceFail1(t *testing.T) { } // This is fail: modified -// TestCourseReferenceFail2 detects mismatched section-course references. func TestCourseReferenceFail2(t *testing.T) { for key, value := range sectionCourseMap { t.Run(fmt.Sprintf("Section %v & course %v", key, value), func(t *testing.T) { @@ -181,15 +175,13 @@ func TestCourseReferenceFail2(t *testing.T) { } // Test section reference to professor, designed for pass case -// TestSectionReferenceProfPass ensures section professor references are mutual. func TestSectionReferenceProfPass(t *testing.T) { - // Build profs maps profs := make(map[schema.ProfessorKey]*schema.Professor) for _, professor := range testProfessors { - profKey := schema.ProfessorKey { + profKey := schema.ProfessorKey{ First_name: professor.First_name, - Last_name: professor.Last_name, + Last_name: professor.Last_name, } profs[profKey] = professor } @@ -212,58 +204,14 @@ func TestSectionReferenceProfPass(t *testing.T) { } } -// Test section reference to professors, designed for fail case -// TestSectionReferenceProfFail catches missing professor back-references. -func TestSectionReferenceProfFail(t *testing.T) { - profs := make(map[schema.ProfessorKey]*schema.Professor) - - for i, professor := range testProfessors { - if i != 0 { - profKey := schema.ProfessorKey { - First_name: professor.First_name, - Last_name: professor.Last_name, - } - profs[profKey] = professor - } - } - - var logBuffer bytes.Buffer - log.SetOutput(&logBuffer) - - defer func() { - logOutput := logBuffer.String() - for _, msg := range []string{ - "Nonexistent professor reference found for section ID ObjectID(\"67d07ee0c972c18731e23bea\")!", - "Referenced professor key: {Naim Bugra Ozel}", - } { - if !strings.Contains(logOutput, msg) { - t.Errorf("The function didn't log correct message. Expected \"%v\"", msg) - } - } - - if r := recover(); r == nil { - t.Errorf("The function didn't panic") - } else { - if r != "Sections failed to validate!" { - t.Errorf("The function panic the wrong message") - } - } - }() - - for _, section := range testSections { - valSectionReferenceProf(section, profs) - } -} - // Test section reference to course -// TestSectionReferenceCourse verifies section-course reference validation. func TestSectionReferenceCourse(t *testing.T) { coursesByKey := make(map[schema.CourseKey]*schema.Course) for _, course := range testCourses { courseKey := schema.CourseKey{ Subject_prefix: course.Subject_prefix, - Course_number: course.Course_number, - Catalog_year: course.Catalog_year, + Course_number: course.Course_number, + Catalog_year: course.Catalog_year, } coursesByKey[courseKey] = course } @@ -286,16 +234,24 @@ func TestSectionReferenceCourse(t *testing.T) { } } +// Test if function log expected msgs when course references section with mismatched compound key +// This is fail: wrong key +func TestCourseReferenceFail3(t *testing.T) { + for key, value := range sectionCourseMap { + t.Run(fmt.Sprintf("Section %v & course %v", key, value), func(t *testing.T) { + testCourseReferenceFail("wrongkey", value, key, t) + }) + } +} + /******** BELOW HERE ARE HELPER FUNCTION FOR TESTS ABOVE ********/ -// Test if validate() throws erros when encountering duplicate -// Design for fail cases +// Test if validate() throws errors when encountering duplicate +// Designed for fail cases func testDuplicateFail(objType string, ix int, t *testing.T) { - // the buffer used to capture the log output var logBuffer bytes.Buffer log.SetOutput(&logBuffer) - // Determine the expected messages and panic messages based on object type var expectedMsgs []string var panicMsg string @@ -303,12 +259,12 @@ func testDuplicateFail(objType string, ix int, t *testing.T) { case "course": failCourse := testCourses[ix] - // list of msgs it must print expectedMsgs = []string{ fmt.Sprintf("Duplicate course found for %s%s!", failCourse.Subject_prefix, failCourse.Course_number), fmt.Sprintf("Course 1: %v\n\nCourse 2: %v", failCourse, failCourse), } panicMsg = "Courses failed to validate!" + case "section": failSection := testSections[ix] @@ -317,6 +273,7 @@ func testDuplicateFail(objType string, ix int, t *testing.T) { fmt.Sprintf("Section 1: %v\n\nSection 2: %v", failSection, failSection), } panicMsg = "Sections failed to validate!" + case "professor": failProf := testProfessors[ix] @@ -328,27 +285,21 @@ func testDuplicateFail(objType string, ix int, t *testing.T) { } defer func() { - logOutput := logBuffer.String() // log output after running the function + logOutput := logBuffer.String() - // Log output needs to contain lines in the list for _, msg := range expectedMsgs { if !strings.Contains(logOutput, msg) { t.Errorf("Expected the message for %v: %v", objType, msg) } } - // Test whether func panics and sends the correct panic msg if r := recover(); r == nil { t.Errorf("The function didn't panic for %v", objType) - } else { - if r != panicMsg { - // The panic msg is incorrect - t.Errorf("The function outputted the wrong panic message for %v.", objType) - } + } else if r != panicMsg { + t.Errorf("The function outputted the wrong panic message for %v.", objType) } }() - // Run func switch objType { case "course": valDuplicateCourses(testCourses[ix], testCourses[ix]) @@ -360,9 +311,8 @@ func testDuplicateFail(objType string, ix int, t *testing.T) { } // Test if func doesn't log anything and doesn't panic. -// Design for pass cases +// Designed for pass cases func testDuplicatePass(objType string, ix1 int, ix2 int, t *testing.T) { - // Buffer to capture the output var logBuffer bytes.Buffer log.SetOutput(&logBuffer) @@ -376,8 +326,6 @@ func testDuplicatePass(objType string, ix1 int, ix2 int, t *testing.T) { } }() - // Run func according to the object type. - // Choose pair of objects which are not duplicate switch objType { case "course": valDuplicateCourses(testCourses[ix1], testCourses[ix2]) @@ -390,11 +338,13 @@ func testDuplicatePass(objType string, ix1 int, ix2 int, t *testing.T) { // fail = "missing" means it lacks one sections // fail = "modified" means one section's course reference has been modified +// fail = "wrongkey" means one section is stored under the wrong compound key func testCourseReferenceFail(fail string, courseIx int, sectionIx int, t *testing.T) { - courseSectionMap := make(map[schema.CourseKey]map[schema.CourseSectionKey]*schema.Section) + courseSectionMap := make(map[schema.CourseKey]map[schema.SectionKey]*schema.Section) // Used to store keys of modified sections - var sectionRef schema.CourseSectionKey + var sectionRef schema.SectionKey + var actualSectionKey schema.SectionKey var sectionCourseRef, originalCourse schema.CourseKey // Build the failed section map based on fail type @@ -404,12 +354,15 @@ func testCourseReferenceFail(fail string, courseIx int, sectionIx int, t *testin for i, section := range testSections { courseKey := section.Course if courseSectionMap[courseKey] == nil { - courseSectionMap[courseKey] = make(map[schema.CourseSectionKey]*schema.Section) + courseSectionMap[courseKey] = make(map[schema.SectionKey]*schema.Section) } - sectionKey := schema.CourseSectionKey{ + sectionKey := schema.SectionKey{ + Subject_prefix: section.Course.Subject_prefix, + Course_number: section.Course.Course_number, + Catalog_year: section.Course.Catalog_year, Section_number: section.Section_number, - Term: section.Academic_session.Name, + Term: section.Academic_session.Name, } if sectionIx != i { @@ -418,17 +371,21 @@ func testCourseReferenceFail(fail string, courseIx int, sectionIx int, t *testin sectionRef = sectionKey // Nonexistent key referenced by course } } + case "modified": // One section doesn't reference to correct courses for i, section := range testSections { courseKey := section.Course if courseSectionMap[courseKey] == nil { - courseSectionMap[courseKey] = make(map[schema.CourseSectionKey]*schema.Section) + courseSectionMap[courseKey] = make(map[schema.SectionKey]*schema.Section) } - sectionKey := schema.CourseSectionKey{ + sectionKey := schema.SectionKey{ + Subject_prefix: section.Course.Subject_prefix, + Course_number: section.Course.Course_number, + Catalog_year: section.Course.Catalog_year, Section_number: section.Section_number, - Term: section.Academic_session.Name, + Term: section.Academic_session.Name, } courseSectionMap[courseKey][sectionKey] = section @@ -442,41 +399,83 @@ func testCourseReferenceFail(fail string, courseIx int, sectionIx int, t *testin courseSectionMap[courseKey][sectionKey].Course = schema.CourseKey{} } } + + case "wrongkey": + // One section exists, but is stored under the wrong compound key + // and the course references that same wrong key + for i, section := range testSections { + courseKey := section.Course + if courseSectionMap[courseKey] == nil { + courseSectionMap[courseKey] = make(map[schema.SectionKey]*schema.Section) + } + + realSectionKey := schema.SectionKey{ + Subject_prefix: section.Course.Subject_prefix, + Course_number: section.Course.Course_number, + Catalog_year: section.Course.Catalog_year, + Section_number: section.Section_number, + Term: section.Academic_session.Name, + } + + if sectionIx == i { + actualSectionKey = realSectionKey + + sectionRef = schema.SectionKey{ + Subject_prefix: section.Course.Subject_prefix, + Course_number: section.Course.Course_number, + Catalog_year: section.Course.Catalog_year, + Section_number: section.Section_number, + Term: section.Academic_session.Name + "_WRONG", + } + + // store section under wrong key so lookup succeeds + courseSectionMap[courseKey][sectionRef] = section + + // replace the matching course reference with the wrong key + course := testCourses[courseIx] + for j, key := range course.Sections { + if key == realSectionKey { + course.Sections[j] = sectionRef + break + } + } + } else { + courseSectionMap[courseKey][realSectionKey] = section + } + } } // Expected msgs var expectedMsgs []string // The course that references nonexistent stuff - var failCourse *schema.Course + failCourse := testCourses[courseIx] + failCourseKey := schema.CourseKey{ + Subject_prefix: failCourse.Subject_prefix, + Course_number: failCourse.Course_number, + Catalog_year: failCourse.Catalog_year, + } if fail == "missing" { - failCourse = testCourses[courseIx] - failCourseKey := schema.CourseKey{ - Subject_prefix: failCourse.Subject_prefix, - Course_number: failCourse.Course_number, - Catalog_year: failCourse.Catalog_year, - } - expectedMsgs = []string{ fmt.Sprintf("Nonexistent section reference found for %s%s!", failCourse.Subject_prefix, failCourse.Course_number), - fmt.Sprintf("Referenced section key: %s\nCourse key: %s", sectionRef, failCourseKey), - } - } else { - failCourse = testCourses[courseIx] - failCourseKey := schema.CourseKey{ - Subject_prefix: failCourse.Subject_prefix, - Course_number: failCourse.Course_number, - Catalog_year: failCourse.Catalog_year, + fmt.Sprintf("Referenced section key: %+v\nCourse key: %+v", sectionRef, failCourseKey), } + } else if fail == "modified" { failSection := testSections[sectionIx] expectedMsgs = []string{ fmt.Sprintf("Inconsistent section reference found for %s%s! The course references the section, but not vice-versa!", failCourse.Subject_prefix, failCourse.Course_number), - fmt.Sprintf("Referenced CourseSection key: %+v\nCourse key: %+v\nSection's course key: %+v", + fmt.Sprintf("Referenced section key: %+v\nCourse key: %+v\nSection's course key: %+v", sectionRef, failCourseKey, failSection.Course), } + } else { + expectedMsgs = []string{ + fmt.Sprintf("Mismatched section key found for %s%s!", failCourse.Subject_prefix, failCourse.Course_number), + fmt.Sprintf("Course stored section key: %+v\nActual section key: %+v", + sectionRef, actualSectionKey), + } } // Buffer to capture the output @@ -497,6 +496,17 @@ func testCourseReferenceFail(fail string, courseIx int, sectionIx int, t *testin courseSectionMap[originalCourse][sectionRef].Course = sectionCourseRef } + // Restore to original section key in course reference (if needed) + if fail == "wrongkey" { + failCourse := testCourses[courseIx] + for j, key := range failCourse.Sections { + if key == sectionRef { + failCourse.Sections[j] = actualSectionKey + break + } + } + } + if r := recover(); r == nil { t.Errorf("The function didn't panic") } else { @@ -505,9 +515,8 @@ func testCourseReferenceFail(fail string, courseIx int, sectionIx int, t *testin } } }() - // Run func for _, course := range testCourses { valCourseReference(course, courseSectionMap) } -} \ No newline at end of file +}