Commit c9546a4719202fa3759de878c03586a87ca86a67

Authored by Dhruv Kapadia
1 parent 26ecf5ed

Refactored nightly api test. Added api test for l/r probabilty

Showing 1 changed file with 188 additions and 109 deletions   Show diff stats
lib/tasks/test_api.rake
@@ -243,13 +243,98 @@ namespace :test_api do @@ -243,13 +243,98 @@ namespace :test_api do
243 desc "Description here" 243 desc "Description here"
244 task(:question_vote_consistency => :environment) do 244 task(:question_vote_consistency => :environment) do
245 questions = Question.find(:all) 245 questions = Question.find(:all)
  246 + errors = []
  247 + successes = []
246 248
247 - error_msg = ""  
248 -  
249 - bad_choices = []  
250 - bad_votes = []  
251 questions.each do |question| 249 questions.each do |question|
252 250
  251 + message, error_occurred = check_basic_balanced_stats(question)
  252 + #hack for now, get around to doing this with block /yield to get rid of duplication
  253 + if error_occurred
  254 + errors << message
  255 + else
  256 + successes << message
  257 + end
  258 +
  259 +
  260 + message, error_occurred = check_each_choice_appears_within_n_stddevs(question)
  261 + if error_occurred
  262 + errors << message
  263 + else
  264 + successes << message
  265 + end
  266 +
  267 + message, error_occurred = check_each_choice_equally_likely_to_appear_left_or_right(question)
  268 + if error_occurred
  269 + errors << message
  270 + else
  271 + successes << message
  272 + end
  273 +
  274 +
  275 +
  276 + message, error_occurred = check_object_counter_cache_values_match_actual_values(question)
  277 + if error_occurred
  278 + errors << message
  279 + else
  280 + successes << message
  281 + end
  282 +
  283 +
  284 + #catchup specific
  285 + if question.uses_catchup?
  286 + message, error_occurred = check_prompt_cache_hit_rate(question)
  287 + if error_occurred
  288 + errors << message
  289 + else
  290 + successes << message
  291 + end
  292 + end
  293 + end
  294 +
  295 + message, error_occurred = ensure_all_votes_and_skips_have_unique_appearance
  296 +
  297 + if error_occurred
  298 + errors << message
  299 + else
  300 + successes << message
  301 + end
  302 +
  303 + message, error_occurred = response_time_tests
  304 +
  305 + if error_occurred
  306 + errors << message
  307 + else
  308 + successes << message
  309 + end
  310 +
  311 + email_text = "Conducted the following tests on API data and found the following results\n" +
  312 + "For each of the #{questions.length} questions in the database: \n"
  313 + errors.each do |e|
  314 + email_text += " Test FAILED: " + e + "\n"
  315 + end
  316 +
  317 + successes.uniq.each do |s|
  318 + s.split("\n").each do |m| # some successes have several lines
  319 + email_text += " Test Passed: " + m + "\n"
  320 + end
  321 + end
  322 +
  323 + puts email_text
  324 +
  325 + if errors.empty?
  326 + CronMailer.deliver_info_message(CRON_EMAIL, "Test of API Vote Consistency passed", email_text)
  327 + else
  328 + CronMailer.deliver_info_message("#{CRON_EMAIL},#{ERRORS_EMAIL}", "Error! Failure of API Vote Consistency " , email_text)
  329 + end
  330 +
  331 + end
  332 + def check_basic_balanced_stats(question)
  333 + error_message = ""
  334 + success_message = "2 x Total Wins = Total Votes\n" +
  335 + "Total Votes (wins + losses) is Even\n" +
  336 + "Total Votes (wins + losses) = 2 x the number of vote objects that belong to the question\n" +
  337 + "Total generated prompts on left = Total generated prompts on right"
253 total_wins =0 338 total_wins =0
254 total_votes =0 339 total_votes =0
255 total_generated_prompts_on_left = 0 340 total_generated_prompts_on_left = 0
@@ -257,6 +342,7 @@ namespace :test_api do @@ -257,6 +342,7 @@ namespace :test_api do
257 total_scores_gte_fifty= 0 342 total_scores_gte_fifty= 0
258 total_scores_lte_fifty= 0 343 total_scores_lte_fifty= 0
259 error_bool = false 344 error_bool = false
  345 +
260 question.choices.each do |choice| 346 question.choices.each do |choice|
261 347
262 if choice.wins 348 if choice.wins
@@ -277,20 +363,19 @@ namespace :test_api do @@ -277,20 +363,19 @@ namespace :test_api do
277 delta = 0.001 363 delta = 0.001
278 364
279 if (cached_score - generated_score).abs >= delta 365 if (cached_score - generated_score).abs >= delta
280 - error_msg += "Error! The cached_score is not equal to the calculated score for choice #{choice.id}" 366 + error_message += "Error! The cached_score is not equal to the calculated score for choice #{choice.id}"
281 367
282 print "This score is wrong! #{choice.id} , Question ID: #{question.id}, #{cached_score}, #{generated_score}, updated: #{choice.updated_at}\n" 368 print "This score is wrong! #{choice.id} , Question ID: #{question.id}, #{cached_score}, #{generated_score}, updated: #{choice.updated_at}\n"
283 369
284 - bad_choices << choice.id  
285 370
286 end 371 end
287 372
288 if cached_score == 0.0 || cached_score == 100.0 || cached_score.nil? 373 if cached_score == 0.0 || cached_score == 100.0 || cached_score.nil?
289 - error_msg += "Error! The cached_score for choice #{choice.id} is exactly 0 or 100, the value: #{cached_score}" 374 + error_message += "Error! The cached_score for choice #{choice.id} is exactly 0 or 100, the value: #{cached_score}"
290 print "Either 0 or 100 This score is wrong! #{choice.id} , Question ID: #{question.id}, #{cached_score}, #{generated_score}, updated: #{choice.updated_at}\n" 375 print "Either 0 or 100 This score is wrong! #{choice.id} , Question ID: #{question.id}, #{cached_score}, #{generated_score}, updated: #{choice.updated_at}\n"
291 - bad_choices << choice.id  
292 end 376 end
293 377
  378 +
294 if cached_score >= 50 379 if cached_score >= 50
295 total_scores_gte_fifty +=1 380 total_scores_gte_fifty +=1
296 end 381 end
@@ -302,31 +387,40 @@ namespace :test_api do @@ -302,31 +387,40 @@ namespace :test_api do
302 end 387 end
303 388
304 if (2*total_wins != total_votes) 389 if (2*total_wins != total_votes)
305 - error_msg += "Error 1: 2 x Total Wins != Total votes" 390 + error_message += "Error 1: 2 x Total Wins != Total votes"
306 error_bool= true 391 error_bool= true
307 end 392 end
308 393
309 if(total_votes % 2 != 0) 394 if(total_votes % 2 != 0)
310 - error_msg += "Error 2: Total votes is not Even!" 395 + error_message += "Error 2: Total votes is not Even!"
311 error_bool= true 396 error_bool= true
312 end 397 end
313 398
314 if(total_votes != 2* question.votes_count) 399 if(total_votes != 2* question.votes_count)
315 - error_msg += "Error 3: Total votes != 2 x # vote objects" 400 + error_message += "Error 3: Total votes != 2 x # vote objects"
316 error_bool = true 401 error_bool = true
317 end 402 end
318 403
319 if(total_generated_prompts_on_right != total_generated_prompts_on_right) 404 if(total_generated_prompts_on_right != total_generated_prompts_on_right)
320 - error_msg += "Error 4: Total generated prompts on left != Total generated prompts on right" 405 + error_message += "Error 4: Total generated prompts on left != Total generated prompts on right"
321 error_bool = true 406 error_bool = true
322 end 407 end
323 408
324 if(total_scores_lte_fifty == question.choices.size || total_scores_gte_fifty == question.choices.size) && (total_scores_lte_fifty != total_scores_gte_fifty) 409 if(total_scores_lte_fifty == question.choices.size || total_scores_gte_fifty == question.choices.size) && (total_scores_lte_fifty != total_scores_gte_fifty)
325 - error_msg += "Error: The scores of all choices are either all above 50, or all below 50. This is probably wrong" 410 + error_message += "Error: The scores of all choices are either all above 50, or all below 50. This is probably wrong"
326 error_bool = true 411 error_bool = true
327 puts "Error score fifty: #{question.id}" 412 puts "Error score fifty: #{question.id}"
328 end 413 end
329 - 414 +
  415 + if error_bool
  416 + error_message += "Question #{question.id}: 2*wins = #{2*total_wins}, total votes = #{total_votes}, vote_count = #{question.votes_count}\n"
  417 + end
  418 + return error_message.blank? ? [success_message, false] : [error_message, true]
  419 + end
  420 + def check_each_choice_appears_within_n_stddevs(question)
  421 + error_message =""
  422 + success_message = "Each choice has appeared n times, where n falls within 6 stddevs of the mean number of appearances for a question " +
  423 + "(Note: this applies only to seed choices (not user submitted) and choices currently marked active)"
330 424
331 wins_by_choice_id = question.votes.active.count(:group => :choice_id) 425 wins_by_choice_id = question.votes.active.count(:group => :choice_id)
332 losses_by_choice_id= question.votes.active.count(:conditions => "loser_choice_id IS NOT NULL", :group => :loser_choice_id) 426 losses_by_choice_id= question.votes.active.count(:conditions => "loser_choice_id IS NOT NULL", :group => :loser_choice_id)
@@ -339,6 +433,7 @@ namespace :test_api do @@ -339,6 +433,7 @@ namespace :test_api do
339 losses_hash.merge!(losses_by_choice_id) 433 losses_hash.merge!(losses_by_choice_id)
340 434
341 435
  436 +
342 appearances_by_choice_id = wins_hash.merge(losses_hash) do |key, oldval, newval| oldval + newval end 437 appearances_by_choice_id = wins_hash.merge(losses_hash) do |key, oldval, newval| oldval + newval end
343 438
344 sum = total_appearances = appearances_by_choice_id.values.inject(0) {|sum, x| sum +=x} 439 sum = total_appearances = appearances_by_choice_id.values.inject(0) {|sum, x| sum +=x}
@@ -349,157 +444,141 @@ namespace :test_api do @@ -349,157 +444,141 @@ namespace :test_api do
349 444
350 appearances_by_choice_id.each do |choice_id, n_i| 445 appearances_by_choice_id.each do |choice_id, n_i|
351 if (n_i < (mean - 6*stddev)) || (n_i > mean + 6 *stddev) 446 if (n_i < (mean - 6*stddev)) || (n_i > mean + 6 *stddev)
352 - error_msg += "Choice #{choice_id} in Question ##{question.id} has an irregular number of appearances: #{n_i}, as compared to the mean: #{mean} and stddev #{stddev} for this question"  
353 - error_bool = true 447 + error_message += "Choice #{choice_id} in Question ##{question.id} has an irregular number of appearances: #{n_i}, as compared to the mean: #{mean} and stddev #{stddev} for this question"
354 end 448 end
355 end 449 end
356 end 450 end
357 - 451 +
  452 + return error_message.blank? ? [success_message, false] : [error_message, true]
  453 + end
  454 + def check_each_choice_equally_likely_to_appear_left_or_right(question)
  455 + error_message = ""
  456 + success_message = "All choices have equal probability of appearing on left or right (within error params)"
  457 + question.choices.each do |c|
  458 + left_prompts_ids = c.prompts_on_the_left.ids_only
  459 + right_prompts_ids = c.prompts_on_the_right.ids_only
  460 +
  461 + left_appearances = question.appearances.count(:conditions => {:prompt_id => left_prompts_ids})
  462 + right_appearances = question.appearances.count(:conditions => {:prompt_id => right_prompts_ids})
  463 +
  464 + n = left_appearances + right_appearances
  465 +
  466 + if n == 0
  467 + next
  468 + end
  469 + est_p = right_appearances.to_f / n.to_f
  470 + z = (est_p - 0.5).abs / Math.sqrt((0.5 * 0.5) / n.to_f)
  471 +
  472 + if z > 6
  473 + error_message += "Error: Choice ID #{c.id} seems to favor one side: Left Appearances #{left_appearances}, Right Appearances: #{right_appearances}, z = #{z}\n"
  474 + end
  475 + end
  476 + return error_message.blank? ? [success_message, false] : [error_message, true]
  477 + end
  478 + def check_prompt_cache_hit_rate(question)
  479 + error_message = ""
  480 + success_message = "At least 90% of prompts on catchup algorithm questions were served from cache\n"
  481 +
  482 + misses = question.get_prompt_cache_misses(Date.yesterday).to_i
  483 + hits = question.get_prompt_cache_hits(Date.yesterday).to_i
  484 +
  485 + question.expire_prompt_cache_tracking_keys(Date.yesterday)
  486 +
  487 + yesterday_votes = question.appearances.count(:conditions => ['date(created_at) = ?', Date.yesterday])
  488 +
  489 + if misses + hits != yesterday_votes
  490 + error_message += "Error! Question #{question.id} isn't tracking prompt cache hits and misses accurately! Expected #{yesterday_votes}, Actual: #{misses+hits}\n"
  491 + end
  492 +
  493 + miss_rate = misses.to_f / yesterday_votes.to_f
  494 + if miss_rate > 0.1
  495 + error_message += "Error! Question #{question.id} has less than 90% of appearances taken from a pre-generated cache! Expected <#{0.1}, Actual: #{miss_rate}\n"
  496 + end
  497 + return error_message.blank? ? [success_message, false] : [error_message, true]
  498 + end
  499 +
  500 + def check_object_counter_cache_values_match_actual_values(question)
  501 + error_message = ""
  502 + success_message = "All cached object values match actual values within database"
358 # Checks that counter_cache is working as expected 503 # Checks that counter_cache is working as expected
359 cached_prompts_size = question.prompts.size 504 cached_prompts_size = question.prompts.size
360 actual_prompts_size = question.prompts.count 505 actual_prompts_size = question.prompts.count
361 506
362 if cached_prompts_size != actual_prompts_size 507 if cached_prompts_size != actual_prompts_size
363 - error_msg += "Error! Question #{question.id} has an inconsistent # of prompts! cached#: #{cached_prompts_size}, actual#: #{actual_prompts_size}\n" 508 + error_message += "Error! Question #{question.id} has an inconsistent # of prompts! cached#: #{cached_prompts_size}, actual#: #{actual_prompts_size}\n"
364 end 509 end
365 510
366 cached_votes_size = question.votes.size 511 cached_votes_size = question.votes.size
367 actual_votes_size = question.votes.count 512 actual_votes_size = question.votes.count
368 513
369 if cached_votes_size != actual_votes_size 514 if cached_votes_size != actual_votes_size
370 - error_msg += "Error! Question #{question.id} has an inconsistent # of votes! cached#: #{cached_votes_size}, actual#: #{actual_votes_size}\n" 515 + error_message += "Error! Question #{question.id} has an inconsistent # of votes! cached#: #{cached_votes_size}, actual#: #{actual_votes_size}\n"
371 end 516 end
372 517
373 cached_choices_size = question.choices.size 518 cached_choices_size = question.choices.size
374 actual_choices_size = question.choices.count 519 actual_choices_size = question.choices.count
375 520
376 if cached_choices_size != actual_choices_size 521 if cached_choices_size != actual_choices_size
377 - error_msg += "Error! Question #{question.id} has an inconsistent # of choices! cached#: #{cached_choices_size}, actual#: #{actual_choices_size}\n" 522 + error_message+= "Error! Question #{question.id} has an inconsistent # of choices! cached#: #{cached_choices_size}, actual#: #{actual_choices_size}\n"
378 end 523 end
379 524
380 if cached_prompts_size != question.choices.size **2 - question.choices.size 525 if cached_prompts_size != question.choices.size **2 - question.choices.size
381 - error_msg += "Error! Question #{question.id} has an incorrect number of prompts! Expected #{question.choices.size **2 - question.choices.size}, Actual: #{cached_prompts_size}\n" 526 + error_message += "Error! Question #{question.id} has an incorrect number of prompts! Expected #{question.choices.size **2 - question.choices.size}, Actual: #{cached_prompts_size}\n"
382 end 527 end
383 -  
384 -  
385 - #catchup specific  
386 - if question.uses_catchup?  
387 - misses = question.get_prompt_cache_misses(Date.yesterday).to_i  
388 - hits = question.get_prompt_cache_hits(Date.yesterday).to_i  
389 -  
390 - question.expire_prompt_cache_tracking_keys(Date.yesterday)  
391 -  
392 -  
393 -  
394 - yesterday_votes = question.appearances.count(:conditions => ['date(created_at) = ?', Date.yesterday])  
395 -  
396 - if misses + hits != yesterday_votes  
397 - error_msg += "Error! Question #{question.id} isn't tracking prompt cache hits and misses accurately! Expected #{yesterday_votes}, Actual: #{misses+hits}\n"  
398 - end  
399 -  
400 - miss_rate = misses.to_f / yesterday_votes.to_f  
401 - if miss_rate > 0.1  
402 - error_msg += "Error! Question #{question.id} has less than 90% of appearances taken from a pre-generated cache! Expected <#{0.1}, Actual: #{miss_rate}\n"  
403 - end  
404 - end  
405 -  
406 -  
407 - if error_bool  
408 - error_msg += "Question #{question.id}: 2*wins = #{2*total_wins}, total votes = #{total_votes}, vote_count = #{question.votes_count}\n"  
409 - end  
410 -  
411 - error_bool = false  
412 - end  
413 - 528 + return error_message.blank? ? [success_message, false] : [error_message, true]
  529 + end
  530 +
  531 + def ensure_all_votes_and_skips_have_unique_appearance
  532 + error_message = ""
  533 + success_message = "All vote and skip objects have an associated appearance object"
414 votes_without_appearances= Vote.count(:conditions => {:appearance_id => nil}) 534 votes_without_appearances= Vote.count(:conditions => {:appearance_id => nil})
415 if (votes_without_appearances > 0) 535 if (votes_without_appearances > 0)
416 - error_msg += "Error! There are #{votes_without_appearances} votes without associated appearance objects." 536 + error_message += "Error! There are #{votes_without_appearances} votes without associated appearance objects."
417 end 537 end
418 538
419 skips_without_appearances= Skip.count(:conditions => {:appearance_id => nil}) 539 skips_without_appearances= Skip.count(:conditions => {:appearance_id => nil})
420 if (skips_without_appearances > 0) 540 if (skips_without_appearances > 0)
421 - error_msg += "Error! There are #{skips_without_appearances} skips without associated appearance objects." 541 + error_message += "Error! There are #{skips_without_appearances} skips without associated appearance objects."
422 end 542 end
  543 +
  544 + return error_message.blank? ? [success_message, false] : [error_message, true]
  545 + end
423 546
  547 + def response_time_tests
  548 + error_message = ""
  549 + success_message = "All Vote objects have an client response time < calculated server roundtrip time\n"
424 550
425 recording_client_time_start_date = Vote.find(:all, :conditions => 'time_viewed IS NOT NULL', :order => 'created_at', :limit => 1).first.created_at 551 recording_client_time_start_date = Vote.find(:all, :conditions => 'time_viewed IS NOT NULL', :order => 'created_at', :limit => 1).first.created_at
426 552
427 Vote.find_each(:batch_size => 1000, :include => :appearance) do |v| 553 Vote.find_each(:batch_size => 1000, :include => :appearance) do |v|
428 554
429 -  
430 # Subtracting DateTime objects results in the difference in days 555 # Subtracting DateTime objects results in the difference in days
431 server_response_time = v.created_at.to_f - v.appearance.created_at.to_f 556 server_response_time = v.created_at.to_f - v.appearance.created_at.to_f
432 if server_response_time < 0 557 if server_response_time < 0
433 the_error_msg = "Error! Vote #{v.id} was created before the appearance associated with it: Appearance id: #{v.appearance.id}, Vote creation time: #{v.created_at.to_s}, Appearance creation time: #{v.appearance.created_at.to_s}\n" 558 the_error_msg = "Error! Vote #{v.id} was created before the appearance associated with it: Appearance id: #{v.appearance.id}, Vote creation time: #{v.created_at.to_s}, Appearance creation time: #{v.appearance.created_at.to_s}\n"
434 559
435 -  
436 - error_msg += the_error_msg  
437 - print the_error_msg  
438 -  
439 - print "Error!" 560 + error_message += the_error_msg
  561 + print "Error!" + the_error_msg
440 end 562 end
441 563
442 if v.time_viewed && v.time_viewed/1000 > server_response_time 564 if v.time_viewed && v.time_viewed/1000 > server_response_time
443 the_error_msg = "Error! Vote #{v.id} with Appearance #{v.appearance.id}, has a longer client response time than is possible. Vote creation time: #{v.created_at.to_s}, Appearance creation time: #{v.appearance.created_at.to_s}, Client side response time: #{v.time_viewed}\n" 565 the_error_msg = "Error! Vote #{v.id} with Appearance #{v.appearance.id}, has a longer client response time than is possible. Vote creation time: #{v.created_at.to_s}, Appearance creation time: #{v.appearance.created_at.to_s}, Client side response time: #{v.time_viewed}\n"
444 566
445 - error_msg += the_error_msg  
446 - print the_error_msg  
447 -  
448 - bad_votes << v.id 567 + error_message += the_error_msg
  568 + print the_error_msg
449 569
450 elsif v.time_viewed.nil? 570 elsif v.time_viewed.nil?
451 if v.created_at > recording_client_time_start_date && v.missing_response_time_exp != 'invalid' 571 if v.created_at > recording_client_time_start_date && v.missing_response_time_exp != 'invalid'
452 the_error_msg = "Error! Vote #{v.id} with Appearance #{v.appearance.id}, does not have a client response, even though it should! Vote creation time: #{v.created_at.to_s}, Appearance creation time: #{v.appearance.created_at.to_s}, Client side response time: #{v.time_viewed}\n" 572 the_error_msg = "Error! Vote #{v.id} with Appearance #{v.appearance.id}, does not have a client response, even though it should! Vote creation time: #{v.created_at.to_s}, Appearance creation time: #{v.appearance.created_at.to_s}, Client side response time: #{v.time_viewed}\n"
453 - error_msg += the_error_msg 573 + error_message += the_error_msg
454 print the_error_msg 574 print the_error_msg
455 end 575 end
456 576
457 end 577 end
458 578
459 -  
460 - end  
461 -  
462 - if error_msg.blank?  
463 -  
464 - success_msg = "Conducted the following tests on API data and found no inconsistencies.\n" +  
465 - "For each of the #{questions.length} questions in the database: \n" +  
466 - " 2 x Total Wins = Total Votes\n" +  
467 - " Total Votes (wins + losses) is Even\n" +  
468 - " Total Votes (wins + losses) = 2 x the number of vote objects that belong to the question\n" +  
469 - " Total generated prompts on left = Total generated prompts on right\n" +  
470 - " Each choice has appeared n times, where n falls within 6 stddevs of the mean number of appearances for a question\n" +  
471 - " Note: this applies only to seed choices (not user submitted) and choices currently marked active\n" +  
472 - " The cached score value matches the calculated score value for each choice\n" +  
473 - " The cached vote count matches the actual number of votes for each question\n" +  
474 - " The cached choices count matches the actual number of choices for each question\n" +  
475 - " The cached prompt count matches the actual number of prompts for each question\n" +  
476 - " The prompt count matches the expected number of prompts ( num_choices ^2 - num choices) for each question\n" +  
477 - " All Vote objects have an associated appearance object\n" +  
478 - " All Vote objects have an client response time < calculated server roundtrip time\n"  
479 - " More than 90% of prompts on catchup algorithm questions were served from cache\n"  
480 -  
481 - print success_msg  
482 -  
483 - CronMailer.deliver_info_message(CRON_EMAIL, "Test of API Vote Consistency passed", success_msg)  
484 - else  
485 - CronMailer.deliver_info_message("#{CRON_EMAIL},#{ERRORS_EMAIL}", "Error! Failure of API Vote Consistency " , error_msg)  
486 -  
487 - puts "There were errors: "  
488 - puts error_msg  
489 -  
490 - unless bad_choices.blank?  
491 -  
492 - puts "Here's a list of choice ids that you may want to modify: #{bad_choices.uniq.inspect}"  
493 -  
494 - end  
495 - unless bad_votes.blank?  
496 -  
497 - puts "Here's a list of vote ids that you may want to modify: #{bad_votes.uniq.inspect}"  
498 -  
499 - end  
500 - print error_msg 579 + end
  580 +
  581 + return error_message.blank? ? [success_message, false] : [error_message, true]
501 end 582 end
502 -  
503 - end  
504 end 583 end
505 584