[Racket] Ferti hydroponic nutrient solver, redux.
Add unit tests to model modules.
Changed files
models/crop-requirement.rkt
@@ -163,3 +163,93 @@
163
163
([(nutrient value) (in-hash (crop-requirement-nutrient-values crop-requirement))])
164
164
(define contribution (* value weight))
165
165
(hash-update acc nutrient (λ (old) (+ old contribution)) 0))))
166
Added:
167
Added:
(module+ test
168
Added:
(require rackunit
169
Added:
rackunit/text-ui
170
Added:
"../db/conn.rkt"
171
Added:
"../db/migrations.rkt"
172
Added:
"../models/nutrient.rkt"
173
Added:
"../models/crop.rkt")
174
Added:
175
Added:
(define requirement-profile "Tomato - Vegetative")
176
Added:
177
Added:
(run-tests
178
Added:
(test-suite "Crop requirement model"
179
Added:
#:before (λ ()
180
Added:
(connect! #:path 'memory)
181
Added:
(migrate-all!)
182
Added:
(create-nutrient! "Nitrogen" "Azote" "N")
183
Added:
(create-nutrient! "Phosphorus" "Phosphore" "P")
184
Added:
(create-nutrient! "Potassium" "Potassium" "K"))
185
Added:
#:after (λ () (disconnect!))
186
Added:
187
Added:
(test-case "Create requirement with profile and values"
188
Added:
(define nitrogen (get-nutrient #:name "Nitrogen"))
189
Added:
(define phosphorus (get-nutrient #:name "Phosphorus"))
190
Added:
191
Added:
(create-crop-requirement! requirement-profile (hash nitrogen 150 phosphorus 50))
192
Added:
193
Added:
(check-equal? (length (get-crop-requirements)) 1)
194
Added:
195
Added:
(define cr (get-crop-requirement #:profile requirement-profile))
196
Added:
(check-true (crop-requirement? cr))
197
Added:
(check-equal? (crop-requirement-profile cr) requirement-profile))
198
Added:
199
Added:
(test-case "Create requirement with associated crop"
200
Added:
(define tomato (create-crop! "Tomato"))
201
Added:
(define nitrogen (get-nutrient #:name "Nitrogen"))
202
Added:
203
Added:
(define cr (create-crop-requirement! "Tomato - Fruiting" (hash nitrogen 200) tomato))
204
Added:
205
Added:
(check-equal? (crop-requirement-crop-id cr) (crop-id tomato)))
206
Added:
207
Added:
(test-case "Check all requirement values"
208
Added:
(define nitrogen (get-nutrient #:name "Nitrogen"))
209
Added:
(define phosphorus (get-nutrient #:name "Phosphorus"))
210
Added:
211
Added:
(define cr (get-crop-requirement #:profile requirement-profile))
212
Added:
213
Added:
(check-= (get-crop-requirement-value cr nitrogen) 150 0)
214
Added:
(check-= (get-crop-requirement-value cr phosphorus) 50 0)
215
Added:
216
Added:
(define crv (crop-requirement-nutrient-values cr))
217
Added:
218
Added:
(check-equal?
219
Added:
(get-crop-requirement-values cr)
220
Added:
crv
221
Added:
"return value of get-crop-requirement-values ≠ crop-requirement-values struct accessor")
222
Added:
223
Added:
(check-equal? (hash-count crv) 2)
224
Added:
(check-= (hash-ref crv nitrogen) 150 0)
225
Added:
(check-= (hash-ref crv phosphorus) 50 0))
226
Added:
227
Added:
(test-case "Get requirement by id"
228
Added:
(define cr (get-crop-requirement #:profile requirement-profile))
229
Added:
(define cr-by-id (get-crop-requirement #:id (crop-requirement-id cr)))
230
Added:
231
Added:
(check-equal? cr cr-by-id))
232
Added:
233
Added:
(test-case "Average crop requirement nutrient values"
234
Added:
(define nitrogen (get-nutrient #:name "Nitrogen"))
235
Added:
(define phosphorus (get-nutrient #:name "Phosphorus"))
236
Added:
237
Added:
(define cr1 (get-crop-requirement #:profile requirement-profile))
238
Added:
(define cr2 (create-crop-requirement! "Lettuce" (hash nitrogen 100 phosphorus 30)))
239
Added:
240
Added:
(define mix (list (cons cr1 60) (cons cr2 40)))
241
Added:
(define avg (average-crop-requirement-nutrient-values mix))
242
Added:
243
Added:
;; 150 * 0.6 + 100 * 0.4 = 90 + 40 = 130
244
Added:
(check-= (hash-ref avg nitrogen) 130 0.01)
245
Added:
;; 50 * 0.6 + 30 * 0.4 = 30 + 12 = 42
246
Added:
(check-= (hash-ref avg phosphorus) 42 0.01))
247
Added:
248
Added:
(test-case "Delete requirement and cascade to requirement values"
249
Added:
(define cr (get-crop-requirement #:profile requirement-profile))
250
Added:
(delete-crop-requirement! cr)
251
Added:
(check-false (get-crop-requirement #:id (crop-requirement-id cr)))
252
Added:
(check-equal? (length (get-crop-requirements))
253
Added:
2
254
Added:
"wrong number of crop requirements were deleted")
255
Added:
(check-true (hash-empty? (get-crop-requirement-values cr)))))))
models/crop.rkt
@@ -25,9 +25,8 @@
25
25
26
26
(define (create-crop! name)
27
27
(or (get-crop #:name name)
28
Removed:
(with-tx
29
Removed:
(query-exec (current-conn) (insert #:into crops #:set [canonical_name ,name]))
30
Removed:
(get-crop #:name name))))
28
Added:
(with-tx (query-exec (current-conn) (insert #:into crops #:set [canonical_name ,name]))
29
Added:
(get-crop #:name name))))
31
30
32
31
;; READ
33
32
models/fertilizer-product.rkt
@@ -26,7 +26,10 @@
26
26
(struct fertilizer-product (id canonical-name nutrient-values brand-name)
27
27
#:transparent
28
28
#:guard (λ (id canonical-name nutrient-values brand-name _)
29
Removed:
(values id canonical-name nutrient-values (if (sql-null? brand-name) #f brand-name)))
29
Added:
(values id
30
Added:
canonical-name
31
Added:
nutrient-values
32
Added:
(if (or (sql-null? brand-name) (= (string-length brand-name) 0)) #f brand-name)))
30
33
#:property prop:custom-write
31
34
(λ (v out _mode)
32
35
(fprintf out "Fertilizer #~a\n" (fertilizer-product-id v))
@@ -163,3 +166,96 @@
163
166
164
167
(define (delete-fertilizer-product! fp-or-id)
165
168
(query-exec (current-conn) (delete #:from fertilizer_products #:where (= id ,(->fp-id fp-or-id)))))
169
Added:
170
Added:
(module+ test
171
Added:
(require rackunit
172
Added:
rackunit/text-ui
173
Added:
"../db/conn.rkt"
174
Added:
"../db/migrations.rkt"
175
Added:
"../models/nutrient.rkt")
176
Added:
177
Added:
(define canonical-product-name "MasterBlend 4-20")
178
Added:
179
Added:
(run-tests
180
Added:
(test-suite "Fertilizer product model"
181
Added:
#:before (λ ()
182
Added:
(connect! #:path 'memory)
183
Added:
(migrate-all!)
184
Added:
(create-nutrient! "Nitrogen" "Azote" "N")
185
Added:
(create-nutrient! "Phosphorus" "Phosphore" "P")
186
Added:
(create-nutrient! "Potassium" "Potassium" "K"))
187
Added:
#:after (λ () (disconnect!))
188
Added:
189
Added:
(test-case "Create product with name, brand, and values"
190
Added:
(define nitrogen (get-nutrient #:name "Nitrogen"))
191
Added:
(define phosphorus (get-nutrient #:name "Phosphorus"))
192
Added:
193
Added:
(create-fertilizer-product! canonical-product-name
194
Added:
"MasterBlend"
195
Added:
(hash nitrogen 40 phosphorus 200))
196
Added:
197
Added:
(check-equal? (length (get-fertilizer-products)) 1)
198
Added:
199
Added:
(define fp (get-fertilizer-product #:canonical-name canonical-product-name))
200
Added:
(check-true (fertilizer-product? fp))
201
Added:
(check-equal? (fertilizer-product-canonical-name fp) canonical-product-name)
202
Added:
(check-equal? (fertilizer-product-brand-name fp) "MasterBlend"))
203
Added:
204
Added:
(test-case "Create product without brand name"
205
Added:
(define nitrogen (get-nutrient #:name "Nitrogen"))
206
Added:
207
Added:
(define fp (create-fertilizer-product! "Generic N" "" (hash nitrogen 100)))
208
Added:
209
Added:
(check-false (fertilizer-product-brand-name fp)))
210
Added:
211
Added:
(test-case "Check all product values"
212
Added:
(define nitrogen (get-nutrient #:name "Nitrogen"))
213
Added:
(define phosphorus (get-nutrient #:name "Phosphorus"))
214
Added:
215
Added:
(define fp (get-fertilizer-product #:canonical-name canonical-product-name))
216
Added:
217
Added:
(check-= (get-fertilizer-product-value fp nitrogen) 40 0)
218
Added:
(check-= (get-fertilizer-product-value fp phosphorus) 200 0)
219
Added:
220
Added:
(define fpv (fertilizer-product-nutrient-values fp))
221
Added:
222
Added:
(check-equal?
223
Added:
(get-fertilizer-product-values fp)
224
Added:
fpv
225
Added:
"return value of get-fertilizer-product-values ≠ fertilizer-product-values struct accessor")
226
Added:
227
Added:
(check-equal? (hash-count fpv) 2))
228
Added:
229
Added:
(test-case "Get product by id"
230
Added:
(define fp (get-fertilizer-product #:canonical-name canonical-product-name))
231
Added:
(define fp-by-id (get-fertilizer-product #:id (fertilizer-product-id fp)))
232
Added:
233
Added:
(check-equal? fp fp-by-id))
234
Added:
235
Added:
(test-case "Handle missing nutrient in product"
236
Added:
(define nitrogen (get-nutrient #:name "Nitrogen"))
237
Added:
(define potassium (get-nutrient #:name "Potassium"))
238
Added:
239
Added:
(define fp (get-fertilizer-product #:canonical-name canonical-product-name))
240
Added:
(check-false (get-fertilizer-product-value fp potassium)))
241
Added:
242
Added:
(test-case "Custom write property formatting"
243
Added:
(define nitrogen (get-nutrient #:name "Nitrogen"))
244
Added:
(define fp (create-fertilizer-product! "Test Fertilizer" "TestBrand" (hash nitrogen 50)))
245
Added:
246
Added:
(define output (open-output-string))
247
Added:
(write fp output)
248
Added:
(define result (get-output-string output))
249
Added:
250
Added:
(check-true (string-contains? result "Fertilizer #"))
251
Added:
(check-true (string-contains? result "Test Fertilizer"))
252
Added:
(check-true (string-contains? result "TestBrand")))
253
Added:
254
Added:
(test-case "Delete product and cascade to product values"
255
Added:
(define fp (get-fertilizer-product #:canonical-name canonical-product-name))
256
Added:
(delete-fertilizer-product! fp)
257
Added:
(check-false (get-fertilizer-product #:id (fertilizer-product-id fp)))
258
Added:
(check-equal? (length (get-fertilizer-products))
259
Added:
2
260
Added:
"wrong number of fertilizer products were deleted")
261
Added:
(check-true (hash-empty? (get-fertilizer-product-values fp)))))))
models/nutrient-target.rkt
@@ -177,5 +177,67 @@
177
177
;; DELETE
178
178
179
179
(define (delete-nutrient-target! nt-or-id)
180
Removed:
(define id (nutrient-target-id nutrient-target))
181
180
(query-exec (current-conn) (delete #:from nutrient_targets #:where (= id ,(->nt-id nt-or-id)))))
181
Added:
182
Added:
(module+ test
183
Added:
(require rackunit
184
Added:
rackunit/text-ui
185
Added:
"../db/conn.rkt"
186
Added:
"../db/migrations.rkt"
187
Added:
"../models/nutrient.rkt")
188
Added:
189
Added:
(define target-date "2025-09-01")
190
Added:
191
Added:
(run-tests
192
Added:
(test-suite "Nutrient target model"
193
Added:
#:before (λ ()
194
Added:
(connect! #:path 'memory)
195
Added:
(migrate-all!)
196
Added:
(create-nutrient! "Nitrogen" "" "N")
197
Added:
(create-nutrient! "Phosphorus" "" "P")
198
Added:
(create-nutrient! "Potassium" "" "K"))
199
Added:
#:after (λ () (disconnect!))
200
Added:
201
Added:
(test-case "Create target with date and values"
202
Added:
(define nitrogen (get-nutrient #:name "Nitrogen"))
203
Added:
(define phosphorus (get-nutrient #:name "Phosphorus"))
204
Added:
(create-nutrient-target! target-date (hash nitrogen 12.3 phosphorus 4.5))
205
Added:
(check-equal? (length (get-nutrient-targets)) 1)
206
Added:
(define nt (get-nutrient-target #:effective-on target-date))
207
Added:
(check-true (nutrient-target? nt))
208
Added:
(check-equal? (nutrient-target-effective-on nt) target-date))
209
Added:
210
Added:
(test-case "Check all target values"
211
Added:
(define nitrogen (get-nutrient #:name "Nitrogen"))
212
Added:
(define phosphorus (get-nutrient #:name "Phosphorus"))
213
Added:
214
Added:
(define nt (get-nutrient-target #:effective-on target-date))
215
Added:
(check-equal? (get-nutrient-target-value nt nitrogen) 12.3)
216
Added:
(check-equal? (get-nutrient-target-value nt phosphorus) 4.5)
217
Added:
218
Added:
(define ntv (nutrient-target-nutrient-values nt))
219
Added:
(check-equal?
220
Added:
(get-nutrient-target-values nt)
221
Added:
ntv
222
Added:
"return value of get-nutrient-target-values ≠ nutrient-target-values struct accessor")
223
Added:
(check-equal? (hash-count ntv) 2)
224
Added:
(check-equal? (hash-ref ntv nitrogen) 12.3)
225
Added:
(check-equal? (hash-ref ntv phosphorus) 4.5))
226
Added:
227
Added:
(test-case "Retrieve latest target values"
228
Added:
(define nitrogen (get-nutrient #:name "Nitrogen"))
229
Added:
(define phosphorus (get-nutrient #:name "Phosphorus"))
230
Added:
(define second-target-date "2025-09-02")
231
Added:
(create-nutrient-target! second-target-date (hash nitrogen 6.7 phosphorus 8.9))
232
Added:
233
Added:
(check-equal? (get-latest-nutrient-target-value nitrogen) 6.7)
234
Added:
(check-equal? (get-latest-nutrient-target-value phosphorus) 8.9))
235
Added:
236
Added:
(test-case "Delete target and cascade to target values"
237
Added:
(define nt (get-nutrient-target #:effective-on target-date))
238
Added:
(delete-nutrient-target! nt)
239
Added:
(check-false (get-nutrient-target #:id (nutrient-target-id nt)))
240
Added:
(check-equal? (length (get-nutrient-targets))
241
Added:
1
242
Added:
"wrong number of nutrient targets were deleted")
243
Added:
(check-true (hash-empty? (get-nutrient-target-values nt)))))))