From 5ae2652609a15aa52fa5fb38c3773a719951f18c Mon Sep 17 00:00:00 2001 From: Alomir Date: Wed, 18 Dec 2024 18:05:39 -0500 Subject: [PATCH 01/16] Adds event handler infrastructure with simple passing test --- Makefile | 40 +++- events.c | 227 +++++++++++++++++++++ events.h | 67 ++++++ frontend.c | 11 + modelStructures.h | 17 ++ sipnet.c | 57 ++++-- sipnet.h | 4 + tests/sipnet/test_events/Makefile | 27 +++ tests/sipnet/test_events/infra_events.in | 5 + tests/sipnet/test_events/modelStructures.h | 17 ++ tests/sipnet/test_events/testEventInfra.c | 152 ++++++++++++++ tests/sipnet/utils.h | 37 ++++ tests/test.template | 37 ++++ 13 files changed, 676 insertions(+), 22 deletions(-) create mode 100644 events.c create mode 100644 events.h create mode 100644 modelStructures.h create mode 100644 tests/sipnet/test_events/Makefile create mode 100644 tests/sipnet/test_events/infra_events.in create mode 100644 tests/sipnet/test_events/modelStructures.h create mode 100644 tests/sipnet/test_events/testEventInfra.c create mode 100644 tests/sipnet/utils.h create mode 100644 tests/test.template diff --git a/Makefile b/Makefile index 200df75..9b2a37c 100755 --- a/Makefile +++ b/Makefile @@ -1,15 +1,16 @@ CC=gcc LD=gcc +AR=ar CFLAGS=-Wall LIBLINKS=-lm -ESTIMATE_CFILES=sipnet.c ml-metro5.c ml-metrorun.c paramchange.c runmean.c util.c spatialParams.c namelistInput.c outputItems.c +ESTIMATE_CFILES=sipnet.c ml-metro5.c ml-metrorun.c paramchange.c runmean.c util.c spatialParams.c namelistInput.c outputItems.c events.c ESTIMATE_OFILES=$(ESTIMATE_CFILES:.c=.o) -SENSTEST_CFILES=sipnet.c sensTest.c paramchange.c runmean.c util.c spatialParams.c namelistInput.c outputItems.c +SENSTEST_CFILES=sipnet.c sensTest.c paramchange.c runmean.c util.c spatialParams.c namelistInput.c outputItems.c events.c SENSTEST_OFILES=$(SENSTEST_CFILES:.c=.o) -SIPNET_CFILES=sipnet.c frontend.c runmean.c util.c spatialParams.c namelistInput.c outputItems.c +SIPNET_CFILES=sipnet.c frontend.c runmean.c util.c spatialParams.c namelistInput.c outputItems.c events.c SIPNET_OFILES=$(SIPNET_CFILES:.c=.o) TRANSPOSE_CFILES=transpose.c util.c @@ -42,10 +43,41 @@ clean: #clean: # rm -f $(ESTIMATE_OFILES) $(SENSTEST_OFILES) $(SIPNET_OFILES) $(TRANSPOSE_OFILES) $(SUBSET_DATA_OFILES) estimate sensTest sipnet transpose subsetData +# +# UNIT TESTS +SIPNET_TEST_DIRS:=$(shell find tests/sipnet -type d -mindepth 1 -maxdepth 1) +SIPNET_TEST_DIRS_CLEAN:= $(addsuffix .clean, $(SIPNET_TEST_DIRS)) +SIPNET_LIB=libsipnet.a + +$(SIPNET_LIB): $(SIPNET_LIB)($(SIPNET_OFILES)) + ranlib $(SIPNET_LIB) + +test: pretest $(SIPNET_TEST_DIRS) posttest $(SIPNET_LIB) + +pretest: + cp modelStructures.h modelStructures.orig.h + +# The dash in the build command tells make to continue if there are errors, allowing cleanup +$(SIPNET_TEST_DIRS): pretest $(SIPNET_LIB) + cp $@/modelStructures.h modelStructures.h + -$(MAKE) -C $@ + +# This is far from infallible, as model_structures.h will be in a bad place if a test +# build step fails in a non-catchable way +posttest: $(SIPNET_TEST_DIRS) + mv modelStructures.orig.h modelStructures.h + +testclean: $(SIPNET_TEST_DIRS_CLEAN) + rm -f $(SIPNET_LIB) + +$(SIPNET_TEST_DIRS_CLEAN): + $(MAKE) -C $(basename $@) clean + +.PHONY: all test $(SIPNET_TEST_DIRS) pretest posttest $(SIPNET_LIB) testclean $(SIPNET_TEST_DIRS_CLEAN) + #This target automatically builds dependencies. depend:: makedepend $(CFILES) # DO NOT DELETE THIS LINE -- make depend depends on it. - diff --git a/events.c b/events.c new file mode 100644 index 0000000..b8c48cc --- /dev/null +++ b/events.c @@ -0,0 +1,227 @@ +// +// events.c +// + +#include "events.h" +#include +#include +#include +#include "util.h" + +void printEvent(EventNode* event); + +EventNode* createEventNode( + int loc, int year, int day, int eventType, char* eventParamsStr +) { + EventNode *event = (EventNode*)malloc(sizeof(EventNode)); + event->loc = loc; + event->year = year; + event->day = day; + event->type = eventType; + + switch (eventType) { + case HARVEST: + { + double fracRA, fracRB, fracTA, fracTB; + HarvestParams *params = (HarvestParams*)malloc(sizeof(HarvestParams)); + sscanf(eventParamsStr, "%lf %lf %lf %lf", &fracRA, &fracRB, &fracTA, &fracTB); + params->fractionRemovedAbove = fracRA; + params->fractionRemovedBelow = fracRB; + params->fractionTransferredAbove = fracTA; + params->fractionTransferredBelow = fracTB; + event->eventParams = params; + } + break; + case IRRIGATION: + { + double amountAdded; + int location; + IrrigationParams *params = (IrrigationParams*)malloc(sizeof(IrrigationParams)); + sscanf(eventParamsStr, "%lf %d", &amountAdded, &location); + params->amountAdded = amountAdded; + params->location = location; + event->eventParams = params; + } + break; + case FERTILIZATION: + { + // If/when we try two N pools, enable the additional nh4_no3_frac param (likely via + // compiler switch) + double orgN, orgC, minN; + // double nh4_no3_frac; + FertilizationParams *params = (FertilizationParams*)malloc(sizeof(FertilizationParams)); + sscanf(eventParamsStr, "%lf %lf %lf", &orgN, &orgC, &minN); + // scanf(eventParamsStr, "%lf %lf %lf %lf", &org_N, &org_C, &min_N, &nh4_no3_frac); + params->orgN = orgN; + params->orgC = orgC; + params->minN = minN; + // params->nh4_no3_frac = nh4_nos_frac; + event->eventParams = params; + } + break; + case PLANTING: + { + int emergenceLag; + double addedC, addedN; + PlantingParams *params = (PlantingParams*)malloc(sizeof(PlantingParams)); + sscanf(eventParamsStr, "%d %lf %lf", &emergenceLag, &addedC, &addedN); + params->emergenceLag = emergenceLag; + params->addedC = addedC; + params->addedN = addedN; + event->eventParams = params; + } + break; + case TILLAGE: + { + double fracLT, somDM, litterDM; + TillageParams *params = (TillageParams*)malloc(sizeof(TillageParams)); + sscanf(eventParamsStr, "%lf %lf %lf", &fracLT, &somDM, &litterDM); + params->fractionLitterTransferred = fracLT; + params->somDecompModifier = somDM; + params->litterDecompModifier = litterDM; + event->eventParams = params; + } + break; + default: + // Unknown type, error and exit + printf("Error reading from event file: unknown event type %d\n", eventType); + exit(1); + } + + event->nextEvent = NULL; + return event; +} + +enum EventType getEventType(char *eventTypeStr) { + if (strcmp(eventTypeStr, "irrig") == 0) { + return IRRIGATION; + } else if (strcmp(eventTypeStr, "fert") == 0) { + return FERTILIZATION; + } else if (strcmp(eventTypeStr, "plant") == 0) { + return PLANTING; + } else if (strcmp(eventTypeStr, "till") == 0) { + return TILLAGE; + } else if (strcmp(eventTypeStr, "harv") == 0) { + return HARVEST; + } + return UNKNOWN_EVENT; +} + +EventNode** readEventData(char *eventFile, int numLocs) { + int loc, year, day, eventType; + int currLoc, currYear, currDay; + int EVENT_LINE_SIZE = 1024; + int numBytes; + char *eventParamsStr; + char eventTypeStr[20]; + char line[EVENT_LINE_SIZE]; + EventNode *curr, *next; + + printf("Begin reading event data from file %s\n", eventFile); + + EventNode** events = (EventNode**)calloc(sizeof(EventNode*), numLocs * sizeof(EventNode*)); + // status of the read + FILE *in = openFile(eventFile, "r"); + + if (fgets(line, EVENT_LINE_SIZE, in) == NULL) { + printf("Error: no event data in %s\n", eventFile); + exit(1); + } + sscanf(line, "%d %d %d %s %n", &loc, &year, &day, eventTypeStr, &numBytes); + eventParamsStr = line + numBytes; + + eventType = getEventType(eventTypeStr); + if (eventType == UNKNOWN_EVENT) { + printf("Error: unknown event type %s\n", eventTypeStr); + exit(1); + } + printf("Found event type %d (%s)\n", eventType, eventTypeStr); + + next = createEventNode(loc, year, day, eventType, eventParamsStr); + events[loc] = next; + currLoc = loc; + currYear = year; + currDay = day; + + printf("Found events:\n"); + printEvent(next); + + while (fgets(line, EVENT_LINE_SIZE, in) != NULL) { + // We have another event + curr = next; + sscanf(line, "%d %d %d %s %n", &loc, &year, &day, eventTypeStr, &numBytes); + eventParamsStr = line + numBytes; + + eventType = getEventType(eventTypeStr); + if (eventType == UNKNOWN_EVENT) { + printf("Error: unknown event type %s\n", eventTypeStr); + exit(1); + } + + // make sure location and time are non-decreasing + if (loc < currLoc) { + printf("Error reading event file: was reading location %d, trying to read location %d\n", currLoc, loc); + printf("Event records for a given location should be contiguous, and locations should be in ascending order\n"); + exit(1); + } + if ((loc == currLoc) && ((year < currYear) || (day < currDay))) { + printf("Error reading event file: for location %d, last event was at (%d, %d) ", currLoc, currYear, currDay); + printf("next event is at (%d, %d)\n", year, day); + printf("Event records for a given location should be in time-ascending order\n"); + exit(1); + } + + next = createEventNode(loc, year, day, eventType, eventParamsStr); + printEvent(next); + if (currLoc == loc) { + // Same location, add the new event to this location's list + curr->nextEvent = next; + } + else { + // New location, update location and start a new list + currLoc = loc; + events[currLoc] = next; + } + } + + fclose(in); + return events; +} + +void printEvent(EventNode *event) { + if (event == NULL) { + return; + } + int loc = event->loc; + int year = event->year; + int day = event->day; + switch (event->type) { + case IRRIGATION: + printf("IRRIGATION at loc %d on %d %d, ", loc, year, day); + IrrigationParams *const iParams = (IrrigationParams*)event->eventParams; + printf("with params: amount added %4.2f\n", iParams->amountAdded); + break; + case FERTILIZATION: + printf("FERTILIZATION at loc %d on %d %d, ", loc, year, day); + FertilizationParams *const fParams = (FertilizationParams*)event->eventParams; + printf("with params: org N %4.2f, org C %4.2f, min N %4.2f\n", fParams->orgN, fParams->orgC, fParams->minN); + break; + case PLANTING: + printf("PLANTING at loc %d on %d %d, ", loc, year, day); + PlantingParams *const pParams = (PlantingParams*)event->eventParams; + printf("with params: emergence lag %d, added C %4.2f, added N %4.2f\n", pParams->emergenceLag, pParams->addedC, pParams->addedN); + break; + case TILLAGE: + printf("TILLAGE at loc %d on %d %d, ", loc, year, day); + TillageParams *const tParams = (TillageParams*)event->eventParams; + printf("with params: frac litter transferred %4.2f, som decomp modifier %4.2f, litter decomp modifier %4.2f\n", tParams->fractionLitterTransferred, tParams->somDecompModifier, tParams->litterDecompModifier); + break; + case HARVEST: + printf("HARVEST at loc %d on %d %d, ", loc, year, day); + HarvestParams *const hParams = (HarvestParams*)event->eventParams; + printf("with params: frac removed above %4.2f, frac removed below %4.2f, frac transferred above %4.2f, frac transferred below %4.2f\n", hParams->fractionRemovedAbove, hParams->fractionRemovedBelow, hParams->fractionTransferredAbove, hParams->fractionTransferredBelow); + break; + default: + printf("Error printing event: unknown type %d\n", event->type); + } +} diff --git a/events.h b/events.h new file mode 100644 index 0000000..de027a6 --- /dev/null +++ b/events.h @@ -0,0 +1,67 @@ +// +// Created by Michael J Longfritz on 11/8/24. +// + +#ifndef EVENTS_H +#define EVENTS_H + +#include "modelStructures.h" + +enum EventType { + FERTILIZATION, + HARVEST, + IRRIGATION, + PLANTING, + TILLAGE, + UNKNOWN_EVENT +}; + +typedef struct HarvestParams { + double fractionRemovedAbove; + double fractionRemovedBelow; + double fractionTransferredAbove; // to surface litter pool + double fractionTransferredBelow; // to soil litter pool +} HarvestParams; + +typedef struct IrrigationParams { + double amountAdded; + int location; // 0=canopy, 1=soil +} IrrigationParams; + +typedef struct FertilizationParams { + double orgN; + double orgC; + double minN; + //double nh4_no3_frac; for two-pool version +} FertilizationParams; + +typedef struct PlantingParams { + int emergenceLag; + double addedC; + double addedN; +} PlantingParams; + +typedef struct TillageParams { + double fractionLitterTransferred; + double somDecompModifier; + double litterDecompModifier; +} TillageParams; + +typedef struct EventNode EventNode; +struct EventNode { + enum EventType type; + int loc, year, day; + void *eventParams; + EventNode *nextEvent; +}; + +/* Read event data from .event + * + * Format: returned data is structured as an array of EventNode pointers, indexed by + * location. Each element of the array is the first event for that location (or null + * if there are no events). It is assumed that the events are in chrono order. + */ +EventNode** readEventData(char *eventFile, int numLocs); + + +#endif //EVENTS_H diff --git a/frontend.c b/frontend.c index 018039d..c79633f 100755 --- a/frontend.c +++ b/frontend.c @@ -13,6 +13,7 @@ #include "spatialParams.h" #include "namelistInput.h" #include "outputItems.h" +#include "modelStructures.h" // important constants - default values: @@ -81,6 +82,10 @@ int main(int argc, char *argv[]) { double **standarddevs; int dataTypeIndices[MAX_DATA_TYPES]; +#ifdef EVENT_HANDLER + // Extra filename if we are handing events + char eventFile[FILE_MAXNAME+24]; +#endif // get command-line arguments: while ((option = getopt(argc, argv, "hi:")) != -1) { @@ -151,6 +156,12 @@ int main(int argc, char *argv[]) { strcat(climFile, ".clim"); numLocs = initModel(&spatialParams, &steps, paramFile, climFile); +#ifdef EVENT_HANDLER + strcpy(eventFile, fileName); + strcat(eventFile, ".event"); + initEvents(eventFile, numLocs); +#endif + if (doSingleOutputs) { outputItems = newOutputItems(fileName, ' '); setupOutputItems(outputItems); diff --git a/modelStructures.h b/modelStructures.h new file mode 100644 index 0000000..5a591bb --- /dev/null +++ b/modelStructures.h @@ -0,0 +1,17 @@ +// +// This file is inteneded to hold settings for different model structure options, +// implemented as compile-time flags. The options are here (with nothing else) to +// improve testability. +// + +#ifndef MODEL_STRUCTURES_H +#define MODEL_STRUCTURES_H + +// Begin definitions for choosing different model structures +// See also sipnet.c for other options (that should be moved here if/when testing is added) + +#define EVENT_HANDLER 1 +// Read in and process agronomic events. Expects a file named .event to exist. + + +#endif //MODEL_STRUCTURES_H diff --git a/sipnet.c b/sipnet.c index 0875145..ee6f717 100644 --- a/sipnet.c +++ b/sipnet.c @@ -18,6 +18,8 @@ #include "util.h" #include "spatialParams.h" #include "outputItems.h" +#include "modelStructures.h" +#include "events.h" // begin definitions for choosing different model structures // (1 -> true, 0 -> false) @@ -26,14 +28,14 @@ //alternative transpiration methods modified by Dave Moore #define ALTERNATIVE_TRANS 0 -// do we want to impliment alternative transpiration? +// do we want to implement alternative transpiration? #define BALL_BERRY 0 -//impliment a Ball Berry submodel to calculate gs from RH, CO2 and A +//implement a Ball Berry submodel to calculate gs from RH, CO2 and A //MUST BE OFF for PENMAN MONTEITH TO RUN #define PENMAN_MONTEITH_TRANS 0 -//impliment a transpiration calculation based on the Penman-Monteith Equation. +//implement a transpiration calculation based on the Penman-Monteith Equation. //March 1st 2007 PM equation not really working. //#define G 0 @@ -487,7 +489,9 @@ static ClimateNode *climate; // current climate static Fluxes fluxes; static double *outputPtrs[MAX_DATA_TYPES]; // pointers to different possible outputs - +#ifdef EVENT_HANDLER +static EventNode **events; // MJL: event structs +#endif /* Read climate file into linked lists, make firstClimates be a vector where each element is a pointer to the head of a list corresponding to one spatial location @@ -934,8 +938,8 @@ void calcLightEff3 (double *lightEff, double lai, double par) { coeff = 1; int err; double fAPAR; // Calculation of fAPAR according to 1 - exp(attenuation*LAI) - double APAR; // Absorbed PAR by the canopy - double lightIntensityTop, lightIntensityBottom; // PAR absorbed by the canopy + //double APAR; // Absorbed PAR by the canopy + //double lightIntensityTop, lightIntensityBottom; // PAR absorbed by the canopy cumfAPAR = 0.0; @@ -966,11 +970,11 @@ void calcLightEff3 (double *lightEff, double lai, double par) { fAPAR = cumfAPAR/(3.0*NUM_LAYERS); // the average value, multiplying by h/3 in Simpson's rule, and dividing by the number of steps - lightIntensityTop = par; // Energy at the top of the canopy - lightIntensityBottom = par * exp(-1.0 * params.attenuation * lai); // LAI at the bottom of the canopy + //lightIntensityTop = par; // Energy at the top of the canopy + //lightIntensityBottom = par * exp(-1.0 * params.attenuation * lai); // LAI at the bottom of the canopy // between 0 and par // this is the amount of incident par - APAR = params.m_ballBerry * (lightIntensityTop - lightIntensityBottom); // APAR at this layer + //APAR = params.m_ballBerry * (lightIntensityTop - lightIntensityBottom); // APAR at this layer // is a fraction of the difference between incoming par and transmitted par @@ -1932,8 +1936,8 @@ void calculateFluxes() { double potGrossPsn; // potential photosynthesis, without water stress double dWater; double lai; // m^2 leaf/m^2 ground (calculated from plantLeafC) - double litterBreakdown; /* total litter breakdown (i.e. litterToSoil + rLitter) - (g C/m^2 ground/day) */ + //double litterBreakdown; /* total litter breakdown (i.e. litterToSoil + rLitter) + // (g C/m^2 ground/day) */ double folResp, woodResp; // maintenance respiration terms, g C * m^-2 ground area * day^-1 double litterWater, soilWater; /* amount of water in litter and soil (cm) taken from either environment or climate drivers, depending on value of MODEL_WATER */ @@ -2005,7 +2009,7 @@ void calculateFluxes() { fluxes.litterToSoil = litterBreakdown * (1.0 - params.fracLitterRespired); // NOTE: right now, we don't have capability to use separate cold soil params for litter #else - litterBreakdown = 0; + // litterBreakdown = 0; fluxes.rLitter = 0; fluxes.litterToSoil = 0; #endif @@ -2479,6 +2483,12 @@ void setupModel(SpatialParams *spatialParams, int loc) { } +// Setup events at given location +void setupEvents(int currLoc) { + +} + + /* Do one run of the model using parameter values in spatialParams If out != NULL, output results to out If printHeader = 1, print a header for the output file, if 0 don't @@ -2504,6 +2514,9 @@ void runModelOutput(FILE *out, OutputItems *outputItems, int printHeader, Spatia for (currLoc = firstLoc; currLoc <= lastLoc; currLoc++) { setupModel(spatialParams, currLoc); +#ifdef EVENT_HANDLER + setupEvents(currLoc); +#endif if ((loc == -1) && (outputItems != NULL)) { // print the current location at the start of the line sprintf(label, "%d", currLoc); writeOutputItemLabels(outputItems, label); @@ -2511,10 +2524,12 @@ void runModelOutput(FILE *out, OutputItems *outputItems, int printHeader, Spatia while (climate != NULL) { updateState(); - if (out != NULL) - outputState(out, currLoc, climate->year, climate->day, climate->time); - if (outputItems != NULL) - writeOutputItemValues(outputItems); + if (out != NULL) { + outputState(out, currLoc, climate->year, climate->day, climate->time); + } + if (outputItems != NULL) { + writeOutputItemValues(outputItems); + } climate = climate->nextClim; } if (outputItems != NULL) @@ -2775,8 +2790,14 @@ int initModel(SpatialParams **spatialParams, int **steps, char *paramFile, char return numLocs; } - - +/* Do initialization of event data if event handling is turned on. + * Populates static event structs + */ +void initEvents(char *eventFile, int numLocs) { +#ifdef EVENT_HANDLER + events = readEventData(eventFile, numLocs); +#endif +} // call this when done running model: // de-allocates space for climate linked list // (needs to know number of locations) diff --git a/sipnet.h b/sipnet.h index 3378d40..6c8278b 100755 --- a/sipnet.h +++ b/sipnet.h @@ -51,6 +51,10 @@ char **getDataTypeNames(); */ int initModel(SpatialParams **spatialParams, int **steps, char *paramFile, char *climFile); +/* + * TBD + */ +void initEvents(char *eventFile, int numLocs); // call this when done running model: // de-allocates space for climate linked list diff --git a/tests/sipnet/test_events/Makefile b/tests/sipnet/test_events/Makefile new file mode 100644 index 0000000..261cb8a --- /dev/null +++ b/tests/sipnet/test_events/Makefile @@ -0,0 +1,27 @@ +CC=gcc +LD=gcc +CFLAGS=-Wall -g +LDFLAGS=-L../../.. +LDLIBS=-lsipnet +#INCLUDE=../../../. + +# List test files in this directory here +TEST_CFILES=testEventInfra.c +TEST_OBJ_FILES=$(TEST_CFILES:%.c=%.o) +TEST_EXECUTABLES:=$(basename $(TEST_CFILES)) + +all: tests + +tests: $(TEST_EXECUTABLES) + +$(TEST_EXECUTABLES): %: %.o + $(CC) $(LDFLAGS) $< $(LDLIBS) -o $@ + +$(TEST_OBJ_FILES): ../utils.h ../../../libsipnet.a +$(TEST_OBJ_FILES): %.o: %.c + $(CC) $(CFLAGS) -c -o $@ $< + +clean: + rm -f $(TEST_OBJ_FILES) $(TEST_EXECUTABLES) events.in + +.PHONY: all tests clean diff --git a/tests/sipnet/test_events/infra_events.in b/tests/sipnet/test_events/infra_events.in new file mode 100644 index 0000000..ef0169f --- /dev/null +++ b/tests/sipnet/test_events/infra_events.in @@ -0,0 +1,5 @@ +0 2022 40 irrig 5 0 # 5cm canopy irrigation on day 40 +0 2022 40 fert 15 5 10 # fertilized with 15 g/m2 organic N, 5 g/m2 organic C, and 10 g/m2 mineral N on day 40 +0 2022 45 till 0.1 0.2 0.3 # tilled on day 45, 10% of surface litter transferred to soil pool, soil organic matter pool decomposition rate increases by 20% and soil litter pool decomposition rate increases by 30% +0 2022 46 plant 10 10 1 # planting - planting occurs on day 40, after 10 days the plants have emerged with a size of 10 g C/m2 and 1 g N / m2 +0 2022 250 harv 0.4 0.1 0.2 0.3 # harvest 10% of aboveground plant biomass on day 250 diff --git a/tests/sipnet/test_events/modelStructures.h b/tests/sipnet/test_events/modelStructures.h new file mode 100644 index 0000000..5a591bb --- /dev/null +++ b/tests/sipnet/test_events/modelStructures.h @@ -0,0 +1,17 @@ +// +// This file is inteneded to hold settings for different model structure options, +// implemented as compile-time flags. The options are here (with nothing else) to +// improve testability. +// + +#ifndef MODEL_STRUCTURES_H +#define MODEL_STRUCTURES_H + +// Begin definitions for choosing different model structures +// See also sipnet.c for other options (that should be moved here if/when testing is added) + +#define EVENT_HANDLER 1 +// Read in and process agronomic events. Expects a file named .event to exist. + + +#endif //MODEL_STRUCTURES_H diff --git a/tests/sipnet/test_events/testEventInfra.c b/tests/sipnet/test_events/testEventInfra.c new file mode 100644 index 0000000..8081bae --- /dev/null +++ b/tests/sipnet/test_events/testEventInfra.c @@ -0,0 +1,152 @@ + +#include +#include +#include + +#include "../utils.h" +#include "../../../events.c" + +int checkEvent(EventNode* event, int loc, int year, int day, enum EventType type) { + int success = + event->loc == loc && + event->year == year && + event->day == day && + event->type == type; + if (!success) { + printf("Error checking event\n"); + printEvent(event); + return 1; + } + return 0; +} + +int checkHarvestParams( + HarvestParams *params, double fracRemAbove, + double fracRemBelow, + double fracTransAbove, + double fracTransBelow +) { + return !( + compareDoubles(params->fractionRemovedAbove, fracRemAbove) && + compareDoubles(params->fractionRemovedBelow, fracRemBelow) && + compareDoubles(params->fractionTransferredAbove, fracTransAbove) && + compareDoubles(params->fractionTransferredBelow, fracTransBelow) + ); +} + +int checkIrrigationParams( + IrrigationParams *params, + double amountAdded, + int location + ) { + return !( + compareDoubles(params->amountAdded, amountAdded) && + params->location == location + ); +} + +int checkFertilizationParams( + FertilizationParams *params, + double orgN, + double orgC, + double minN +) { + return !( + compareDoubles(params->orgN, orgN) && + compareDoubles(params->orgC, orgC) && + compareDoubles(params->minN, minN) + ); +} + +int checkPlantingParams( + PlantingParams *params, int emergenceLag, double addedC, double addedN + ) { + return !( + params->emergenceLag == emergenceLag && + compareDoubles(params->addedC, addedC) && + compareDoubles(params->addedN, addedN) + ); +} + +int checkTillageParams( + TillageParams *params, + double fracLitterTransferred, + double somDecompModifier, + double litterDecompModifier + ) { + return !( + compareDoubles(params->fractionLitterTransferred, fracLitterTransferred) && + compareDoubles(params->somDecompModifier, somDecompModifier) && + compareDoubles(params->litterDecompModifier, litterDecompModifier) + ); +} + +int init() { + // char * filename = "infra_events.in"; + // if (access(filename, F_OK) == -1) { + // return 1; + // } + // + // return copyFile(filename, "events.in"); + return 0; +} + +int run() { + int numLocs = 1; + EventNode** output = readEventData("infra_events.in", numLocs); + + if (!output) { + return 1; + } + + // check output is correct + int status = 0; + EventNode *event = output[0]; + status |= checkEvent(event, 0, 2022, 40, IRRIGATION); + status |= checkIrrigationParams((IrrigationParams*)event->eventParams, 5, 0); + event = event->nextEvent; + status |= checkEvent(event, 0, 2022, 40, FERTILIZATION); + status |= checkFertilizationParams((FertilizationParams*)event->eventParams, 15, 5,10); + event = event->nextEvent; + status |= checkEvent(event, 0, 2022, 45, TILLAGE); + status |= checkTillageParams((TillageParams*)event->eventParams, 0.1, 0.2, 0.3); + event = event->nextEvent; + status |= checkEvent(event, 0, 2022, 46, PLANTING); + status |= checkPlantingParams((PlantingParams*)event->eventParams, 10, 10, 1); + event = event->nextEvent; + status |= checkEvent(event, 0, 2022, 250, HARVEST); + status |= checkHarvestParams((HarvestParams*)event->eventParams, 0.4, 0.1, 0.2, 0.3); + + if (status) { + return 1; + } + + return 0; +} + +void cleanup() { + // Perform any cleanup as needed + // None needed here, we can leave the copied file +} + +int main() { + int status; + status = init(); + if (status) { + printf("Test initialization failed with status %d\n", status); + exit(status); + } else { + printf("Test initialized\n"); + } + + printf("Starting run()\n"); + status = run(); + if (status) { + printf("Test run failed with status %d\n", status); + exit(status); + } + + printf("testEventInfra PASSED\n"); + + cleanup(); +} \ No newline at end of file diff --git a/tests/sipnet/utils.h b/tests/sipnet/utils.h new file mode 100644 index 0000000..54f3429 --- /dev/null +++ b/tests/sipnet/utils.h @@ -0,0 +1,37 @@ +#ifndef UTILS_H +#define UTILS_H + +#include +#include + +extern inline int copyFile(char* src, char* dest) { + FILE *source = fopen(src, "rb"); // Binary mode for compatibility + if (source == NULL) { + printf("Error opening source file %s", src); + return 1; + } + + FILE *destination = fopen(dest, "wb"); + if (destination == NULL) { + printf("Error opening destination file %s", dest); + fclose(source); + return 1; + } + + char buffer[4096]; // 4KB buffer + size_t bytesRead; + + while ((bytesRead = fread(buffer, 1, sizeof(buffer), source)) > 0) { + fwrite(buffer, 1, bytesRead, destination); + } + + fclose(source); + fclose(destination); + return 0; +} + +extern inline int compareDoubles(double a, double b) { + return fabs(a-b) < 1e-6; +} + +#endif //UTILS_H diff --git a/tests/test.template b/tests/test.template new file mode 100644 index 0000000..4ec9aec --- /dev/null +++ b/tests/test.template @@ -0,0 +1,37 @@ +// Unit test template file +// +// Copy and modify as needed (and remove these comments!) + +#include +#include + +int init() { + // Initialization steps for the test; eg copying files, allocating mem, etc + + // Return a status code, 0 = success +} + +int run() { + // Run the tests, of course + + // Return a status code, 0 = success +} + +void cleanup() { + // Perform any cleanup as needed +} + +int main() { + int status; + status = init(); + if (status) { + printf("Test initialization failed with status %d\b", status); + } + + status = run(); + if (status) { + printf("Test run failed with status %d\b", status); + } + + cleanup(); +} \ No newline at end of file From 7418af62aba94ab6407f4fa58ddae40be795b5f7 Mon Sep 17 00:00:00 2001 From: Alomir Date: Thu, 19 Dec 2024 12:15:25 -0500 Subject: [PATCH 02/16] Sets event handler off by default --- modelStructures.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modelStructures.h b/modelStructures.h index 5a591bb..e0c8014 100644 --- a/modelStructures.h +++ b/modelStructures.h @@ -10,7 +10,7 @@ // Begin definitions for choosing different model structures // See also sipnet.c for other options (that should be moved here if/when testing is added) -#define EVENT_HANDLER 1 +#define EVENT_HANDLER 0 // Read in and process agronomic events. Expects a file named .event to exist. From 1e9b7e62246327115f7328914ff5caaeeb890e83 Mon Sep 17 00:00:00 2001 From: Alomir Date: Thu, 19 Dec 2024 12:19:48 -0500 Subject: [PATCH 03/16] Sets event handler off by default for realz, I hope --- frontend.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend.c b/frontend.c index c79633f..96333f7 100755 --- a/frontend.c +++ b/frontend.c @@ -156,9 +156,9 @@ int main(int argc, char *argv[]) { strcat(climFile, ".clim"); numLocs = initModel(&spatialParams, &steps, paramFile, climFile); -#ifdef EVENT_HANDLER strcpy(eventFile, fileName); strcat(eventFile, ".event"); +#if EVENT_HANDLER initEvents(eventFile, numLocs); #endif From 86175afc7cfe5d3f48ba2b1fdbde3a343ae5edb3 Mon Sep 17 00:00:00 2001 From: Alomir Date: Thu, 19 Dec 2024 12:32:49 -0500 Subject: [PATCH 04/16] Adds ci steps to run unit tests --- .github/workflows/ci.yml | 24 ++++++++++++++++++++++++ Makefile | 8 +++++++- tests/sipnet/test_events/Makefile | 9 +++++++-- 3 files changed, 38 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0c2880a..1da462f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,6 +9,30 @@ on: jobs: + # Build and run unit tests + test: + strategy: + fail-fast: false + matrix: + OS: + - macos-latest + - ubuntu-latest + - ubuntu-20.04 + + runs-on: ${{ matrix.OS }} + + steps: + # checkout source code + - uses: actions/checkout@v2 + + # compile tests + - name: compile tests + run: make test + + # run tests + - name: run tests + run: make testrun + # try to build SIPNET on Ubuntu & MacOS build: strategy: diff --git a/Makefile b/Makefile index 9b2a37c..6adbfe3 100755 --- a/Makefile +++ b/Makefile @@ -46,6 +46,7 @@ clean: # # UNIT TESTS SIPNET_TEST_DIRS:=$(shell find tests/sipnet -type d -mindepth 1 -maxdepth 1) +SIPNET_TEST_DIRS_RUN:= $(addsuffix .run, $(SIPNET_TEST_DIRS)) SIPNET_TEST_DIRS_CLEAN:= $(addsuffix .clean, $(SIPNET_TEST_DIRS)) SIPNET_LIB=libsipnet.a @@ -67,13 +68,18 @@ $(SIPNET_TEST_DIRS): pretest $(SIPNET_LIB) posttest: $(SIPNET_TEST_DIRS) mv modelStructures.orig.h modelStructures.h +testrun: $(SIPNET_TEST_DIRS_RUN) + +$(SIPNET_TEST_DIRS_RUN): + $(MAKE) -C $(basename $@) run + testclean: $(SIPNET_TEST_DIRS_CLEAN) rm -f $(SIPNET_LIB) $(SIPNET_TEST_DIRS_CLEAN): $(MAKE) -C $(basename $@) clean -.PHONY: all test $(SIPNET_TEST_DIRS) pretest posttest $(SIPNET_LIB) testclean $(SIPNET_TEST_DIRS_CLEAN) +.PHONY: all test $(SIPNET_TEST_DIRS) pretest posttest $(SIPNET_LIB) testrun $(SIPNET_TEST_DIRS_RUN) testclean $(SIPNET_TEST_DIRS_CLEAN) #This target automatically builds dependencies. diff --git a/tests/sipnet/test_events/Makefile b/tests/sipnet/test_events/Makefile index 261cb8a..dbf1d02 100644 --- a/tests/sipnet/test_events/Makefile +++ b/tests/sipnet/test_events/Makefile @@ -3,12 +3,12 @@ LD=gcc CFLAGS=-Wall -g LDFLAGS=-L../../.. LDLIBS=-lsipnet -#INCLUDE=../../../. # List test files in this directory here TEST_CFILES=testEventInfra.c TEST_OBJ_FILES=$(TEST_CFILES:%.c=%.o) TEST_EXECUTABLES:=$(basename $(TEST_CFILES)) +RUN_EXECUTABLES:= $(addsuffix .run, $(TEST_EXECUTABLES)) all: tests @@ -21,7 +21,12 @@ $(TEST_OBJ_FILES): ../utils.h ../../../libsipnet.a $(TEST_OBJ_FILES): %.o: %.c $(CC) $(CFLAGS) -c -o $@ $< +run: $(RUN_EXECUTABLES) + +$(RUN_EXECUTABLES): + ./$(basename $@) + clean: rm -f $(TEST_OBJ_FILES) $(TEST_EXECUTABLES) events.in -.PHONY: all tests clean +.PHONY: all tests clean run $(RUN_EXECUTABLES) From f9690e3a6be3b6b4633cc4c774cdae4248df5649 Mon Sep 17 00:00:00 2001 From: Alomir Date: Thu, 19 Dec 2024 13:57:25 -0500 Subject: [PATCH 05/16] Adds math lib to linux build linking --- tests/sipnet/test_events/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/sipnet/test_events/Makefile b/tests/sipnet/test_events/Makefile index dbf1d02..0da516a 100644 --- a/tests/sipnet/test_events/Makefile +++ b/tests/sipnet/test_events/Makefile @@ -2,7 +2,7 @@ CC=gcc LD=gcc CFLAGS=-Wall -g LDFLAGS=-L../../.. -LDLIBS=-lsipnet +LDLIBS=-lsipnet -lm # List test files in this directory here TEST_CFILES=testEventInfra.c From a10988b28bfeca9c2f764f9c0aac9906d5198695 Mon Sep 17 00:00:00 2001 From: Alomir Date: Thu, 19 Dec 2024 17:04:06 -0500 Subject: [PATCH 06/16] Adds negative tests and an exit function handler --- events.c | 7 +- tests/sipnet/exitHandler.c | 47 ++++++++++ tests/sipnet/test_events/Makefile | 6 +- .../test_events/infra_events_date_ooo.in | 2 + .../test_events/infra_events_loc_ooo.in | 3 + .../sipnet/test_events/infra_events_multi.in | 15 ++++ ...infra_events.in => infra_events_simple.in} | 2 +- .../test_events/infra_events_unknown.in | 1 + tests/sipnet/test_events/testEventInfra.c | 86 ++++++++++++++++++- tests/sipnet/test_events/testEventInfraNeg.c | 68 +++++++++++++++ 10 files changed, 226 insertions(+), 11 deletions(-) create mode 100644 tests/sipnet/exitHandler.c create mode 100644 tests/sipnet/test_events/infra_events_date_ooo.in create mode 100644 tests/sipnet/test_events/infra_events_loc_ooo.in create mode 100644 tests/sipnet/test_events/infra_events_multi.in rename tests/sipnet/test_events/{infra_events.in => infra_events_simple.in} (90%) create mode 100644 tests/sipnet/test_events/infra_events_unknown.in create mode 100644 tests/sipnet/test_events/testEventInfraNeg.c diff --git a/events.c b/events.c index b8c48cc..a152673 100644 --- a/events.c +++ b/events.c @@ -135,7 +135,6 @@ EventNode** readEventData(char *eventFile, int numLocs) { printf("Error: unknown event type %s\n", eventTypeStr); exit(1); } - printf("Found event type %d (%s)\n", eventType, eventTypeStr); next = createEventNode(loc, year, day, eventType, eventParamsStr); events[loc] = next; @@ -143,8 +142,8 @@ EventNode** readEventData(char *eventFile, int numLocs) { currYear = year; currDay = day; - printf("Found events:\n"); - printEvent(next); + //printf("Found events:\n"); + //printEvent(next); while (fgets(line, EVENT_LINE_SIZE, in) != NULL) { // We have another event @@ -172,7 +171,7 @@ EventNode** readEventData(char *eventFile, int numLocs) { } next = createEventNode(loc, year, day, eventType, eventParamsStr); - printEvent(next); + //printEvent(next); if (currLoc == loc) { // Same location, add the new event to this location's list curr->nextEvent = next; diff --git a/tests/sipnet/exitHandler.c b/tests/sipnet/exitHandler.c new file mode 100644 index 0000000..acda85e --- /dev/null +++ b/tests/sipnet/exitHandler.c @@ -0,0 +1,47 @@ +#include +#include + +static int expected_code = 1; // the expected value a tested function passes to exit +static int should_exit = 1; // 1 if exit should have been called +static int really_exit = 0; // set to 1 to prevent stubbing behavior and actually exit + +static jmp_buf jump_env; + +static int exit_result = 1; +#define test_assert(x) (exit_result = exit_result && (x)) + +// set should_exit=0 when code SHOULDN'T exit(), e.g.: +// +// should_exit = 0; +// if (!(jmp_rval=setjmp(jump_env))) +// { +// call_to_non_exiting_code(); +// } +// test_assert(jmp_rval==0); +// +// set should_exit=1 when exit(0 is expected: +// +// should_exit = 1; +// expected_code = 1; // or whatever the exit code should be +// if (!(jmp_rval=setjmp(jump_env))) +// { +// call_to_exiting_code(); +// } +// +// test_assert(jmp_rval==1); + +// stub function +void exit(int code) +{ + if (!really_exit) + { + printf("Mocking the exit call\n"); + test_assert(should_exit==1); + test_assert(expected_code==code); + longjmp(jump_env, 1); + } + else + { + _exit(code); + } +} diff --git a/tests/sipnet/test_events/Makefile b/tests/sipnet/test_events/Makefile index 0da516a..d224b95 100644 --- a/tests/sipnet/test_events/Makefile +++ b/tests/sipnet/test_events/Makefile @@ -5,7 +5,9 @@ LDFLAGS=-L../../.. LDLIBS=-lsipnet -lm # List test files in this directory here -TEST_CFILES=testEventInfra.c +TEST_CFILES=testEventInfra.c testEventInfraNeg.c + +# The rest is boilerplate, likely copyable as is to a new test directory TEST_OBJ_FILES=$(TEST_CFILES:%.c=%.o) TEST_EXECUTABLES:=$(basename $(TEST_CFILES)) RUN_EXECUTABLES:= $(addsuffix .run, $(TEST_EXECUTABLES)) @@ -17,7 +19,7 @@ tests: $(TEST_EXECUTABLES) $(TEST_EXECUTABLES): %: %.o $(CC) $(LDFLAGS) $< $(LDLIBS) -o $@ -$(TEST_OBJ_FILES): ../utils.h ../../../libsipnet.a +$(TEST_OBJ_FILES): ../utils.h ../../../libsipnet.a ../exitHandler.c $(TEST_OBJ_FILES): %.o: %.c $(CC) $(CFLAGS) -c -o $@ $< diff --git a/tests/sipnet/test_events/infra_events_date_ooo.in b/tests/sipnet/test_events/infra_events_date_ooo.in new file mode 100644 index 0000000..a39aa78 --- /dev/null +++ b/tests/sipnet/test_events/infra_events_date_ooo.in @@ -0,0 +1,2 @@ +0 2024 60 till 0.1 0.2 0.3 +0 2024 55 plant 15 20 5 diff --git a/tests/sipnet/test_events/infra_events_loc_ooo.in b/tests/sipnet/test_events/infra_events_loc_ooo.in new file mode 100644 index 0000000..2678621 --- /dev/null +++ b/tests/sipnet/test_events/infra_events_loc_ooo.in @@ -0,0 +1,3 @@ +0 2024 60 till 0.1 0.2 0.3 +1 2024 65 plant 15 20 5 +0 2024 70 irrig 5 1 diff --git a/tests/sipnet/test_events/infra_events_multi.in b/tests/sipnet/test_events/infra_events_multi.in new file mode 100644 index 0000000..9013e35 --- /dev/null +++ b/tests/sipnet/test_events/infra_events_multi.in @@ -0,0 +1,15 @@ +0 2024 60 till 0.1 0.2 0.3 +0 2024 65 plant 15 20 5 +0 2024 70 irrig 5 1 +0 2024 75 fert 15 10 5 +0 2024 250 harv 0.1 0.2 0.3 0.4 +1 2024 59 till 0.2 0.3 0.1 +1 2024 64 plant 15 20 5 +1 2024 69 irrig 5 0 +1 2024 74 fert 5 15 10 +1 2024 249 harv 0.2 0.3 0.4 0.1 +2 2024 58 till 0.3 0.1 0.2 +2 2024 63 plant 15 20 5 +2 2024 68 irrig 6 1 +2 2024 73 fert 5 10 15 +2 2024 248 harv 0.3 0.4 0.1 0.2 diff --git a/tests/sipnet/test_events/infra_events.in b/tests/sipnet/test_events/infra_events_simple.in similarity index 90% rename from tests/sipnet/test_events/infra_events.in rename to tests/sipnet/test_events/infra_events_simple.in index ef0169f..506cf14 100644 --- a/tests/sipnet/test_events/infra_events.in +++ b/tests/sipnet/test_events/infra_events_simple.in @@ -1,4 +1,4 @@ -0 2022 40 irrig 5 0 # 5cm canopy irrigation on day 40 +0 2022 40 irrig 5 0 # 5cm canopy irrigation on day 40 0 2022 40 fert 15 5 10 # fertilized with 15 g/m2 organic N, 5 g/m2 organic C, and 10 g/m2 mineral N on day 40 0 2022 45 till 0.1 0.2 0.3 # tilled on day 45, 10% of surface litter transferred to soil pool, soil organic matter pool decomposition rate increases by 20% and soil litter pool decomposition rate increases by 30% 0 2022 46 plant 10 10 1 # planting - planting occurs on day 40, after 10 days the plants have emerged with a size of 10 g C/m2 and 1 g N / m2 diff --git a/tests/sipnet/test_events/infra_events_unknown.in b/tests/sipnet/test_events/infra_events_unknown.in new file mode 100644 index 0000000..2e7b097 --- /dev/null +++ b/tests/sipnet/test_events/infra_events_unknown.in @@ -0,0 +1 @@ +0 2022 40 blarg 5 0 diff --git a/tests/sipnet/test_events/testEventInfra.c b/tests/sipnet/test_events/testEventInfra.c index 8081bae..6538b49 100644 --- a/tests/sipnet/test_events/testEventInfra.c +++ b/tests/sipnet/test_events/testEventInfra.c @@ -1,7 +1,6 @@ #include #include -#include #include "../utils.h" #include "../../../events.c" @@ -82,7 +81,7 @@ int checkTillageParams( } int init() { - // char * filename = "infra_events.in"; + // char * filename = "infra_events_simple.in"; // if (access(filename, F_OK) == -1) { // return 1; // } @@ -91,9 +90,10 @@ int init() { return 0; } -int run() { +int runTestSimple() { + // Simple test with one loc, one event per type int numLocs = 1; - EventNode** output = readEventData("infra_events.in", numLocs); + EventNode** output = readEventData("infra_events_simple.in", numLocs); if (!output) { return 1; @@ -117,6 +117,84 @@ int run() { status |= checkEvent(event, 0, 2022, 250, HARVEST); status |= checkHarvestParams((HarvestParams*)event->eventParams, 0.4, 0.1, 0.2, 0.3); + return status; +} + +int runTestMulti() { + // More complex test; multiple locations + int numLocs = 1; + EventNode** output = readEventData("infra_events_multi.in", numLocs); + + if (!output) { + return 1; + } + + // check output is correct + int status = 0; + int loc = 0; + EventNode *event = output[loc]; + status |= checkEvent(event, loc, 2024, 60, TILLAGE); + status |= checkTillageParams((TillageParams*)event->eventParams, 0.1, 0.2, 0.3); + event = event->nextEvent; + status |= checkEvent(event, loc, 2024, 65, PLANTING); + status |= checkPlantingParams((PlantingParams*)event->eventParams, 15, 20, 5); + event = event->nextEvent; + status |= checkEvent(event, loc, 2024, 70, IRRIGATION); + status |= checkIrrigationParams((IrrigationParams*)event->eventParams, 5, 1); + event = event->nextEvent; + status |= checkEvent(event, loc, 2024, 75, FERTILIZATION); + status |= checkFertilizationParams((FertilizationParams*)event->eventParams, 15, 10,5); + event = event->nextEvent; + status |= checkEvent(event, loc, 2024, 250, HARVEST); + status |= checkHarvestParams((HarvestParams*)event->eventParams, 0.1, 0.2, 0.3, 0.4); + + loc++; + event = output[loc]; + status |= checkEvent(event, loc, 2024, 59, TILLAGE); + status |= checkTillageParams((TillageParams*)event->eventParams, 0.2, 0.3, 0.1); + event = event->nextEvent; + status |= checkEvent(event, loc, 2024, 64, PLANTING); + status |= checkPlantingParams((PlantingParams*)event->eventParams, 15, 20, 5); + event = event->nextEvent; + status |= checkEvent(event, loc, 2024, 69, IRRIGATION); + status |= checkIrrigationParams((IrrigationParams*)event->eventParams, 5, 0); + event = event->nextEvent; + status |= checkEvent(event, loc, 2024, 74, FERTILIZATION); + status |= checkFertilizationParams((FertilizationParams*)event->eventParams, 5, 15,10); + event = event->nextEvent; + status |= checkEvent(event, loc, 2024, 249, HARVEST); + status |= checkHarvestParams((HarvestParams*)event->eventParams, 0.2, 0.3, 0.4, 0.1); + + loc++; + event = output[loc]; + status |= checkEvent(event, loc, 2024, 58, TILLAGE); + status |= checkTillageParams((TillageParams*)event->eventParams, 0.3, 0.1, 0.2); + event = event->nextEvent; + status |= checkEvent(event, loc, 2024, 63, PLANTING); + status |= checkPlantingParams((PlantingParams*)event->eventParams, 15, 20, 5); + event = event->nextEvent; + status |= checkEvent(event, loc, 2024, 68, IRRIGATION); + status |= checkIrrigationParams((IrrigationParams*)event->eventParams, 6, 1); + event = event->nextEvent; + status |= checkEvent(event, loc, 2024, 73, FERTILIZATION); + status |= checkFertilizationParams((FertilizationParams*)event->eventParams, 5, 10,15); + event = event->nextEvent; + status |= checkEvent(event, loc, 2024, 248, HARVEST); + status |= checkHarvestParams((HarvestParams*)event->eventParams, 0.3, 0.4, 0.1, 0.2); + + return status; +} + +int run() { + + int status; + + status = runTestSimple(); + if (status) { + return 1; + } + + status = runTestMulti(); if (status) { return 1; } diff --git a/tests/sipnet/test_events/testEventInfraNeg.c b/tests/sipnet/test_events/testEventInfraNeg.c new file mode 100644 index 0000000..eb7d06a --- /dev/null +++ b/tests/sipnet/test_events/testEventInfraNeg.c @@ -0,0 +1,68 @@ + +#include +#include + +#include "../../../events.c" +#include "../exitHandler.c" + +int run() { + int status = 0; + + // exit() handling params + int jmp_rval; + expected_code = 1; + exit_result = 1; + + // Step 0: make sure that we don't have a false positive on good data + should_exit = 0; + jmp_rval = setjmp(jump_env); + if (!jmp_rval) { + readEventData("infra_events_simple.in", 1); + } + test_assert(jmp_rval==0); + status |= !exit_result; + + // First test + should_exit = 1; + jmp_rval = setjmp(jump_env); + if (!jmp_rval) { + readEventData("infra_events_unknown.in", 1); + } + printf("status %d exit_res %d jmp_rval %d\n", status, exit_result, jmp_rval); + test_assert(jmp_rval==1); + status |= !exit_result; + + // Second test + jmp_rval = setjmp(jump_env); + if (!jmp_rval) { + readEventData("infra_events_loc_ooo.in", 1); + } + test_assert(jmp_rval==1); + status |= !exit_result; + + // Third test + jmp_rval = setjmp(jump_env); + if (!jmp_rval) { + readEventData("infra_events_date_ooo.in", 1); + } + test_assert(jmp_rval==1); + status |= !exit_result; + + // Allow a real exit, not that this is really needed + really_exit = 1; + + return status; +} + +int main() { + int status; + + printf("Starting testEventInfraNeg:run()\n"); + status = run(); + if (status) { + printf("Test run failed with status %d\n", status); + exit(status); + } + + printf("testEventInfra PASSED\n"); +} \ No newline at end of file From e9ed8b0f51ad5d90cc9c3ca666f1510072573196 Mon Sep 17 00:00:00 2001 From: Alomir Date: Tue, 7 Jan 2025 16:21:27 -0500 Subject: [PATCH 07/16] Code cleanup 1 for PR --- events.c | 4 ---- events.h | 5 +++-- frontend.c | 2 +- modelStructures.h | 3 ++- sipnet.c | 23 ++++++++++++----------- sipnet.h | 9 ++++++++- tests/sipnet/exitHandler.c | 14 +++++++------- tests/test.template | 2 +- 8 files changed, 34 insertions(+), 28 deletions(-) diff --git a/events.c b/events.c index a152673..b4f3027 100644 --- a/events.c +++ b/events.c @@ -142,9 +142,6 @@ EventNode** readEventData(char *eventFile, int numLocs) { currYear = year; currDay = day; - //printf("Found events:\n"); - //printEvent(next); - while (fgets(line, EVENT_LINE_SIZE, in) != NULL) { // We have another event curr = next; @@ -171,7 +168,6 @@ EventNode** readEventData(char *eventFile, int numLocs) { } next = createEventNode(loc, year, day, eventType, eventParamsStr); - //printEvent(next); if (currLoc == loc) { // Same location, add the new event to this location's list curr->nextEvent = next; diff --git a/events.h b/events.h index de027a6..f96f27a 100644 --- a/events.h +++ b/events.h @@ -55,11 +55,12 @@ struct EventNode { EventNode *nextEvent; }; -/* Read event data from .event +/* Read event data from input filename (canonically events.in) * * Format: returned data is structured as an array of EventNode pointers, indexed by * location. Each element of the array is the first event for that location (or null - * if there are no events). It is assumed that the events are in chrono order. + * if there are no events). It is assumed that the events are ordered first by location + * and then by year and day. */ EventNode** readEventData(char *eventFile, int numLocs); diff --git a/frontend.c b/frontend.c index 96333f7..302929c 100755 --- a/frontend.c +++ b/frontend.c @@ -82,7 +82,7 @@ int main(int argc, char *argv[]) { double **standarddevs; int dataTypeIndices[MAX_DATA_TYPES]; -#ifdef EVENT_HANDLER +#if EVENT_HANDLER // Extra filename if we are handing events char eventFile[FILE_MAXNAME+24]; #endif diff --git a/modelStructures.h b/modelStructures.h index e0c8014..207287a 100644 --- a/modelStructures.h +++ b/modelStructures.h @@ -11,7 +11,8 @@ // See also sipnet.c for other options (that should be moved here if/when testing is added) #define EVENT_HANDLER 0 -// Read in and process agronomic events. Expects a file named .event to exist. +// Read in and process agronomic events. SIPNET expects a file named events.in to exist, though +// unit tests may use other names. #endif //MODEL_STRUCTURES_H diff --git a/sipnet.c b/sipnet.c index ee6f717..ddcdd0f 100644 --- a/sipnet.c +++ b/sipnet.c @@ -489,8 +489,8 @@ static ClimateNode *climate; // current climate static Fluxes fluxes; static double *outputPtrs[MAX_DATA_TYPES]; // pointers to different possible outputs -#ifdef EVENT_HANDLER -static EventNode **events; // MJL: event structs +#if EVENT_HANDLER +static EventNode **events; #endif /* Read climate file into linked lists, @@ -1936,8 +1936,8 @@ void calculateFluxes() { double potGrossPsn; // potential photosynthesis, without water stress double dWater; double lai; // m^2 leaf/m^2 ground (calculated from plantLeafC) - //double litterBreakdown; /* total litter breakdown (i.e. litterToSoil + rLitter) - // (g C/m^2 ground/day) */ + //double litterBreakdown; /* total litter breakdown (i.e. litterToSoil + rLitter) + //(g C/m^2 ground/day) */ double folResp, woodResp; // maintenance respiration terms, g C * m^-2 ground area * day^-1 double litterWater, soilWater; /* amount of water in litter and soil (cm) taken from either environment or climate drivers, depending on value of MODEL_WATER */ @@ -2485,7 +2485,7 @@ void setupModel(SpatialParams *spatialParams, int loc) { // Setup events at given location void setupEvents(int currLoc) { - + // Implementation TBD } @@ -2514,8 +2514,8 @@ void runModelOutput(FILE *out, OutputItems *outputItems, int printHeader, Spatia for (currLoc = firstLoc; currLoc <= lastLoc; currLoc++) { setupModel(spatialParams, currLoc); -#ifdef EVENT_HANDLER - setupEvents(currLoc); +#if EVENT_HANDLER + setupEvents(currLoc); #endif if ((loc == -1) && (outputItems != NULL)) { // print the current location at the start of the line sprintf(label, "%d", currLoc); @@ -2525,15 +2525,16 @@ void runModelOutput(FILE *out, OutputItems *outputItems, int printHeader, Spatia while (climate != NULL) { updateState(); if (out != NULL) { - outputState(out, currLoc, climate->year, climate->day, climate->time); + outputState(out, currLoc, climate->year, climate->day, climate->time); } if (outputItems != NULL) { - writeOutputItemValues(outputItems); + writeOutputItemValues(outputItems); } climate = climate->nextClim; } - if (outputItems != NULL) + if (outputItems != NULL) { terminateOutputItemLines(outputItems); + } } } @@ -2794,7 +2795,7 @@ int initModel(SpatialParams **spatialParams, int **steps, char *paramFile, char * Populates static event structs */ void initEvents(char *eventFile, int numLocs) { -#ifdef EVENT_HANDLER +#if EVENT_HANDLER events = readEventData(eventFile, numLocs); #endif } diff --git a/sipnet.h b/sipnet.h index 6c8278b..5ce3349 100755 --- a/sipnet.h +++ b/sipnet.h @@ -52,7 +52,14 @@ char **getDataTypeNames(); int initModel(SpatialParams **spatialParams, int **steps, char *paramFile, char *climFile); /* - * TBD + * Read in event data for all the model runs + * + * Read in event data from a file with the following specification: + * - one line per event + * - all events are ordered first by location (ascending) and then by year/day (ascending) + * + * @param eventFile Name of file containing event data + * @param numLocs Number of locations in the event file. */ void initEvents(char *eventFile, int numLocs); diff --git a/tests/sipnet/exitHandler.c b/tests/sipnet/exitHandler.c index acda85e..8fc56a7 100644 --- a/tests/sipnet/exitHandler.c +++ b/tests/sipnet/exitHandler.c @@ -1,9 +1,11 @@ #include #include +// + static int expected_code = 1; // the expected value a tested function passes to exit -static int should_exit = 1; // 1 if exit should have been called -static int really_exit = 0; // set to 1 to prevent stubbing behavior and actually exit +static int should_exit = 1; // set in test code; 1 if exit should have been called +static int really_exit = 0; // set to 1 to prevent stubbing behavior and actually exit static jmp_buf jump_env; @@ -19,7 +21,7 @@ static int exit_result = 1; // } // test_assert(jmp_rval==0); // -// set should_exit=1 when exit(0 is expected: +// set should_exit=1 when exit is expected: // // should_exit = 1; // expected_code = 1; // or whatever the exit code should be @@ -40,8 +42,6 @@ void exit(int code) test_assert(expected_code==code); longjmp(jump_env, 1); } - else - { - _exit(code); - } + + _exit(code); } diff --git a/tests/test.template b/tests/test.template index 4ec9aec..53aac2d 100644 --- a/tests/test.template +++ b/tests/test.template @@ -34,4 +34,4 @@ int main() { } cleanup(); -} \ No newline at end of file +} From 0d8d16c3624ecc258f4f68b9d5cd4fedb2df77fb Mon Sep 17 00:00:00 2001 From: Alomir Date: Thu, 9 Jan 2025 11:44:47 -0500 Subject: [PATCH 08/16] Adds fix for frontend.c and test Makefile template --- frontend.c | 2 +- tests/Makefile.template | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 tests/Makefile.template diff --git a/frontend.c b/frontend.c index 302929c..9fc48e7 100755 --- a/frontend.c +++ b/frontend.c @@ -156,9 +156,9 @@ int main(int argc, char *argv[]) { strcat(climFile, ".clim"); numLocs = initModel(&spatialParams, &steps, paramFile, climFile); +#if EVENT_HANDLER strcpy(eventFile, fileName); strcat(eventFile, ".event"); -#if EVENT_HANDLER initEvents(eventFile, numLocs); #endif diff --git a/tests/Makefile.template b/tests/Makefile.template new file mode 100644 index 0000000..e26a64d --- /dev/null +++ b/tests/Makefile.template @@ -0,0 +1,34 @@ +CC=gcc +LD=gcc +CFLAGS=-Wall -g +LDFLAGS=-L../../.. +LDLIBS=-lsipnet -lm + +# List test files in this directory here +TEST_CFILES= <<== CHANGE THIS + +# The rest is boilerplate, likely copyable as is to a new test directory +TEST_OBJ_FILES=$(TEST_CFILES:%.c=%.o) +TEST_EXECUTABLES:=$(basename $(TEST_CFILES)) +RUN_EXECUTABLES:= $(addsuffix .run, $(TEST_EXECUTABLES)) + +all: tests + +tests: $(TEST_EXECUTABLES) + +$(TEST_EXECUTABLES): %: %.o + $(CC) $(LDFLAGS) $< $(LDLIBS) -o $@ + +$(TEST_OBJ_FILES): ../utils.h ../../../libsipnet.a ../exitHandler.c +$(TEST_OBJ_FILES): %.o: %.c + $(CC) $(CFLAGS) -c -o $@ $< + +run: $(RUN_EXECUTABLES) + +$(RUN_EXECUTABLES): + ./$(basename $@) + +clean: + rm -f $(TEST_OBJ_FILES) $(TEST_EXECUTABLES) events.in + +.PHONY: all tests clean run $(RUN_EXECUTABLES) From 94730e57d0c0971a8a82078a782a8da897540c42 Mon Sep 17 00:00:00 2001 From: David LeBauer Date: Thu, 9 Jan 2025 15:13:24 -0700 Subject: [PATCH 09/16] replace spaces with tab --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 77a9925..fa78dd3 100755 --- a/Makefile +++ b/Makefile @@ -92,8 +92,8 @@ $(SIPNET_TEST_DIRS_CLEAN): $(MAKE) -C $(basename $@) clean .PHONY: all clean document estimate sipnet transpose subsetData doxygen - test $(SIPNET_TEST_DIRS) pretest posttest $(SIPNET_LIB) testrun - $(SIPNET_TEST_DIRS_RUN) testclean $(SIPNET_TEST_DIRS_CLEAN) + test $(SIPNET_TEST_DIRS) pretest posttest $(SIPNET_LIB) testrun + $(SIPNET_TEST_DIRS_RUN) testclean $(SIPNET_TEST_DIRS_CLEAN) help: @echo "Available targets:" From f379cb9a95f5790a465aa15bac7d7ce26170c190 Mon Sep 17 00:00:00 2001 From: David LeBauer Date: Thu, 9 Jan 2025 15:36:05 -0700 Subject: [PATCH 10/16] Update ci.yml change order of build and unit tests update names and error messages for clarity --- .github/workflows/ci.yml | 104 +++++++++++++++++++-------------------- 1 file changed, 51 insertions(+), 53 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 131f49e..25efadf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,85 +4,83 @@ on: push: branches: - master - pull_request: jobs: - - # Build and run unit tests - test: + # Build and Integration Test + build_integration_test: strategy: fail-fast: false matrix: - OS: + OS: - macos-latest - ubuntu-latest - ubuntu-20.04 - runs-on: ${{ matrix.OS }} steps: # checkout source code - uses: actions/checkout@v2 - # compile tests - - name: compile tests - run: make test + # install doxygen + - name: Install Doxygen + run: | + if [[ "$RUNNER_OS" == "Linux" ]]; then + sudo apt-get install doxygen -y + elif [[ "$RUNNER_OS" == "macOS" ]]; then + brew install --formula doxygen + fi + + # compile SIPNET + - name: compile sipnet + run: make - # run tests - - name: run tests - run: make testrun + # remove existing test output file + - name: Remove Niwout Output File + run: rm Sites/Niwot/niwot.out + + # run single sipnet run + - name: Run SIPNET on Sites/Niwot/niwot + run: ./sipnet + + # check for correct output of Niwot run + - name: Check if niwot.out is generated + if: ${{ !files.exists('Sites/Niwot/niwot.out') }} + run: | + echo "::error title={No Output}::Test run for Niwot site failed to produce output" + exit 1 + # Check if niwot.out has changed + - name: Check whether niwot.out has changed + shell: bash + run: | + if git diff --exit-code Sites/Niwot/niwot.out; then + echo "Success: Niwot.out created and has not changed" + else + echo "::error title={Output Changed}::The test file niwot.out has changed. This is expected to fail with some changes to SIPNET. When this happens, assess correctness and then update the reference niwot.out." + exit 1 + fi - # try to build SIPNET on Ubuntu & MacOS - build: + # Run Unit Tests + test: + needs: build_integration_test strategy: fail-fast: false matrix: - OS: + OS: - macos-latest - ubuntu-latest - ubuntu-20.04 - runs-on: ${{ matrix.OS }} steps: - # checkout source code - - uses: actions/checkout@v2 - - # install doxygen - - name: Install Doxygen - run: | - if [[ "$RUNNER_OS" == "Linux" ]]; then - sudo apt-get install doxygen -y - elif [[ "$RUNNER_OS" == "macOS" ]]; then - brew install --formula doxygen - fi - - # compile SIPNET - - name: compile sipnet - run: make + # checkout source code + - uses: actions/checkout@v2 - # remove existing test output file - - name: Remove Niwout Output File - run: rm Sites/Niwot/niwot.out + # compile unit tests + - name: compile tests + run: make test - # run single sipnet run - - name: sipnet on Sites/Niwot/niwot - run: ./sipnet + # run tests + - name: Run Unit Tests + run: make testrun - # check output of test - - name: fail if no niwot output exists - if: ${{ hashFiles('Sites/Niwot/niwot.out') == '' }} - run: | - echo "::error title={No Output}::Test run for Niwot site failed to produce output" - exit 1 - # check whether niwot.out has changed - - name: Check whether niwot.out has changed - shell: bash - run: | - if git diff --exit-code Sites/Niwot/niwot.out; then - echo "Success: Niwot.out created and has not changed" - else - echo "::error title={Output Changed}::The test file niwot.out has changed" - exit 1 - fi From 1b5123a5b5c295abde74ac82b82535b72acd72bf Mon Sep 17 00:00:00 2001 From: David LeBauer Date: Thu, 9 Jan 2025 15:46:31 -0700 Subject: [PATCH 11/16] Update ci.yml --- .github/workflows/ci.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 25efadf..de33391 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -45,10 +45,13 @@ jobs: # check for correct output of Niwot run - name: Check if niwot.out is generated - if: ${{ !files.exists('Sites/Niwot/niwot.out') }} + shell: bash run: | - echo "::error title={No Output}::Test run for Niwot site failed to produce output" - exit 1 + if: ${{ !files.exists('Sites/Niwot/niwot.out') }} + echo "::error title={No Output}::Test run for Niwot site failed to produce output" + exit 1 + fi + # Check if niwot.out has changed - name: Check whether niwot.out has changed shell: bash From ad10b3dcc360db03dc616fe93d7a92a1734b2074 Mon Sep 17 00:00:00 2001 From: David LeBauer Date: Thu, 9 Jan 2025 15:48:24 -0700 Subject: [PATCH 12/16] Update ci.yml --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index de33391..8238260 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,7 +47,7 @@ jobs: - name: Check if niwot.out is generated shell: bash run: | - if: ${{ !files.exists('Sites/Niwot/niwot.out') }} + if [ ! -f Sites/Niwot/niwot.out ]; then echo "::error title={No Output}::Test run for Niwot site failed to produce output" exit 1 fi From 6fc965f6af5b8d5c7121fa24d26636f94149a89a Mon Sep 17 00:00:00 2001 From: David LeBauer Date: Thu, 9 Jan 2025 15:49:13 -0700 Subject: [PATCH 13/16] Update ci.yml --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8238260..4708e41 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,7 +8,7 @@ on: jobs: # Build and Integration Test - build_integration_test: + build: strategy: fail-fast: false matrix: From 93adfd1328e7c924351f07fb4a137549e5bba85a Mon Sep 17 00:00:00 2001 From: David LeBauer Date: Thu, 9 Jan 2025 15:49:55 -0700 Subject: [PATCH 14/16] Update ci.yml --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4708e41..fe0c452 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -65,7 +65,7 @@ jobs: # Run Unit Tests test: - needs: build_integration_test + needs: build strategy: fail-fast: false matrix: From e5774f3784f0b5bd1c2d078de2f7f7f438caf92a Mon Sep 17 00:00:00 2001 From: Mike Longfritz Date: Fri, 10 Jan 2025 12:22:28 -0500 Subject: [PATCH 15/16] Updates events.c to add doc comments at top Co-authored-by: David LeBauer --- events.c | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/events.c b/events.c index b4f3027..15259af 100644 --- a/events.c +++ b/events.c @@ -1,5 +1,27 @@ // // events.c +/** + * @file events.c + * @brief Handles reading, parsing, and storing agronomic events for SIPNET simulations. + * + * The `events.in` file specifies agronomic events with the following columns: + * | col | parameter | description | units | + * |-----|-------------|--------------------------------------------|------------------| + * | 1 | loc | spatial location index | | + * | 2 | year | year of start of this timestep | | + * | 3 | day | day of start of this timestep | Day of year | + * | 4 | event_type | type of event | (e.g., "irrig") | + * | 5...n| event_param| parameters specific to the event type | (varies) | + * + * + * Event types include: + * - "irrig": Parameters = [amount added (cm/d), type (0=canopy, 1=soil, 2=flood)] + * - "fert": Parameters = [Org-N (g/m²), Org-C (g/ha), Min-N (g/m²), Min-N2 (g/m²)] + * - "till": Parameters = [SOM decomposition modifier, litter decomposition modifier] + * - "plant": Parameters = [emergence lag (days), C (g/m²), N (g/m²)]] + * - "harv": Parameters = [fraction aboveground removed, fraction belowground removed, ...] + * See test examples in `tests/sipnet/test_events/`. + */ // #include "events.h" From 394c4ab8d3ce5d76e2bc782e9964482cc06085e5 Mon Sep 17 00:00:00 2001 From: Alomir Date: Fri, 10 Jan 2025 12:32:43 -0500 Subject: [PATCH 16/16] Adds docs for building and running unit tests to CONTRIBUTING.md --- CONTRIBUTING.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7806eda..2d1d609 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -15,3 +15,14 @@ make clean # list all make commands make help ``` +## Testing + +SIPNET also uses `make` to build and run its unit tests. This can be done with the following commands: +```shell +# Compile tests +make test +# Run tests +make testrun +# Clean after tests are run +make testclean +```