Skip to content

Commit

Permalink
Merge pull request #102 from CurricularAnalytics/development
Browse files Browse the repository at this point in the history
Improve test coverage
  • Loading branch information
heileman committed Sep 16, 2020
2 parents 520951e + 8f6b942 commit ddd1da7
Show file tree
Hide file tree
Showing 15 changed files with 307 additions and 59 deletions.
1 change: 1 addition & 0 deletions Project.toml
Expand Up @@ -7,6 +7,7 @@ version = "1.1.0"
CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b"
DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0"
DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8"
Dates = "ade2ca70-3891-5945-98fb-dc099432e06a"
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
LightGraphs = "093fc24a-ae57-5d10-9952-331d41423f4d"
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
Expand Down
16 changes: 9 additions & 7 deletions src/CurricularAnalytics.jl
Expand Up @@ -14,6 +14,7 @@ using DataStructures
using Printf
using Markdown
using Documenter
using Dates

include("DataTypes/DataTypes.jl")
include("DataHandler.jl")
Expand All @@ -22,14 +23,15 @@ include("DegreePlanAnalytics.jl")
include("DegreePlanCreation.jl")
include("Simulation/Simulation.jl")

export AA, AAS, AS, BA, BS, Course, CourseCatalog, Curriculum, Degree, DegreePlan, EdgeClass, LearningOutcome, Requisite, System, Term, add_course!,
add_lo_requisite!, add_requisite!, all_paths, basic_metrics, basic_statistics, bin_filling, blocking_factor, centrality, co, compare_curricula, convert_ids,
complexity, course, course_from_id, course_from_vertex, course_id, courses_from_vertices, create_degree_plan, dead_ends, delay_factor, delete_requisite!,
dfs, extraneous_requisites, find_term, gad, homology, is_duplicate, isvalid_curriculum, isvalid_degree_plan, longest_path, longest_paths, merge_curricula,
export AA, AAS, AS, AbstractRequirement, BA, BS, Course, CourseCollection, CourseCatalog, CourseRecord, CourseSet, Curriculum, Degree, DegreePlan, EdgeClass,
Enrollment, Grade, LearningOutcome, PassRate, RequirementSet, Requisite, Student, StudentRecord, Simulation, System, Term, TransferArticulation,
add_course!, add_lo_requisite!, add_requisite!, add_transfer_catalog, add_transfer_course, all_paths, back_edge, basic_metrics, basic_statistics,
bin_filling, blocking_factor, centrality, co, compare_curricula, convert_ids, complexity, course, course_from_id, course_from_vertex, course_id,
courses_from_vertices, create_degree_plan, cross_edge, dead_ends, delay_factor, delete_requisite!, dfs, extraneous_requisites, find_term, forward_edge,
gad, grade, homology, is_duplicate, isvalid_curriculum, isvalid_degree_plan, longest_path, longest_paths, merge_curricula, pass_table, passrate_table,
pre, print_plan, quarter, reach, reach_subgraph, reachable_from, reachable_from_subgraph, reachable_to, reachable_to_subgraph, read_csv, requisite_distance,
requisite_type, semester, similarity, strict_co, topological_sort, total_credits, write_csv, Grade, grade, AbstractRequirement, CourseSet, RequirementSet,
CourseRecord, StudentRecord, TransferArticulation, add_transfer_catalog, add_transfer_course, transfer_equiv, PassRate, Enrollment, Simulation, Student, set_passrates,
set_passrates_from_csv, simple_students, simulate, simulation_report, pass_table, passrate_table, set_passrate_for_course
requisite_type, semester, set_passrates, set_passrate_for_course, set_passrates_from_csv, similarity, simple_students, simulate, simulation_report,
strict_co, topological_sort, total_credits, transfer_equiv, tree_edge, write_csv

# Check if a curriculum graph has requisite cycles.
"""
Expand Down
26 changes: 19 additions & 7 deletions src/DataTypes/Course.jl
Expand Up @@ -127,8 +127,14 @@ end
Add course rc as a requisite, of type requisite_type, for target course tc.
# Arguments
Required:
- `rc::AbstractCourse` : requisite course.
- `tc::AbstractCourse` : target course, i.e., course for which `rc` is a requisite.
- `requisite_type::Requisite` : requisite type.
# Requisite types
One of the following requisite types must be specified for `rc`:
One of the following requisite types must be specified for the `requisite_type`:
- `pre` : a prerequisite course that must be passed before `tc` can be attempted.
- `co` : a co-requisite course that may be taken before or at the same time as `tc`.
- `strict_co` : a strict co-requisite course that must be taken at the same time as `tc`.
Expand All @@ -142,8 +148,14 @@ end
Add a collection of requisites to target course tc.
# Arguments
Required:
- `rc::Array{AbstractCourse}` : and array of requisite courses.
- `tc::AbstractCourse` : target course, i.e., course for which `rc` is a requisite.
- `requisite_type::Array{Requisite}` : an array of requisite types.
# Requisite types
The following requisite types may be specified for `rc`:
The following requisite types may be specified for the `requisite_type`:
- `pre` : a prerequisite course that must be passed before `tc` can be attempted.
- `co` : a co-requisite course that may be taken before or at the same time as `tc`.
- `strict_co` : a strict co-requisite course that must be taken at the same time as `tc`.
Expand All @@ -161,11 +173,11 @@ end
Remove course rc as a requisite for target course tc. If rc is not an existing requisite for tc, an
error is thrown.
# Requisite types
The following requisite types may be specified for `rc`:
- `pre` : a prerequisite course that must be passed before `tc` can be attempted.
- `co` : a co-requisite course that may be taken before or at the same time as `tc`.
- `strict_co` : a strict co-requisite course that must be taken at the same time as `tc`.
# Arguments
Required:
- `rc::AbstractCourse` : requisite course.
- `tc::AbstractCourse` : target course, i.e., course for which `rc` is a requisite.
"""
function delete_requisite!(requisite_course::Course, course::Course)
#if !haskey(course.requisites, requisite_course.id)
Expand Down
2 changes: 1 addition & 1 deletion src/DataTypes/CourseCatalog.jl
Expand Up @@ -6,7 +6,7 @@ mutable struct CourseCatalog
name::AbstractString # Name of the course catalog
institution::AbstractString # Institution offering the courses in the catalog
date_range::Tuple # range of dates the catalog is applicable over
catalog::Dict{Int, Course} # dictionary of courses in (course_id, course) format
catalog::Dict{Int, Course} # dictionary of courses in (course_id, course) format

# Constructor
function CourseCatalog(name::AbstractString, institution::AbstractString; courses::Array{Course}=Array{Course,1}(),
Expand Down
11 changes: 6 additions & 5 deletions src/DataTypes/Curriculum.jl
Expand Up @@ -177,18 +177,19 @@ function create_graph!(curriculum::Curriculum)
end
end

# find requisite type from vertex ids in a curriculum graph
function requisite_type(curriculum::Curriculum, src_course_id::Int, dst_course_id::Int)
src = 0; dst = 0
for c in curriculum.courses
if c.vertex_id == src_course_id
if c.vertex_id[curriculum.id] == src_course_id
src = c
elseif c.vertex_id == dst_course_id
elseif c.vertex_id[curriculum.id] == dst_course_id
dst = c
end
end
if ((src == 0 || dst == 0) || !haskey(dst.requisites, src))
error("edge ($src_course_id, $dst_course_id) does not exist")
if ((src == 0 || dst == 0) || !haskey(dst.requisites, src.id))
error("edge ($src_course_id, $dst_course_id) does not exist in curriculum graph")
else
return dst.requisites[src]
return dst.requisites[src.id]
end
end
8 changes: 2 additions & 6 deletions src/DataTypes/DegreeRequirements.jl
Expand Up @@ -154,8 +154,9 @@ mutable struct CourseSet <: AbstractRequirement
# Constructor
# A requirement may involve a set of courses, or a set of requirements, but not both
function CourseSet(name::AbstractString, credit_hours::Real, course_reqs::Array{Pair{Course,Grade},1}=Array{Pair{Course,Grade},1}(); description::AbstractString="",
course_catalog::CourseCatalog=CourseCatalog("", ""), prefix_regex::Regex=r".*", num_regex::Regex=r".*", course_regex::Regex=r".*",
course_catalog::CourseCatalog=CourseCatalog("", ""), prefix_regex::Regex=r".^", num_regex::Regex=r".^", course_regex::Regex=r".^",
min_grade::Grade=grade("D"), double_count::Bool=false)
# r".^" is a regex that matches nothing
this = new()
this.name = name
this.description = description
Expand All @@ -171,11 +172,6 @@ mutable struct CourseSet <: AbstractRequirement
push!(course_reqs, c[2] => min_grade)
end
end
for c in course_catalog.catalog # search the supplied course catalog for courses course regular expression
if occursin(course_regex, c[2].prefix * c[2].num)
push!(course_reqs, c[2] => min_grade)
end
end
return this
end
end
Expand Down
2 changes: 1 addition & 1 deletion src/DataTypes/LearningOutcome.jl
Expand Up @@ -44,7 +44,7 @@ end

#"""
#add_lo_requisite!(rlo, tlo, requisite_type)
#Add learning outcome rlo as a requisite, of type requisite_type, for target learning
#Add learning outcome rlo as a requisite, of type requisite_type, for target learning outcome tlo
#outcome tlo.
#"""
function add_lo_requisite!(requisite_lo::LearningOutcome, lo::LearningOutcome, requisite_type::Requisite)
Expand Down
3 changes: 1 addition & 2 deletions src/DataTypes/Simulation.jl
@@ -1,5 +1,5 @@
mutable struct Simulation
degree_plan::DegreePlan # The curriculum that is simulated
degree_plan::DegreePlan # The curriculum that is simulated
duration::Int # The number of terms the simulation runs for
course_attempt_limit::Int # The number of times that a course is allowed to take

Expand All @@ -26,7 +26,6 @@ mutable struct Simulation
this = new()

this.degree_plan = degree_plan

this.enrolled_students = Student[]
this.graduated_students = Student[]
this.stopout_students = Student[]
Expand Down
4 changes: 2 additions & 2 deletions src/DataTypes/Student.jl
Expand Up @@ -15,7 +15,7 @@ mutable struct Student


# Constructor
function Student(id::Int, attributes::Dict)
function Student(id::Int; attributes::Dict=Dict())
this = new()
this.id = id
this.termcredits = 0
Expand All @@ -33,7 +33,7 @@ end
function simple_students(number)
students = Student[]
for i = 1:number
student = Student(i, Dict())
student = Student(i)
student.stopout = false
push!(students, student)
end
Expand Down
163 changes: 136 additions & 27 deletions test/DataTypes.jl
@@ -1,4 +1,8 @@
# DataTypes tests

using Dates
using LightGraphs

@testset "DataTypes Tests" begin

# 8-vertex test curriculum - valid
Expand All @@ -11,31 +15,33 @@
# (A,C) - pre; (C,E) - pre; (B,C) - pre; (D,C) - co; (C,E) - pre; (D,F) - pre
#

# Test course creation
A = Course("Introduction to Baskets", 3, institution="ACME State University", prefix="BW", num="101", canonical_name="Baskets I")
include("test_degree_plan.jl")

# Test Course creation
@test A.name == "Introduction to Baskets"
@test A.credit_hours == 3
@test A.prefix == "BW"
@test A.num == "101"
@test A.num == "110"
@test A.institution == "ACME State University"
@test A.canonical_name == "Baskets I"

# Test curriciulum creation
B = Course("Swimming", 3, institution="ACME State University", prefix="PE", num="115", canonical_name="Physical Education")
C = Course("Basic Basket Forms", 3, institution="ACME State University", prefix="BW", num="111", canonical_name="Baskets I")
D = Course("Basic Basket Forms Lab", 1, institution="ACME State University", prefix="BW", num="111L", canonical_name="Baskets I Laboratory")
E = Course("Advanced Basketry", 3, institution="ACME State University", prefix="CS", num="300", canonical_name="Baskets II")
F = Course("Basket Materials & Decoration", 3, institution="ACME State University", prefix="BW", num="214", canonical_name="Basket Materials")
G = Course("Humanitites Elective", 3, institution="ACME State University", prefix="EGR", num="101", canonical_name="Humanitites Core")
H = Course("Technical Elective", 3, institution="ACME State University", prefix="BW", num="3xx", canonical_name="Elective")

add_requisite!(A,C,pre)
add_requisite!(B,C,pre)
add_requisite!(D,C,co)
add_requisite!(C,E,pre)
add_requisite!(D,F,pre)

curric = Curriculum("Underwater Basket Weaving", [A,B,C,D,E,F,G,H], institution="ACME State University", CIP="445786")
# Test course_id function
@test course_id(A.prefix, A.num, A.name, A.institution) == convert(Int, mod(hash(A.name * A.prefix * A.num * A.institution), UInt32))

# Test add_requisite! function
@test length(A.requisites) == 0
@test length(B.requisites) == 0
@test length(C.requisites) == 3
@test length(D.requisites) == 0
@test length(E.requisites) == 1
@test length(F.requisites) == 1

# Test delete_requisite! function
delete_requisite!(A,C);
@test length(C.requisites) == 2
add_requisite!(A,C,pre);

# Test Curriciulum creation
@test curric.name == "Underwater Basket Weaving"
@test curric.institution == "ACME State University"
@test curric.degree_type == BS
Expand All @@ -44,16 +50,119 @@ curric = Curriculum("Underwater Basket Weaving", [A,B,C,D,E,F,G,H], institution=
@test curric.num_courses == 8
@test curric.credit_hours == 22

# Test degree plan creation
terms = Array{Term}(undef, 4)
terms[1] = Term([A,B])
terms[2] = Term([C,D])
terms[3] = Term([E,F])
terms[4] = Term([G,H])
dp = DegreePlan("2019 Plan", curric, terms)
# test the underlying graph
@test nv(curric.graph) == 8
@test ne(curric.graph) == 5

lo1 = LearningOutcome("Test learning outcome #1", "students will demonstrate ability to do #1", 12)
lo2 = LearningOutcome("Test learning outcome #1", "students will demonstrate ability to do #2", 10)
lo3 = LearningOutcome("Test learning outcome #1", "students will demonstrate ability to do #3", 15)
lo4 = LearningOutcome("Test learning outcome #1", "students will demonstrate ability to do #3", 7)
add_lo_requisite!(lo1, lo2, pre)
add_lo_requisite!([lo2, lo3], lo4, [pre, co])
@test length(lo1.requisites) == 0
@test length(lo2.requisites) == 1
@test length(lo3.requisites) == 0
@test length(lo4.requisites) == 2

mapped_ids = CurricularAnalytics.map_vertex_ids(curric)
@test requisite_type(curric,mapped_ids[A.id],mapped_ids[C.id]) == pre
@test requisite_type(curric,mapped_ids[D.id],mapped_ids[C.id]) == co

@test total_credits(curric) == 22
@test course_from_vertex(curric, 1) in [A,B,C,D,E,F,G,H]
@test course_from_vertex(curric, 2) in [A,B,C,D,E,F,G,H]
@test course_from_vertex(curric, 3) in [A,B,C,D,E,F,G,H]
@test course_from_vertex(curric, 4) in [A,B,C,D,E,F,G,H]
@test course_from_vertex(curric, 5) in [A,B,C,D,E,F,G,H]
@test course_from_vertex(curric, 6) in [A,B,C,D,E,F,G,H]
@test course_from_vertex(curric, 7) in [A,B,C,D,E,F,G,H]
@test course_from_vertex(curric, 8) in [A,B,C,D,E,F,G,H]

@test course_from_id(curric, A.id) == A
@test course(curric, "BW", "110", "Introduction to Baskets", "ACME State University") == A
id = A.id
convert_ids(curric); # this should not change the ids, since the curriculum was not created from a CSV file
@test A.id == id

# Test CourseCollection creation
CC = CourseCollection("Test Course Collection", 3, [A,B,C,E], institution="ACME State University");
@test CC.name == "Test Course Collection"
@test CC.credit_hours == 3
@test length(CC.courses) == 4
@test CC.institution == "ACME State University"

# Test CourseCatalog creation
CCat = CourseCatalog("Test Course Catalog", "ACME State University", courses=[A], catalog=Dict([(B.id=>B),C.id=>C]), date_range=(Date(2019,8), Date(2020,7,31)));
@test CCat.name == "Test Course Catalog"
@test CCat.institution == "ACME State University"
@test length(CCat.catalog) == 3

# Test add_course! functions
add_course!(CCat, [D]);
@test length(CCat.catalog) == 4
add_course!(CCat, [E,F,G]);
@test length(CCat.catalog) == 7
@test is_duplicate(CCat, A) == true
@test is_duplicate(CCat, H) == false
@test (CCat.date_range[2] - CCat.date_range[1]) == Dates.Day(365)
@test A == course(CCat, "BW", "110", "Introduction to Baskets")

# Test DegreePlan creation, other degree plan functions tested in ./test/DegreePlanAnalytics.jl
@test dp.name == "2019 Plan"
@test dp.curriculum === curric # tests that they're the same object in memory
@test dp.num_terms == 4
@test dp.credit_hours == 22

end;
# Test DegreeRequirements creation
# The regex's specified will match all courses with the EGR prefix and any number
cs1 = CourseSet("Test Course Set 1", 3, [(A=>grade("C")), (B=>grade("D"))], course_catalog=CCat, prefix_regex=r"^\s*+EGR\s*+$", num_regex=r".*", double_count=true);
@test cs1.name == "Test Course Set 1"
@test cs1.course_catalog == CCat
@test cs1.double_count == true
@test length(cs1.course_reqs) == 3
# The regex's specified will match all courses with number 111 and any prefix
cs2 = CourseSet("Test Course Set 2", 3, Array{Pair{Course,Grade},1}(), course_catalog=CCat, prefix_regex=r".*", num_regex=r"^\s*+111\s*+$");
@test cs2.double_count == false
@test length(cs2.course_reqs) == 1

req_set = AbstractRequirement[cs1,cs2];
rs = RequirementSet("Test Requirement Set", 6, req_set);
@test rs.name == "Test Requirement Set"
@test rs.credit_hours == 6
@test rs.satisfy == 2
rs = RequirementSet("Test Requirement Set", 6, req_set, satisfy=1);
@test rs.satisfy == 1
rs = RequirementSet("Test Requirement Set", 6, req_set, satisfy=5);
@test rs.satisfy == 2

# Test StudentRecord creation
cr1 = CourseRecord(A, grade("C"), "FALL 2020");
@test cr1.course == A
@test cr1.grade == 6
cr2 = CourseRecord(B, UInt64(13), "SPRING 2020");
@test cr2.grade == grade("A➕")
std_rec = StudentRecord("A14356", "Patti", "Furniture", "O", [cr1, cr2]);
@test length(std_rec.transcript) == 2

# Test Student creation
std = Student(1, attributes = Dict("race" => "other", "HS_GPA" => 3.5));
@test length(std.attributes) == 2
stds = simple_students(100);
@test length(stds) == 100

# Test TransferArticulation creation
XA = Course("Baskets 101", 3, institution="Tri-county Community College", prefix="BW", num="101", canonical_name="Baskets I");
XCat = CourseCatalog("Another Course Catalog", "Tri-county Community College", courses=[XA], date_range=(Date(2019,8), Date(2020,7,31)));
#xfer_map = Dict((XCat.id, XA.id) => [A.id]) # this should work, but it fails
#ta = TransferArticulation("Test Xfer Articulation", "ACME State University", CCat, Dict(XCat.id => XCat), xfer_map);
ta = TransferArticulation(
"Test Xfer Articulation", "ACME State University", CCat, Dict(XCat.id => XCat));
add_transfer_course(ta, [A.id], XCat.id, XA.id)
@test transfer_equiv(ta, XCat.id, XA.id) == [A.id]

# Test Simulation creation
sim_obj = Simulation(dp);
@test sim_obj.degree_plan == dp

end;

0 comments on commit ddd1da7

Please sign in to comment.