diff --git a/src/main/java/com/codejoust/main/dao/GameReportRepository.java b/src/main/java/com/codejoust/main/dao/GameReportRepository.java new file mode 100644 index 000000000..6127a7bd0 --- /dev/null +++ b/src/main/java/com/codejoust/main/dao/GameReportRepository.java @@ -0,0 +1,10 @@ +package com.codejoust.main.dao; + +import com.codejoust.main.model.report.GameReport; + +import org.springframework.data.repository.CrudRepository; + +public interface GameReportRepository extends CrudRepository { + + GameReport findGameReportByGameReportId(String gameReportId); +} diff --git a/src/main/java/com/codejoust/main/dao/ProblemContainerRepository.java b/src/main/java/com/codejoust/main/dao/ProblemContainerRepository.java new file mode 100644 index 000000000..8d48e42cd --- /dev/null +++ b/src/main/java/com/codejoust/main/dao/ProblemContainerRepository.java @@ -0,0 +1,14 @@ +package com.codejoust.main.dao; + +import org.springframework.data.repository.CrudRepository; + +import java.util.List; + +import com.codejoust.main.model.problem.Problem; +import com.codejoust.main.model.problem.ProblemContainer; + +// This will be AUTO IMPLEMENTED by Spring into a Bean called +// problemContainerRepository; CRUD refers Create, Read, Update, Delete +public interface ProblemContainerRepository extends CrudRepository { + List findAllByProblem(Problem problem); +} diff --git a/src/main/java/com/codejoust/main/dao/RoomRepository.java b/src/main/java/com/codejoust/main/dao/RoomRepository.java index b4132eb7d..89d469be2 100644 --- a/src/main/java/com/codejoust/main/dao/RoomRepository.java +++ b/src/main/java/com/codejoust/main/dao/RoomRepository.java @@ -1,5 +1,7 @@ package com.codejoust.main.dao; +import java.util.List; + import com.codejoust.main.model.Room; import org.springframework.data.repository.CrudRepository; @@ -8,4 +10,6 @@ public interface RoomRepository extends CrudRepository { // Auto generated by CrudRepository Room findRoomByRoomId(String roomId); + + List findByProblems_ProblemId(String problemId); } diff --git a/src/main/java/com/codejoust/main/dto/game/SubmissionMapper.java b/src/main/java/com/codejoust/main/dto/game/SubmissionMapper.java index 0996d686b..ea07ea296 100644 --- a/src/main/java/com/codejoust/main/dto/game/SubmissionMapper.java +++ b/src/main/java/com/codejoust/main/dto/game/SubmissionMapper.java @@ -1,7 +1,11 @@ package com.codejoust.main.dto.game; import com.codejoust.main.dto.problem.ProblemTestCaseDto; +import com.codejoust.main.game_object.PlayerCode; +import com.codejoust.main.game_object.Submission; import com.codejoust.main.game_object.SubmissionResult; +import com.codejoust.main.model.report.CodeLanguage; +import com.codejoust.main.model.report.SubmissionReport; import org.modelmapper.ModelMapper; @@ -29,4 +33,18 @@ public static SubmissionResult toSubmissionResult(TesterResult testerResult, Pro submissionResult.setInput(testCaseDto.getInput()); return submissionResult; } + + public static SubmissionReport toSubmissionReport(Submission submission) { + if (submission == null) { + return null; + } + + SubmissionReport submissionReport = mapper.map(submission, SubmissionReport.class); + + // Get and set the player code for this submission. + PlayerCode playerCode = submission.getPlayerCode(); + submissionReport.setCode(playerCode.getCode()); + submissionReport.setLanguage(CodeLanguage.fromString(submission.getPlayerCode().getLanguage().name())); + return submissionReport; + } } diff --git a/src/main/java/com/codejoust/main/exception/TimerError.java b/src/main/java/com/codejoust/main/exception/TimerError.java index b36ab0854..150533f87 100644 --- a/src/main/java/com/codejoust/main/exception/TimerError.java +++ b/src/main/java/com/codejoust/main/exception/TimerError.java @@ -11,7 +11,7 @@ public enum TimerError implements ApiError { INVALID_DURATION(HttpStatus.BAD_REQUEST, "Please enter a valid duration between 1-60 minutes."), - NULL_SETTING(HttpStatus.BAD_REQUEST, "The game, associated game timer, room, room ID, and socket service must not be null."); + NULL_SETTING(HttpStatus.BAD_REQUEST, "The relevant game settings must not be null."); private final HttpStatus status; private final ApiErrorResponse response; diff --git a/src/main/java/com/codejoust/main/game_object/Game.java b/src/main/java/com/codejoust/main/game_object/Game.java index 3df57e7a4..b6e51a7d1 100644 --- a/src/main/java/com/codejoust/main/game_object/Game.java +++ b/src/main/java/com/codejoust/main/game_object/Game.java @@ -31,4 +31,7 @@ public class Game { // Boolean to hold whether the host ended the game early private Boolean gameEnded = false; + + // Boolean to hold whether the process of creating the game report started + private Boolean createGameReportStarted = false; } diff --git a/src/main/java/com/codejoust/main/game_object/Submission.java b/src/main/java/com/codejoust/main/game_object/Submission.java index fc7f4d8a9..a43cc5428 100644 --- a/src/main/java/com/codejoust/main/game_object/Submission.java +++ b/src/main/java/com/codejoust/main/game_object/Submission.java @@ -13,6 +13,7 @@ public class Submission { private PlayerCode playerCode; + private int problemIndex; private List results; diff --git a/src/main/java/com/codejoust/main/model/Account.java b/src/main/java/com/codejoust/main/model/Account.java index 3c0f6413a..300e717f7 100644 --- a/src/main/java/com/codejoust/main/model/Account.java +++ b/src/main/java/com/codejoust/main/model/Account.java @@ -52,7 +52,7 @@ public class Account { private List problemTags = new ArrayList<>(); // List of tags associated with this problem - @ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REFRESH, CascadeType.DETACH}, fetch = FetchType.EAGER) + @ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL) @Setter(AccessLevel.PRIVATE) @JoinColumn(name = "game_report_id") @Fetch(value = FetchMode.SUBSELECT) @@ -60,4 +60,8 @@ public class Account { @Enumerated(EnumType.STRING) private AccountRole role = AccountRole.TEACHER; + + public void addGameReport(GameReport gameReport) { + gameReports.add(0, gameReport); + } } diff --git a/src/main/java/com/codejoust/main/model/Room.java b/src/main/java/com/codejoust/main/model/Room.java index 5323ac7df..1af7690ab 100644 --- a/src/main/java/com/codejoust/main/model/Room.java +++ b/src/main/java/com/codejoust/main/model/Room.java @@ -148,4 +148,12 @@ public boolean isFull() { return users.size() >= size; } + + public boolean addProblem(Problem problem) { + return problems.add(problem); + } + + public boolean removeProblem(Problem problem) { + return problems.remove(problem); + } } diff --git a/src/main/java/com/codejoust/main/model/User.java b/src/main/java/com/codejoust/main/model/User.java index bbb742fde..a6f69ea4e 100644 --- a/src/main/java/com/codejoust/main/model/User.java +++ b/src/main/java/com/codejoust/main/model/User.java @@ -3,6 +3,7 @@ import java.util.ArrayList; import java.util.List; +import javax.persistence.CascadeType; import javax.persistence.Entity; import javax.persistence.FetchType; import javax.persistence.GeneratedValue; @@ -58,9 +59,13 @@ public class User { private Room room; // Thie list holds the submission group associated with the current room - @OneToMany(fetch = FetchType.EAGER) + @OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL) @Setter(AccessLevel.PRIVATE) @Fetch(value = FetchMode.SUBSELECT) @JoinColumn(name = "submission_group_reports_table_id") private List submissionGroupReports = new ArrayList<>(); + + public void addSubmissionGroupReport(SubmissionGroupReport submissionGroupReport) { + submissionGroupReports.add(submissionGroupReport); + } } diff --git a/src/main/java/com/codejoust/main/model/problem/ProblemTestCase.java b/src/main/java/com/codejoust/main/model/problem/ProblemTestCase.java index 0043ac49a..ae64ae830 100644 --- a/src/main/java/com/codejoust/main/model/problem/ProblemTestCase.java +++ b/src/main/java/com/codejoust/main/model/problem/ProblemTestCase.java @@ -4,6 +4,7 @@ import lombok.Getter; import lombok.Setter; +import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.FetchType; import javax.persistence.GeneratedValue; @@ -24,9 +25,11 @@ public class ProblemTestCase { private Integer id; @EqualsAndHashCode.Include + @Column(columnDefinition = "TEXT") private String input; @EqualsAndHashCode.Include + @Column(columnDefinition = "TEXT") private String output; @EqualsAndHashCode.Include @@ -37,5 +40,6 @@ public class ProblemTestCase { private Problem problem; @EqualsAndHashCode.Include + @Column(columnDefinition = "TEXT") private String explanation; } diff --git a/src/main/java/com/codejoust/main/model/report/GameReport.java b/src/main/java/com/codejoust/main/model/report/GameReport.java index 153f752f9..4bc00c9ef 100644 --- a/src/main/java/com/codejoust/main/model/report/GameReport.java +++ b/src/main/java/com/codejoust/main/model/report/GameReport.java @@ -39,18 +39,26 @@ public class GameReport { @EqualsAndHashCode.Include private String gameReportId = UUID.randomUUID().toString(); - @OneToMany(fetch = FetchType.EAGER) + @OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL) @Setter(AccessLevel.PRIVATE) @Fetch(value = FetchMode.SUBSELECT) @JoinColumn(name = "problem_containers_table_id") private List problemContainers = new ArrayList<>(); - @ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REFRESH, CascadeType.DETACH}, fetch = FetchType.EAGER) + @ManyToMany(cascade = CascadeType.MERGE, fetch = FetchType.EAGER) @Setter(AccessLevel.PRIVATE) - @JoinColumn(name = "users_table_id") @Fetch(value = FetchMode.SUBSELECT) + @JoinColumn(name = "users_table_id") private List users = new ArrayList<>(); + private int numTestCases; + + // The average (mean) number of test cases passed. + private Double averageTestCasesPassed; + + // The average (mean) number of problems solved. + private Double averageProblemsSolved; + // The start time of the game private Instant createdDateTime; @@ -59,4 +67,12 @@ public class GameReport { // How the game ended. private GameEndType gameEndType; + + public void addProblemContainer(ProblemContainer problemContainer) { + problemContainers.add(problemContainer); + } + + public void addUser(User user) { + users.add(user); + } } diff --git a/src/main/java/com/codejoust/main/model/report/SubmissionGroupReport.java b/src/main/java/com/codejoust/main/model/report/SubmissionGroupReport.java index 9b419f828..17f25ec7c 100644 --- a/src/main/java/com/codejoust/main/model/report/SubmissionGroupReport.java +++ b/src/main/java/com/codejoust/main/model/report/SubmissionGroupReport.java @@ -3,6 +3,7 @@ import java.util.ArrayList; import java.util.List; +import javax.persistence.CascadeType; import javax.persistence.Entity; import javax.persistence.FetchType; import javax.persistence.GeneratedValue; @@ -31,9 +32,22 @@ public class SubmissionGroupReport { private String gameReportId; - @OneToMany(fetch = FetchType.EAGER) + /** + * String to represent problems solved. + * Each index represents the problem index. + * 0 = Not Solved, 1 = Solved. + */ + private String problemsSolved; + + private Integer numTestCasesPassed; + + @OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL) @Setter(AccessLevel.PRIVATE) @Fetch(value = FetchMode.SUBSELECT) @JoinColumn(name = "submission_reports_table_id") private List submissionReports = new ArrayList<>(); + + public void addSubmissionReport(SubmissionReport submissionReport) { + submissionReports.add(submissionReport); + } } diff --git a/src/main/java/com/codejoust/main/model/report/SubmissionReport.java b/src/main/java/com/codejoust/main/model/report/SubmissionReport.java index 34e022941..fac00139b 100644 --- a/src/main/java/com/codejoust/main/model/report/SubmissionReport.java +++ b/src/main/java/com/codejoust/main/model/report/SubmissionReport.java @@ -2,6 +2,7 @@ import java.time.Instant; +import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; @@ -21,10 +22,13 @@ public class SubmissionReport { @GeneratedValue(strategy = GenerationType.AUTO) private Integer id; + @Column(columnDefinition = "TEXT") private String code; private CodeLanguage language; + private int problemIndex; + // The time that the submission was received. private Instant startTime; diff --git a/src/main/java/com/codejoust/main/service/GameManagementService.java b/src/main/java/com/codejoust/main/service/GameManagementService.java index 6cb8382e5..6139de70b 100644 --- a/src/main/java/com/codejoust/main/service/GameManagementService.java +++ b/src/main/java/com/codejoust/main/service/GameManagementService.java @@ -28,7 +28,7 @@ import com.codejoust.main.model.Room; import com.codejoust.main.model.User; import com.codejoust.main.model.problem.Problem; -import com.codejoust.main.util.EndGameTimerTask; +import com.codejoust.main.task.EndGameTimerTask; import com.codejoust.main.util.Utility; import lombok.extern.log4j.Log4j2; @@ -45,18 +45,24 @@ public class GameManagementService { private final NotificationService notificationService; private final SubmitService submitService; private final ProblemService problemService; + private final ReportService reportService; private final Map currentGameMap; @Autowired - protected GameManagementService(RoomRepository repository, SocketService socketService, - LiveGameService liveGameService, NotificationService notificationService, - SubmitService submitService, ProblemService problemService) { + protected GameManagementService(RoomRepository repository, + SocketService socketService, + LiveGameService liveGameService, + NotificationService notificationService, + SubmitService submitService, + ProblemService problemService, + ReportService reportService) { this.repository = repository; this.socketService = socketService; this.liveGameService = liveGameService; this.notificationService = notificationService; this.submitService = submitService; this.problemService = problemService; + this.reportService = reportService; currentGameMap = new HashMap<>(); } @@ -106,7 +112,7 @@ public RoomDto startGame(String roomId, StartGameRequest request) { public RoomDto playAgain(String roomId, PlayAgainRequest request) { Game game = getGameFromRoomId(roomId); - if (!isGameOver(game)) { + if (!Utility.isGameOver(game)) { throw new ApiException(GameError.GAME_NOT_OVER); } @@ -166,7 +172,7 @@ public void setStartGameTimer(Game game, Long duration) { game.setGameTimer(gameTimer); // Schedule the game to end after seconds. - EndGameTimerTask endGameTimerTask = new EndGameTimerTask(socketService, game); + EndGameTimerTask endGameTimerTask = new EndGameTimerTask(this, socketService, game); gameTimer.getTimer().schedule(endGameTimerTask, duration * 1000); } @@ -209,7 +215,7 @@ public SubmissionDto submitSolution(String roomId, SubmissionRequest request) { SubmissionDto submissionDto = submitService.submitSolution(game, request); - if (isGameOver(game)) { + if (Utility.isGameOver(game)) { handleEndGame(game); } @@ -283,7 +289,7 @@ public GameDto manuallyEndGame(String roomId, EndGameRequest request) { return gameDto; } - protected void handleEndGame(Game game) { + public void handleEndGame(Game game) { // Cancel all previously scheduled timers GameTimer gameTimer = game.getGameTimer(); gameTimer.getTimer().cancel(); @@ -291,10 +297,8 @@ protected void handleEndGame(Game game) { for (Timer timer : gameTimer.getNotificationTimers()) { timer.cancel(); } - } - protected boolean isGameOver(Game game) { - return game.getGameEnded() || game.getAllSolved() || (game.getGameTimer() != null && game.getGameTimer().isTimeUp()); + reportService.createGameReport(game); } // Update people's socket active status diff --git a/src/main/java/com/codejoust/main/service/NotificationService.java b/src/main/java/com/codejoust/main/service/NotificationService.java index 4b9a5d8ac..5f24d3d9e 100644 --- a/src/main/java/com/codejoust/main/service/NotificationService.java +++ b/src/main/java/com/codejoust/main/service/NotificationService.java @@ -6,7 +6,7 @@ import com.codejoust.main.dto.game.GameNotificationDto; import com.codejoust.main.game_object.Game; import com.codejoust.main.game_object.GameTimer; -import com.codejoust.main.util.NotificationTimerTask; +import com.codejoust.main.task.NotificationTimerTask; import org.springframework.stereotype.Service; diff --git a/src/main/java/com/codejoust/main/service/ProblemService.java b/src/main/java/com/codejoust/main/service/ProblemService.java index 0a31f1bd3..afd0f6a33 100644 --- a/src/main/java/com/codejoust/main/service/ProblemService.java +++ b/src/main/java/com/codejoust/main/service/ProblemService.java @@ -1,8 +1,10 @@ package com.codejoust.main.service; import com.codejoust.main.dao.AccountRepository; +import com.codejoust.main.dao.ProblemContainerRepository; import com.codejoust.main.dao.ProblemRepository; import com.codejoust.main.dao.ProblemTagRepository; +import com.codejoust.main.dao.RoomRepository; import com.codejoust.main.dto.account.AccountRole; import com.codejoust.main.dto.problem.CreateProblemRequest; import com.codejoust.main.dto.problem.CreateProblemTagRequest; @@ -16,7 +18,9 @@ import com.codejoust.main.exception.ProblemError; import com.codejoust.main.exception.api.ApiException; import com.codejoust.main.model.Account; +import com.codejoust.main.model.Room; import com.codejoust.main.model.problem.Problem; +import com.codejoust.main.model.problem.ProblemContainer; import com.codejoust.main.model.problem.ProblemDifficulty; import com.codejoust.main.model.problem.ProblemIOType; import com.codejoust.main.model.problem.ProblemInput; @@ -47,7 +51,9 @@ public class ProblemService { private final FirebaseService service; private final ProblemRepository problemRepository; private final ProblemTagRepository problemTagRepository; + private final ProblemContainerRepository problemContainerRepository; private final AccountRepository accountRepository; + private final RoomRepository roomRepository; private final List defaultCodeGeneratorServiceList; private final Random random = new Random(); private final Gson gson = new Gson(); @@ -56,13 +62,17 @@ public class ProblemService { public ProblemService(FirebaseService service, ProblemRepository problemRepository, ProblemTagRepository problemTagRepository, + ProblemContainerRepository problemContainerRepository, AccountRepository accountRepository, + RoomRepository roomRepository, List defaultCodeGeneratorServiceList) { this.service = service; this.problemRepository = problemRepository; this.problemTagRepository = problemTagRepository; + this.problemContainerRepository = problemContainerRepository; this.accountRepository = accountRepository; + this.roomRepository = roomRepository; this.defaultCodeGeneratorServiceList = defaultCodeGeneratorServiceList; } @@ -243,6 +253,25 @@ public ProblemDto deleteProblem(String problemId, String token) { } service.verifyTokenMatchesUid(token, problem.getOwner().getUid()); + + /** + * Before the problem can be deleted, set all foreign key references + * to the Problem within all ProblemContainers to null. + * See https://stackoverflow.com/a/10030873/7517518. + */ + List problemContainers = problemContainerRepository.findAllByProblem(problem); + for (ProblemContainer problemContainer : problemContainers) { + problemContainer.setProblem(null); + problemContainerRepository.save(problemContainer); + } + + // Remove this problem from all the associated rooms. + List rooms = roomRepository.findByProblems_ProblemId(problemId); + for (Room room : rooms) { + room.removeProblem(problem); + roomRepository.save(room); + } + problemRepository.delete(problem); return ProblemMapper.toDto(problem); diff --git a/src/main/java/com/codejoust/main/service/ReportService.java b/src/main/java/com/codejoust/main/service/ReportService.java new file mode 100644 index 000000000..582c3440a --- /dev/null +++ b/src/main/java/com/codejoust/main/service/ReportService.java @@ -0,0 +1,244 @@ +package com.codejoust.main.service; + +import java.time.Duration; +import java.time.Instant; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import com.codejoust.main.dao.ProblemRepository; +import com.codejoust.main.dao.AccountRepository; +import com.codejoust.main.dao.GameReportRepository; +import com.codejoust.main.dto.game.SubmissionMapper; +import com.codejoust.main.game_object.Game; +import com.codejoust.main.game_object.Player; +import com.codejoust.main.game_object.Submission; +import com.codejoust.main.model.Account; +import com.codejoust.main.model.User; +import com.codejoust.main.model.problem.Problem; +import com.codejoust.main.model.problem.ProblemContainer; +import com.codejoust.main.model.report.GameEndType; +import com.codejoust.main.model.report.GameReport; +import com.codejoust.main.model.report.SubmissionGroupReport; + +import lombok.extern.log4j.Log4j2; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Log4j2 +@Service +public class ReportService { + + private final ProblemRepository problemRepository; + private final GameReportRepository gameReportRepository; + private final AccountRepository accountRepository; + + @Autowired + protected ReportService(ProblemRepository problemRepository, + GameReportRepository gameReportRepository, + AccountRepository accountRepository) { + this.problemRepository = problemRepository; + this.gameReportRepository = gameReportRepository; + this.accountRepository = accountRepository; + } + + public GameReport createGameReport(Game game) { + // Check if game report has already been created (or attempted). + if (game.getCreateGameReportStarted()) { + return null; + } + game.setCreateGameReportStarted(true); + + GameReport gameReport = new GameReport(); + int numProblems = game.getProblems().size(); + int numPlayers = game.getPlayers().size(); + + // Compute the statistic variables per problem, add submission reports. + int[] userSolved = new int[numProblems]; + double[] totalTestCasesPassed = new double[numProblems]; + double[] totalAttemptCount = new double[numProblems]; + computeGameStatisticsAndAddSubmissions(gameReport, game, numProblems, + numPlayers, userSolved, totalTestCasesPassed, totalAttemptCount); + setGameReportStatistics(gameReport, game, numPlayers, userSolved, + totalTestCasesPassed); + createProblemContainers(gameReport, game, numProblems, numPlayers, + userSolved, totalTestCasesPassed, totalAttemptCount); + gameReportRepository.save(gameReport); + addGameReportAccounts(gameReport, game); + + // Log the completion of the latest game report. + log.info("Created game report for game with Room ID {}", game.getRoom().getRoomId()); + return gameReport; + } + + /** + * Compute the game statistics userSolved, totalTestCasesPassed, and + * totalAttemptCount. Submission reports and submission group reports + * were also added. + * + * @param gameReport The game report in progress. + * @param game The game that has just concluded. + * @param numProblems The number of problems in the game. + * @param numPlayers The number of players (non-spectators) in the game. + * @param userSolved The number of users that solved each problem. + * @param totalTestCasesPassed The total test cases passed across all + * users for each problem. + * @param totalAttemptCount The total attempt count across all users for + * each problem. + */ + public void computeGameStatisticsAndAddSubmissions(GameReport gameReport, + Game game, int numProblems, int numPlayers, int[] userSolved, + double[] totalTestCasesPassed, double[] totalAttemptCount) { + + for (Player player : game.getPlayers().values()) { + // For each user, add the relevant submission info. + User user = player.getUser(); + + // Construct the new submission group report for each user. + SubmissionGroupReport submissionGroupReport = new SubmissionGroupReport(); + submissionGroupReport.setGameReportId(gameReport.getGameReportId()); + + // Iterate through each submission and update group statistics. + boolean[] problemsSolved = new boolean[numProblems]; + int[] testCasesPassed = new int[numProblems]; + for (Submission submission : player.getSubmissions()) { + int problemIndex = submission.getProblemIndex(); + totalAttemptCount[problemIndex]++; + + // If the problem was solved, set boolean value to true. + if (submission.getNumTestCases() == submission.getNumCorrect() && !problemsSolved[problemIndex]) { + problemsSolved[problemIndex] = true; + userSolved[problemIndex]++; + } + + // Get the maximum number of test cases passed for each problem. + testCasesPassed[problemIndex] = Math.max(testCasesPassed[problemIndex], submission.getNumCorrect()); + + submissionGroupReport.addSubmissionReport(SubmissionMapper.toSubmissionReport(submission)); + } + + // Iterate through the test cases passed to add to the game total. + for (int i = 0; i < testCasesPassed.length; i++) { + totalTestCasesPassed[i] += testCasesPassed[i]; + } + + // Set the problems and test cases statistics. + submissionGroupReport.setProblemsSolved(compactProblemsSolved(problemsSolved)); + submissionGroupReport.setNumTestCasesPassed(Arrays.stream(testCasesPassed).sum()); + + // Add the submission group report and the user. + user.addSubmissionGroupReport(submissionGroupReport); + gameReport.addUser(user); + } + } + + /** + * Set the game report statistics, including the timing information, + * the game end type, and test case and solve data. + * + * @param gameReport The game report in progress. + * @param game The game that has just concluded. + * @param numPlayers The number of players (non-spectators) in the game. + * @param userSolved The number of users that solved each problem. + * @param totalTestCasesPassed The total test cases passed across all + * users for each problem. + */ + public void setGameReportStatistics(GameReport gameReport, Game game, + int numPlayers, int[] userSolved, double[] totalTestCasesPassed) { + + Instant startTime = game.getGameTimer().getStartTime(); + gameReport.setCreatedDateTime(startTime); + gameReport.setDuration(Duration.between(startTime, Instant.now()).getSeconds()); + + if (game.getGameEnded()) { + gameReport.setGameEndType(GameEndType.MANUAL_END); + } else if (game.getAllSolved()) { + gameReport.setGameEndType(GameEndType.ALL_SOLVED); + } else { + gameReport.setGameEndType(GameEndType.TIME_UP); + } + + gameReport.setAverageTestCasesPassed(Arrays.stream(totalTestCasesPassed).sum() / numPlayers); + gameReport.setAverageProblemsSolved((double) Arrays.stream(userSolved).sum() / numPlayers); + } + + /** + * Create and add the problem containers to the game report, as well + * as iterating through and setting the number of test cases. + * + * @param gameReport The game report in progress. + * @param game The game that has just concluded. + * @param numProblems The number of problems in the game. + * @param numPlayers The number of players (non-spectators) in the game. + * @param userSolved The number of users that solved each problem. + * @param totalTestCasesPassed The total test cases passed across all + * users for each problem. + * @param totalAttemptCount The total attempt count across all users for + * each problem. + */ + public void createProblemContainers(GameReport gameReport, Game game, + int numProblems, int numPlayers, int[] userSolved, + double[] totalTestCasesPassed, double[] totalAttemptCount) { + + // Set problem container variables. + int numTestCases = 0; + List problems = game.getProblems(); + for (int i = 0; i < numProblems; i++) { + Problem problem = problems.get(i); + numTestCases += problem.getTestCases().size(); + + ProblemContainer problemContainer = new ProblemContainer(); + problemContainer.setUserSolvedCount(userSolved[i]); + problemContainer.setTestCaseCount(problem.getTestCases().size()); + problemContainer.setAverageTestCasesPassed(totalTestCasesPassed[i] / numPlayers); + problemContainer.setAverageAttemptCount(totalAttemptCount[i] / numPlayers); + + // Set the Problem fields with the updated database problem. + problemContainer.setProblem(problemRepository.findProblemByProblemId(problem.getProblemId())); + gameReport.addProblemContainer(problemContainer); + } + gameReport.setNumTestCases(numTestCases); + } + + /** + * Add the game report to the associated accounts (players and spectators). + * + * @param gameReport The game report in progress. + * @param game The game that has just concluded. + */ + public void addGameReportAccounts(GameReport gameReport, Game game) { + // Iterate through all room users, players and spectators included. + Set addedAccounts = new HashSet<>(); + for (User user : game.getRoom().getUsers()) { + // If account exists and game report is not added, add game report. + Account account = user.getAccount(); + if (account != null && !addedAccounts.contains(account)) { + account.addGameReport(gameReport); + addedAccounts.add(account); + accountRepository.save(account); + } + } + } + + /** + * The String that represents the different problems solved by a user, + * where the index of the String represents a specific problem and a + * 1 = solved, 0 = not solved. + * + * @param problemsSolved The boolean array representing the problems + * solved by the user. + * @return A String representing the problems solved by the user. + */ + private String compactProblemsSolved(boolean[] problemsSolved) { + StringBuilder builder = new StringBuilder(); + for (boolean problemSolved : problemsSolved) { + if (problemSolved) { + builder.append("1"); + } else { + builder.append("0"); + } + } + return builder.toString(); + } +} diff --git a/src/main/java/com/codejoust/main/service/SubmitService.java b/src/main/java/com/codejoust/main/service/SubmitService.java index f3e113faa..987da15f5 100644 --- a/src/main/java/com/codejoust/main/service/SubmitService.java +++ b/src/main/java/com/codejoust/main/service/SubmitService.java @@ -25,6 +25,7 @@ import com.codejoust.main.game_object.Submission; import com.codejoust.main.game_object.SubmissionResult; import com.codejoust.main.model.problem.Problem; +import com.codejoust.main.util.Utility; import com.google.gson.Gson; import lombok.extern.log4j.Log4j2; @@ -91,8 +92,6 @@ private Submission getDummySubmission(TesterRequest request) { // Test the submission and send a socket update. public SubmissionDto runCode(Game game, SubmissionRequest request) { - String userId = request.getInitiator().getUserId(); - PlayerCode playerCode = new PlayerCode(); playerCode.setCode(request.getCode()); playerCode.setLanguage(request.getLanguage()); @@ -146,29 +145,34 @@ public SubmissionDto submitSolution(Game game, SubmissionRequest request) { testerRequest.setProblem(problemDto); Submission submission = getSubmission(testerRequest); - submission.setProblemIndex(request.getProblemIndex()); - player.getSubmissions().add(submission); - if (submission.getNumCorrect().equals(submission.getNumTestCases())) { - player.getSolved()[request.getProblemIndex()] = true; - } + // Add submission score if game is not over. + if (!Utility.isGameOver(game)) { + submission.setProblemIndex(request.getProblemIndex()); + player.getSubmissions().add(submission); - // Variable to indicate whether all players have solved the problem. - boolean allSolved = true; - for (Player p : game.getPlayers().values()) { - for (Boolean b : p.getSolved()) { - if (b == null || !b) { - allSolved = false; - break; + if (submission.getNumCorrect().equals(submission.getNumTestCases())) { + player.getSolved()[request.getProblemIndex()] = true; + } + + // Variable to indicate whether all players have solved the problem. + boolean allSolved = true; + for (Player p : game.getPlayers().values()) { + for (Boolean b : p.getSolved()) { + if (b == null || !b) { + allSolved = false; + break; + } } } - } - // If the users have all completed the problem, set all solved to true. - if (allSolved) { - game.setAllSolved(true); + // If the users have all completed the problem, set all solved to true. + if (allSolved) { + game.setAllSolved(true); + } } + // TODO: Will this be an issue if the submission is not actually counted? Should I return null in that case? return GameMapper.submissionToDto(submission); } diff --git a/src/main/java/com/codejoust/main/util/EndGameTimerTask.java b/src/main/java/com/codejoust/main/task/EndGameTimerTask.java similarity index 64% rename from src/main/java/com/codejoust/main/util/EndGameTimerTask.java rename to src/main/java/com/codejoust/main/task/EndGameTimerTask.java index 1249baf3e..738c59ef4 100644 --- a/src/main/java/com/codejoust/main/util/EndGameTimerTask.java +++ b/src/main/java/com/codejoust/main/task/EndGameTimerTask.java @@ -1,4 +1,4 @@ -package com.codejoust.main.util; +package com.codejoust.main.task; import java.util.TimerTask; @@ -7,20 +7,25 @@ import com.codejoust.main.exception.TimerError; import com.codejoust.main.exception.api.ApiException; import com.codejoust.main.game_object.Game; +import com.codejoust.main.service.GameManagementService; import com.codejoust.main.service.SocketService; public class EndGameTimerTask extends TimerTask { private final Game game; + private final GameManagementService gameManagementService; + private final SocketService socketService; - public EndGameTimerTask(SocketService socketService, Game game) { + public EndGameTimerTask(GameManagementService gameManagementService, + SocketService socketService, Game game) { + this.gameManagementService = gameManagementService; this.socketService = socketService; this.game = game; // Handle potential errors for run(). - if (game == null || game.getGameTimer() == null || game.getRoom() == null || game.getRoom().getRoomId() == null || socketService == null) { + if (game == null || socketService == null || gameManagementService == null) { throw new ApiException(TimerError.NULL_SETTING); } } @@ -33,6 +38,9 @@ public void run() { // Get the Game DTO and send the relevant socket update. GameDto gameDto = GameMapper.toDto(game); socketService.sendSocketUpdate(gameDto); + + // Create the game report. + gameManagementService.handleEndGame(game); } } diff --git a/src/main/java/com/codejoust/main/util/NotificationTimerTask.java b/src/main/java/com/codejoust/main/task/NotificationTimerTask.java similarity index 97% rename from src/main/java/com/codejoust/main/util/NotificationTimerTask.java rename to src/main/java/com/codejoust/main/task/NotificationTimerTask.java index cacb85136..210de4b8a 100644 --- a/src/main/java/com/codejoust/main/util/NotificationTimerTask.java +++ b/src/main/java/com/codejoust/main/task/NotificationTimerTask.java @@ -1,4 +1,4 @@ -package com.codejoust.main.util; +package com.codejoust.main.task; import java.time.Instant; import java.util.TimerTask; diff --git a/src/main/java/com/codejoust/main/util/Utility.java b/src/main/java/com/codejoust/main/util/Utility.java index c3ef7e3b1..784247528 100644 --- a/src/main/java/com/codejoust/main/util/Utility.java +++ b/src/main/java/com/codejoust/main/util/Utility.java @@ -12,6 +12,7 @@ import com.codejoust.main.dao.RoomRepository; import com.codejoust.main.dao.UserRepository; +import com.codejoust.main.game_object.Game; import com.codejoust.main.game_object.NotificationType; import org.springframework.beans.factory.annotation.Autowired; @@ -131,4 +132,14 @@ && isLetter(identifier.charAt(0)) private static boolean isLetter(Character c) { return Character.toUpperCase(c) != Character.toLowerCase(c); } + + /** + * Check if the game is over. + * + * @param game the game in question + * @return a boolean verifying whether the game is over + */ + public static boolean isGameOver(Game game) { + return game.getGameEnded() || game.getAllSolved() || (game.getGameTimer() != null && game.getGameTimer().isTimeUp()); + } } diff --git a/src/test/java/com/codejoust/main/api/GameTests.java b/src/test/java/com/codejoust/main/api/GameTests.java index e7e9eadfc..d12a52ab8 100644 --- a/src/test/java/com/codejoust/main/api/GameTests.java +++ b/src/test/java/com/codejoust/main/api/GameTests.java @@ -38,7 +38,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; diff --git a/src/test/java/com/codejoust/main/mapper/GameMapperTests.java b/src/test/java/com/codejoust/main/mapper/GameMapperTests.java index 3731e8c3d..e7e24d506 100644 --- a/src/test/java/com/codejoust/main/mapper/GameMapperTests.java +++ b/src/test/java/com/codejoust/main/mapper/GameMapperTests.java @@ -19,7 +19,6 @@ import com.codejoust.main.dto.game.SubmissionDto; import com.codejoust.main.dto.game.SubmissionResultDto; import com.codejoust.main.dto.problem.ProblemDto; -import com.codejoust.main.dto.problem.ProblemMapper; import com.codejoust.main.dto.problem.ProblemTestCaseDto; import com.codejoust.main.dto.room.RoomMapper; import com.codejoust.main.dto.user.UserMapper; diff --git a/src/test/java/com/codejoust/main/service/GameManagementServiceTests.java b/src/test/java/com/codejoust/main/service/GameManagementServiceTests.java index daaf259a9..4f28ad4cd 100644 --- a/src/test/java/com/codejoust/main/service/GameManagementServiceTests.java +++ b/src/test/java/com/codejoust/main/service/GameManagementServiceTests.java @@ -27,7 +27,10 @@ import java.util.Arrays; import java.util.Collections; +import com.codejoust.main.dao.AccountRepository; +import com.codejoust.main.dao.GameReportRepository; import com.codejoust.main.dao.RoomRepository; +import com.codejoust.main.dao.UserRepository; import com.codejoust.main.dto.game.GameDto; import com.codejoust.main.dto.game.GameMapper; import com.codejoust.main.dto.game.GameNotificationDto; @@ -44,14 +47,13 @@ import com.codejoust.main.exception.RoomError; import com.codejoust.main.exception.api.ApiException; import com.codejoust.main.game_object.Game; -import com.codejoust.main.game_object.GameTimer; import com.codejoust.main.game_object.NotificationType; import com.codejoust.main.game_object.Player; -import com.codejoust.main.game_object.Submission; import com.codejoust.main.model.Room; import com.codejoust.main.model.User; import com.codejoust.main.model.problem.Problem; import com.codejoust.main.model.problem.ProblemDifficulty; +import com.codejoust.main.util.UtilityTestMethods; @ExtendWith(MockitoExtension.class) public class GameManagementServiceTests { @@ -59,6 +61,15 @@ public class GameManagementServiceTests { @Mock private RoomRepository repository; + @Mock + private GameReportRepository gameReportRepository; + + @Mock + private AccountRepository accountRepository; + + @Mock + private UserRepository userRepository; + @Mock private SocketService socketService; @@ -74,23 +85,13 @@ public class GameManagementServiceTests { @Mock private LiveGameService liveGameService; + @Mock + private ReportService reportService; + @Spy @InjectMocks private GameManagementService gameService; - // Helper method to add a dummy submission to a Player object - private void addSubmissionHelper(Player player, int numCorrect) { - Submission submission = new Submission(); - submission.setNumCorrect(numCorrect); - submission.setNumTestCases(TestFields.NUM_PROBLEMS); - submission.setStartTime(Instant.now()); - - player.getSubmissions().add(submission); - if (numCorrect == TestFields.NUM_PROBLEMS) { - player.setSolved(new boolean[]{true}); - } - } - @Test public void addGetAndRemoveGame() { // Initially, room doesn't exist @@ -360,7 +361,7 @@ public void submitSolutionSuccess() { submissionDto.setNumTestCases(TestFields.NUM_PROBLEMS); Mockito.doAnswer(new Answer() { public SubmissionDto answer(InvocationOnMock invocation) { - addSubmissionHelper(game.getPlayers().get(TestFields.USER_ID), 10); + UtilityTestMethods.addSubmissionHelper(game.getPlayers().get(TestFields.USER_ID), 0, TestFields.PLAYER_CODE_1, 1); game.setAllSolved(true); return submissionDto; }}) @@ -401,8 +402,8 @@ public void sendAllSolvedSocketUpdate() { Game game = gameService.getGameFromRoomId(TestFields.ROOM_ID); // Add submissions for the first two users. - addSubmissionHelper(game.getPlayers().get(TestFields.USER_ID), 10); - addSubmissionHelper(game.getPlayers().get(TestFields.USER_ID_2), 10); + UtilityTestMethods.addSubmissionHelper(game.getPlayers().get(TestFields.USER_ID), 0, TestFields.PLAYER_CODE_1, 1); + UtilityTestMethods.addSubmissionHelper(game.getPlayers().get(TestFields.USER_ID_2), 0, TestFields.PLAYER_CODE_1, 1); SubmissionRequest request = new SubmissionRequest(); request.setLanguage(TestFields.PYTHON_LANGUAGE); @@ -415,7 +416,7 @@ public void sendAllSolvedSocketUpdate() { submissionDto.setNumTestCases(TestFields.NUM_PROBLEMS); Mockito.doAnswer(new Answer() { public SubmissionDto answer(InvocationOnMock invocation) { - addSubmissionHelper(game.getPlayers().get(TestFields.USER_ID_3), 10); + UtilityTestMethods.addSubmissionHelper(game.getPlayers().get(TestFields.USER_ID_3), 0,TestFields.PLAYER_CODE_1, 1); game.setAllSolved(true); return submissionDto; }}) @@ -461,7 +462,7 @@ public void submitSolutionNotAllSolved() { submissionDto.setNumTestCases(TestFields.NUM_PROBLEMS); Mockito.doAnswer(new Answer() { public SubmissionDto answer(InvocationOnMock invocation) { - addSubmissionHelper(game.getPlayers().get(TestFields.USER_ID), 10); + UtilityTestMethods.addSubmissionHelper(game.getPlayers().get(TestFields.USER_ID), 0, TestFields.PLAYER_CODE_1, 1); return submissionDto; }}) .when(submitService).submitSolution(game, request); @@ -907,12 +908,12 @@ public void updateCodeSuccess() { Mockito.doReturn(Collections.singletonList(new Problem())).when(problemService).getProblemsFromDifficulty(Mockito.any(), Mockito.any()); gameService.createAddGameFromRoom(room); Game game = gameService.getGameFromRoomId(TestFields.ROOM_ID); - gameService.updateCode(TestFields.ROOM_ID, TestFields.USER_ID, TestFields.PLAYER_CODE); + gameService.updateCode(TestFields.ROOM_ID, TestFields.USER_ID, TestFields.PLAYER_CODE_1); Player player = game.getPlayers().get(TestFields.USER_ID); // Confirm that the live game service method is called correctly. - verify(liveGameService).updateCode(eq(player), eq(TestFields.PLAYER_CODE)); + verify(liveGameService).updateCode(eq(player), eq(TestFields.PLAYER_CODE_1)); } @Test @@ -927,7 +928,7 @@ public void updateCodeInvalidRoomId() { Mockito.doReturn(Collections.singletonList(new Problem())).when(problemService).getProblemsFromDifficulty(Mockito.any(), Mockito.any()); gameService.createAddGameFromRoom(room); - ApiException exception = assertThrows(ApiException.class, () -> gameService.updateCode("999999", TestFields.USER_ID, TestFields.PLAYER_CODE)); + ApiException exception = assertThrows(ApiException.class, () -> gameService.updateCode("999999", TestFields.USER_ID, TestFields.PLAYER_CODE_1)); assertEquals(GameError.NOT_FOUND, exception.getError()); } @@ -943,7 +944,7 @@ public void updateCodeInvalidUserId() { Mockito.doReturn(Collections.singletonList(new Problem())).when(problemService).getProblemsFromDifficulty(Mockito.any(), Mockito.any()); gameService.createAddGameFromRoom(room); - ApiException exception = assertThrows(ApiException.class, () -> gameService.updateCode(TestFields.ROOM_ID, "999999", TestFields.PLAYER_CODE)); + ApiException exception = assertThrows(ApiException.class, () -> gameService.updateCode(TestFields.ROOM_ID, "999999", TestFields.PLAYER_CODE_1)); assertEquals(GameError.USER_NOT_IN_GAME, exception.getError()); } @@ -964,29 +965,7 @@ public void updateCodeEmptyPlayerCode() { } @Test - public void isGameOverFunctionsCorrectly() { - Game game = new Game(); - game.setGameTimer(new GameTimer(TestFields.DURATION)); - - game.setAllSolved(false); - game.getGameTimer().setTimeUp(false); - assertFalse(gameService.isGameOver(game)); - - game.setAllSolved(true); - game.getGameTimer().setTimeUp(false); - assertTrue(gameService.isGameOver(game)); - - game.setAllSolved(false); - game.getGameTimer().setTimeUp(true); - assertTrue(gameService.isGameOver(game)); - - game.setAllSolved(false); - game.setGameTimer(null); - assertFalse(gameService.isGameOver(game)); - } - - @Test - public void endGameCancelsTimers() { + public void endGameCancelsTimersCreateReport() { Room room = new Room(); room.setRoomId(TestFields.ROOM_ID); room.setDuration(12L); @@ -1007,6 +986,9 @@ public void endGameCancelsTimers() { // Neither the end game nor time left notifications are sent verify(socketService, after(13000).never()).sendSocketUpdate(Mockito.any(String.class), Mockito.any(GameNotificationDto.class)); verify(socketService, never()).sendSocketUpdate(Mockito.any(GameDto.class)); + + // The game report is created upon end game + verify(reportService).createGameReport(eq(game)); } @Test diff --git a/src/test/java/com/codejoust/main/service/LiveGameServiceTests.java b/src/test/java/com/codejoust/main/service/LiveGameServiceTests.java index 0115573a7..0ca542cd4 100644 --- a/src/test/java/com/codejoust/main/service/LiveGameServiceTests.java +++ b/src/test/java/com/codejoust/main/service/LiveGameServiceTests.java @@ -33,8 +33,8 @@ public void updateCodeSuccess() { Game game = GameMapper.fromRoom(room); Player player = game.getPlayers().get(TestFields.USER_ID); - liveGameService.updateCode(player, TestFields.PLAYER_CODE); + liveGameService.updateCode(player, TestFields.PLAYER_CODE_1); - assertEquals(TestFields.PLAYER_CODE, player.getPlayerCode()); + assertEquals(TestFields.PLAYER_CODE_1, player.getPlayerCode()); } } diff --git a/src/test/java/com/codejoust/main/service/ProblemServiceTests.java b/src/test/java/com/codejoust/main/service/ProblemServiceTests.java index 946a78da9..da9717a94 100644 --- a/src/test/java/com/codejoust/main/service/ProblemServiceTests.java +++ b/src/test/java/com/codejoust/main/service/ProblemServiceTests.java @@ -1,6 +1,7 @@ package com.codejoust.main.service; import com.codejoust.main.dao.AccountRepository; +import com.codejoust.main.dao.ProblemContainerRepository; import com.codejoust.main.exception.AccountError; import com.codejoust.main.exception.api.ApiError; import com.codejoust.main.util.TestFields; @@ -20,6 +21,7 @@ import com.codejoust.main.dao.ProblemRepository; import com.codejoust.main.dao.ProblemTagRepository; +import com.codejoust.main.dao.RoomRepository; import com.codejoust.main.dto.problem.CreateProblemRequest; import com.codejoust.main.dto.problem.CreateProblemTagRequest; import com.codejoust.main.dto.problem.CreateTestCaseRequest; @@ -30,7 +32,9 @@ import com.codejoust.main.dto.problem.ProblemTestCaseDto; import com.codejoust.main.exception.ProblemError; import com.codejoust.main.exception.api.ApiException; +import com.codejoust.main.model.Room; import com.codejoust.main.model.problem.Problem; +import com.codejoust.main.model.problem.ProblemContainer; import com.codejoust.main.model.problem.ProblemDifficulty; import com.codejoust.main.model.problem.ProblemIOType; import com.codejoust.main.model.problem.ProblemInput; @@ -57,6 +61,12 @@ public class ProblemServiceTests { @Mock private AccountRepository accountRepository; + @Mock + private ProblemContainerRepository problemContainerRepository; + + @Mock + private RoomRepository roomRepository; + @Mock private FirebaseService firebaseService; @@ -656,6 +666,12 @@ public void editProblemNewProblemInputInvalidatesTestCases() { @Test public void deleteProblemSuccess() { + /** + * 1. Create the problem and mock the return of findProblemByProblemId. + * 2. Create an associated ProblemContainer and mock its return. + * 3. Create an associated Room and mock its return. + * 4. Delete the problem and verify the calls and assertions. + */ Problem problem = new Problem(); problem.setName(TestFields.NAME); problem.setDescription(TestFields.DESCRIPTION); @@ -664,9 +680,22 @@ public void deleteProblemSuccess() { Mockito.doReturn(problem).when(repository).findProblemByProblemId(problem.getProblemId()); + ProblemContainer problemContainer = new ProblemContainer(); + problemContainer.setProblem(problem); + problemContainer.setTestCaseCount(problem.getTestCases().size()); + Mockito.doReturn(Collections.singletonList(problemContainer)).when(problemContainerRepository).findAllByProblem(problem); + problemContainer.setProblem(null); + + Room room = new Room(); + room.addProblem(problem); + Mockito.doReturn(Collections.singletonList(room)).when(roomRepository).findByProblems_ProblemId(problem.getProblemId()); + room.removeProblem(problem); + ProblemDto response = problemService.deleteProblem(problem.getProblemId(), TestFields.TOKEN); verify(repository).delete(problem); + verify(problemContainerRepository).save(problemContainer); + verify(roomRepository).save(room); assertEquals(problem.getName(), response.getName()); assertEquals(problem.getDescription(), response.getDescription()); diff --git a/src/test/java/com/codejoust/main/service/ReportServiceTests.java b/src/test/java/com/codejoust/main/service/ReportServiceTests.java new file mode 100644 index 000000000..afabae4d7 --- /dev/null +++ b/src/test/java/com/codejoust/main/service/ReportServiceTests.java @@ -0,0 +1,283 @@ +package com.codejoust.main.service; + +import com.codejoust.main.util.TestFields; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.Spy; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.stubbing.Answer; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; + +import java.util.ArrayList; +import java.util.List; + +import com.codejoust.main.dao.AccountRepository; +import com.codejoust.main.dao.GameReportRepository; +import com.codejoust.main.dao.ProblemRepository; +import com.codejoust.main.dao.RoomRepository; +import com.codejoust.main.dao.UserRepository; +import com.codejoust.main.dto.game.SubmissionDto; +import com.codejoust.main.dto.game.SubmissionRequest; +import com.codejoust.main.dto.user.UserMapper; +import com.codejoust.main.game_object.Game; +import com.codejoust.main.model.Room; +import com.codejoust.main.model.User; +import com.codejoust.main.model.problem.Problem; +import com.codejoust.main.model.problem.ProblemContainer; +import com.codejoust.main.model.report.GameEndType; +import com.codejoust.main.model.report.GameReport; +import com.codejoust.main.model.report.SubmissionGroupReport; +import com.codejoust.main.util.UtilityTestMethods; + +@ExtendWith(MockitoExtension.class) +public class ReportServiceTests { + + @Mock + private RoomRepository repository; + + @Mock + private GameReportRepository gameReportRepository; + + @Mock + private AccountRepository accountRepository; + + @Mock + private UserRepository userRepository; + + @Mock + private ProblemRepository problemRepository; + + @Mock + private SocketService socketService; + + @Mock + private SubmitService submitService; + + @Mock + private ProblemService problemService; + + @Mock + private NotificationService notificationService; + + @Mock + private LiveGameService liveGameService; + + @Spy + @InjectMocks + private GameManagementService gameManagementService; + + @Spy + @InjectMocks + private ReportService reportService; + + @Test + public void createGameReportMultipleAttributes() { + /** + * Create a game report to test multiple different attributes: + * 1. Logged-in users, logged-out users, players, and spectators. + * 2. Correct and partially correct submissions. + * 3. Multiple problems, with submissions for each. + * + * The test goes through the following steps: + * 1. Create a room with four users: two with same account, one + * anonymous, and one anonymous spectator. + * 2. Add two different problems to the room, and start the game. + * 3. Add submissions: user1 submits correctly to the first problem, + * then a new incorrect submission to the first problem. user2 does + * not submit. user3 submits correctly to the first problem, then + * incorrectly to the second problem. + * 4. Check that the game and relevant account and user objects save. + * 5. Check assertions for the general game report statistics, + * each of the problem containers, and each of the users' submission + * group reports. + */ + + Room room = new Room(); + room.setRoomId(TestFields.ROOM_ID); + room.setDuration(120L); + User user1 = new User(); + user1.setNickname(TestFields.NICKNAME); + user1.setUserId(TestFields.USER_ID); + user1.setAccount(TestFields.account1()); + User user2 = new User(); + user2.setNickname(TestFields.NICKNAME_2); + user2.setUserId(TestFields.USER_ID_2); + user2.setAccount(TestFields.account1()); + User user3 = new User(); + user3.setNickname(TestFields.NICKNAME_3); + user3.setUserId(TestFields.USER_ID_3); + User user4 = new User(); + user4.setNickname(TestFields.NICKNAME_4); + user4.setUserId(TestFields.USER_ID_4); + user4.setSpectator(true); + room.addUser(user1); + room.addUser(user2); + room.addUser(user3); + room.addUser(user4); + room.setHost(user1); + Problem problem1 = TestFields.problem1(); + Problem problem2 = TestFields.problem2(); + + List problems = new ArrayList<>(); + problems.add(problem1); + problems.add(problem2); + room.setProblems(problems); + room.setNumProblems(problems.size()); + + gameManagementService.createAddGameFromRoom(room); + Game game = gameManagementService.getGameFromRoomId(room.getRoomId()); + + // First correct submission for problem 0 with user1. + SubmissionRequest correctSubmission = new SubmissionRequest(); + correctSubmission.setLanguage(TestFields.PYTHON_LANGUAGE); + correctSubmission.setCode(TestFields.PYTHON_CODE); + correctSubmission.setInitiator(UserMapper.toDto(user1)); + Mockito.doAnswer(new Answer() { + public SubmissionDto answer(InvocationOnMock invocation) { + UtilityTestMethods.addSubmissionHelper(game.getPlayers().get(TestFields.USER_ID), 0, TestFields.PLAYER_CODE_1, 1); + return new SubmissionDto(); + }}) + .when(submitService).submitSolution(game, correctSubmission); + gameManagementService.submitSolution(TestFields.ROOM_ID, correctSubmission); + + // Second correct submission for problem 0 with user3. + correctSubmission.setInitiator(UserMapper.toDto(user3)); + Mockito.doAnswer(new Answer() { + public SubmissionDto answer(InvocationOnMock invocation) { + UtilityTestMethods.addSubmissionHelper(game.getPlayers().get(TestFields.USER_ID_3), 0, TestFields.PLAYER_CODE_1, 1); + return new SubmissionDto(); + }}) + .when(submitService).submitSolution(game, correctSubmission); + gameManagementService.submitSolution(TestFields.ROOM_ID, correctSubmission); + + // Incorrect submission for problem 1 with user3. + SubmissionRequest incorrectSubmission = new SubmissionRequest(); + incorrectSubmission.setLanguage(TestFields.PYTHON_LANGUAGE); + incorrectSubmission.setCode(TestFields.PYTHON_CODE); + incorrectSubmission.setInitiator(UserMapper.toDto(user3)); + Mockito.doAnswer(new Answer() { + public SubmissionDto answer(InvocationOnMock invocation) { + UtilityTestMethods.addSubmissionHelper(game.getPlayers().get(TestFields.USER_ID_3), 1, TestFields.PLAYER_CODE_2, 0); + return new SubmissionDto(); + }}) + .when(submitService).submitSolution(game, incorrectSubmission); + gameManagementService.submitSolution(TestFields.ROOM_ID, incorrectSubmission); + + // Incorrect submission for problem 0 with user1. + incorrectSubmission.setInitiator(UserMapper.toDto(user1)); + Mockito.doAnswer(new Answer() { + public SubmissionDto answer(InvocationOnMock invocation) { + UtilityTestMethods.addSubmissionHelper(game.getPlayers().get(TestFields.USER_ID), 0, TestFields.PLAYER_CODE_2, 0); + return new SubmissionDto(); + }}) + .when(submitService).submitSolution(game, incorrectSubmission); + gameManagementService.submitSolution(TestFields.ROOM_ID, incorrectSubmission); + + // Set end game, second problem deleted, and trigger game report. + game.setGameEnded(true); + Mockito.doReturn(problem1).when(problemRepository).findProblemByProblemId(problem1.getProblemId()); + Mockito.doReturn(null).when(problemRepository).findProblemByProblemId(problem2.getProblemId()); + GameReport gameReport = reportService.createGameReport(game); + + // Confirm that the game report, account, and users are saved. + verify(gameReportRepository).save(Mockito.any(GameReport.class)); + verify(accountRepository).save(eq(user1.getAccount())); + + // Check assertions for top-level report variables. + assertEquals(game.getGameTimer().getStartTime(), gameReport.getCreatedDateTime()); + assertEquals(2, gameReport.getNumTestCases()); + assertEquals((double) 2 / 3, gameReport.getAverageProblemsSolved()); + assertEquals((double) 2 / 3, gameReport.getAverageTestCasesPassed()); + assertEquals(GameEndType.MANUAL_END, gameReport.getGameEndType()); + + // Check assertions for individual problem containers. + assertEquals(2, gameReport.getProblemContainers().size()); + ProblemContainer problemContainer1 = gameReport.getProblemContainers().get(0); + assertEquals(problem1, problemContainer1.getProblem()); + assertEquals(1, problemContainer1.getAverageAttemptCount()); + assertEquals((double) 2 / 3, problemContainer1.getAverageTestCasesPassed()); + assertEquals(1, problemContainer1.getTestCaseCount()); + assertEquals(2, problemContainer1.getUserSolvedCount()); + ProblemContainer problemContainer2 = gameReport.getProblemContainers().get(1); + assertNull(problemContainer2.getProblem()); + assertEquals((double) 1 / 3, problemContainer2.getAverageAttemptCount()); + assertEquals(0, problemContainer2.getAverageTestCasesPassed()); + assertEquals(1, problemContainer2.getTestCaseCount()); + assertEquals(0, problemContainer2.getUserSolvedCount()); + + // Check assertions for each user and submission group. + assertEquals(3, gameReport.getUsers().size()); + assertEquals(1, gameReport.getUsers().get(0).getSubmissionGroupReports().size()); + SubmissionGroupReport submissionGroupReport1 = gameReport.getUsers().get(0).getSubmissionGroupReports().get(0); + assertEquals(gameReport.getGameReportId(), submissionGroupReport1.getGameReportId()); + assertEquals(1, submissionGroupReport1.getNumTestCasesPassed()); + assertEquals("10", submissionGroupReport1.getProblemsSolved()); + assertEquals(2, submissionGroupReport1.getSubmissionReports().size()); + + assertEquals(1, gameReport.getUsers().get(1).getSubmissionGroupReports().size()); + SubmissionGroupReport submissionGroupReport2 = gameReport.getUsers().get(1).getSubmissionGroupReports().get(0); + assertEquals(gameReport.getGameReportId(), submissionGroupReport2.getGameReportId()); + assertEquals(0, submissionGroupReport2.getNumTestCasesPassed()); + assertEquals("00", submissionGroupReport2.getProblemsSolved()); + assertEquals(0, submissionGroupReport2.getSubmissionReports().size()); + + assertEquals(1, gameReport.getUsers().get(2).getSubmissionGroupReports().size()); + SubmissionGroupReport submissionGroupReport3 = gameReport.getUsers().get(2).getSubmissionGroupReports().get(0); + assertEquals(gameReport.getGameReportId(), submissionGroupReport3.getGameReportId()); + assertEquals(1, submissionGroupReport3.getNumTestCasesPassed()); + assertEquals("10", submissionGroupReport3.getProblemsSolved()); + assertEquals(2, submissionGroupReport3.getSubmissionReports().size()); + } + + @Test + public void createTwoGameReports() { + /** + * Create two games to verify that two submission group reports are + * made with the same user. + * 1. Create room with one user and one problem. + * 2. Start and immediately end game, manually triggering the create + * game report. + * 3. Start and immediately end game again, and check that user has two + * submission group reports, as well as the all solved game end type. + */ + + Room room = new Room(); + room.setRoomId(TestFields.ROOM_ID); + room.setDuration(120L); + User user1 = new User(); + user1.setNickname(TestFields.NICKNAME); + user1.setUserId(TestFields.USER_ID); + user1.setAccount(TestFields.account1()); + room.addUser(user1); + room.setHost(user1); + Problem problem1 = TestFields.problem1(); + + List problems = new ArrayList<>(); + problems.add(problem1); + room.setProblems(problems); + room.setNumProblems(problems.size()); + + gameManagementService.createAddGameFromRoom(room); + Game game = gameManagementService.getGameFromRoomId(room.getRoomId()); + game.setGameEnded(true); + + Mockito.doReturn(problem1).when(problemRepository).findProblemByProblemId(problem1.getProblemId()); + reportService.createGameReport(game); + + gameManagementService.createAddGameFromRoom(room); + game = gameManagementService.getGameFromRoomId(room.getRoomId()); + game.setAllSolved(true); + GameReport gameReport = reportService.createGameReport(game); + + assertEquals(GameEndType.ALL_SOLVED, gameReport.getGameEndType()); + assertEquals(2, gameReport.getUsers().get(0).getSubmissionGroupReports().size()); + } +} diff --git a/src/test/java/com/codejoust/main/task/EndGameTimerTaskTests.java b/src/test/java/com/codejoust/main/task/EndGameTimerTaskTests.java index 1dcad45d3..e130c9f15 100644 --- a/src/test/java/com/codejoust/main/task/EndGameTimerTaskTests.java +++ b/src/test/java/com/codejoust/main/task/EndGameTimerTaskTests.java @@ -14,12 +14,10 @@ import com.codejoust.main.model.Room; import com.codejoust.main.model.User; import com.codejoust.main.model.problem.ProblemDifficulty; +import com.codejoust.main.service.GameManagementService; import com.codejoust.main.service.SocketService; - -import com.codejoust.main.util.EndGameTimerTask; import com.codejoust.main.util.TestFields; import org.junit.Test; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; @@ -28,17 +26,16 @@ @ExtendWith(MockitoExtension.class) public class EndGameTimerTaskTests { + @Mock + private GameManagementService gameManagementService; + @Mock private SocketService socketService; - @BeforeEach - public void setup() throws Exception { - MockitoAnnotations.initMocks(this); - } - @Test public void endGameTimerTaskSocketMessageNullGame() { - assertThrows(ApiException.class, () -> new EndGameTimerTask(socketService, null)); + MockitoAnnotations.initMocks(this); + assertThrows(ApiException.class, () -> new EndGameTimerTask(gameManagementService, socketService, null)); } @Test @@ -58,11 +55,13 @@ public void endGameTimerTaskSocketMessageNullSocketService() { GameTimer gameTimer = new GameTimer(10L); game.setGameTimer(gameTimer); - assertThrows(ApiException.class, () -> new EndGameTimerTask(null, game)); + MockitoAnnotations.initMocks(this); + + assertThrows(ApiException.class, () -> new EndGameTimerTask(gameManagementService, null, game)); } @Test - public void endGameTimerTaskSocketMessageNullGameTimer() { + public void endGameTimerTaskSocketMessageNullGameManagementService() { User user = new User(); user.setNickname(TestFields.NICKNAME); user.setUserId(TestFields.USER_ID); @@ -75,36 +74,12 @@ public void endGameTimerTaskSocketMessageNullGameTimer() { room.addUser(user); Game game = GameMapper.fromRoom(room); - - assertThrows(ApiException.class, () -> new EndGameTimerTask(socketService, game)); - } - - @Test - public void endGameTimerTaskSocketMessageNullRoom() { - Game game = new Game(); GameTimer gameTimer = new GameTimer(10L); game.setGameTimer(gameTimer); - assertThrows(ApiException.class, () -> new EndGameTimerTask(socketService, game)); - } - - @Test - public void endGameTimerTaskSocketMessageNullRoomId() { - User user = new User(); - user.setNickname(TestFields.NICKNAME); - user.setUserId(TestFields.USER_ID); - user.setSessionId(TestFields.SESSION_ID); - - Room room = new Room(); - room.setDifficulty(ProblemDifficulty.MEDIUM); - room.setHost(user); - room.addUser(user); - - Game game = GameMapper.fromRoom(room); - GameTimer gameTimer = new GameTimer(10L); - game.setGameTimer(gameTimer); + MockitoAnnotations.initMocks(this); - assertThrows(ApiException.class, () -> new EndGameTimerTask(socketService, game)); + assertThrows(ApiException.class, () -> new EndGameTimerTask(null, socketService, game)); } @Test @@ -130,16 +105,19 @@ public void endGameTimerTaskSocketMessage() { MockitoAnnotations.initMocks(this); - EndGameTimerTask endGameTimerTask = new EndGameTimerTask(socketService, game); + EndGameTimerTask endGameTimerTask = new EndGameTimerTask(gameManagementService, socketService, game); gameTimer.getTimer().schedule(endGameTimerTask, 1000L); /** * Confirm that the socket update is not called immediately, - * but is called 1 second later (wait for timer task). + * but is called 1 second later (wait for timer task), + * along with the game management service. */ verify(socketService, never()).sendSocketUpdate(eq(gameDto)); verify(socketService, timeout(1200)).sendSocketUpdate(eq(gameDto)); + + verify(gameManagementService, timeout(1200)).handleEndGame(game); } } diff --git a/src/test/java/com/codejoust/main/task/NotificationTimerTaskTests.java b/src/test/java/com/codejoust/main/task/NotificationTimerTaskTests.java index ad85128ff..f2e7cc95b 100644 --- a/src/test/java/com/codejoust/main/task/NotificationTimerTaskTests.java +++ b/src/test/java/com/codejoust/main/task/NotificationTimerTaskTests.java @@ -13,7 +13,6 @@ import com.codejoust.main.game_object.NotificationType; import com.codejoust.main.service.SocketService; -import com.codejoust.main.util.NotificationTimerTask; import org.junit.Test; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.extension.ExtendWith; diff --git a/src/test/java/com/codejoust/main/util/TestFields.java b/src/test/java/com/codejoust/main/util/TestFields.java index bd33f74c1..ec1d23143 100644 --- a/src/test/java/com/codejoust/main/util/TestFields.java +++ b/src/test/java/com/codejoust/main/util/TestFields.java @@ -30,6 +30,7 @@ public class TestFields { public static final String USER_ID = "012345"; public static final String USER_ID_2 = "678910"; public static final String USER_ID_3 = "024681"; + public static final String USER_ID_4 = "401905"; public static final String SESSION_ID = "abcde"; public static final String SESSION_ID_2 = "fghij"; public static final Integer ID = 1; @@ -65,7 +66,11 @@ public class TestFields { public static final String PYTHON_CODE = "print('hello')"; public static final CodeLanguage PYTHON_LANGUAGE = CodeLanguage.PYTHON; - public static final PlayerCode PLAYER_CODE = new PlayerCode(PYTHON_CODE, PYTHON_LANGUAGE); + public static final PlayerCode PLAYER_CODE_1 = new PlayerCode(PYTHON_CODE, PYTHON_LANGUAGE); + + public static final String JAVA_CODE = "System.out.println(\"hello\");"; + public static final CodeLanguage JAVA_LANGUAGE = CodeLanguage.JAVA; + public static final PlayerCode PLAYER_CODE_2 = new PlayerCode(JAVA_CODE, JAVA_LANGUAGE); public static final Integer NUM_PROBLEMS = 10; diff --git a/src/test/java/com/codejoust/main/util/UtilityTestMethods.java b/src/test/java/com/codejoust/main/util/UtilityTestMethods.java index 4e831b5e5..63bc75270 100644 --- a/src/test/java/com/codejoust/main/util/UtilityTestMethods.java +++ b/src/test/java/com/codejoust/main/util/UtilityTestMethods.java @@ -7,6 +7,10 @@ import com.google.gson.GsonBuilder; import com.google.gson.JsonDeserializer; +import com.codejoust.main.game_object.Player; +import com.codejoust.main.game_object.PlayerCode; +import com.codejoust.main.game_object.Submission; + public class UtilityTestMethods { public static String convertObjectToJsonString(Object o) { @@ -27,4 +31,21 @@ public static T toObjectType(String json, Type type) { Gson gson = new Gson(); return gson.fromJson(json, type); } + + // Helper method to add a dummy submission to a Player object + public static void addSubmissionHelper(Player player, int problemIndex, PlayerCode playerCode, int numCorrect) { + Submission submission = new Submission(); + submission.setProblemIndex(problemIndex); + submission.setNumCorrect(numCorrect); + submission.setNumTestCases(1); + submission.setStartTime(Instant.now()); + submission.setPlayerCode(playerCode); + + player.getSubmissions().add(submission); + if (numCorrect == 1) { + boolean[] solved = player.getSolved(); + solved[problemIndex] = true; + player.setSolved(solved); + } + } } diff --git a/src/test/java/com/codejoust/main/util/UtilityTests.java b/src/test/java/com/codejoust/main/util/UtilityTests.java index 1de62762c..df1693e3b 100644 --- a/src/test/java/com/codejoust/main/util/UtilityTests.java +++ b/src/test/java/com/codejoust/main/util/UtilityTests.java @@ -19,6 +19,8 @@ import com.codejoust.main.dao.RoomRepository; import com.codejoust.main.dao.UserRepository; +import com.codejoust.main.game_object.Game; +import com.codejoust.main.game_object.GameTimer; import com.codejoust.main.model.User; import com.codejoust.main.service.RoomService; import com.codejoust.main.service.UserService; @@ -99,4 +101,26 @@ public void validateIdentifierFalse(String inputName) { public void validateIdentifierTrue(String inputName) { assertTrue(Utility.validateIdentifier(inputName)); } + + @Test + public void isGameOverFunctionsCorrectly() { + Game game = new Game(); + game.setGameTimer(new GameTimer(TestFields.DURATION)); + + game.setAllSolved(false); + game.getGameTimer().setTimeUp(false); + assertFalse(Utility.isGameOver(game)); + + game.setAllSolved(true); + game.getGameTimer().setTimeUp(false); + assertTrue(Utility.isGameOver(game)); + + game.setAllSolved(false); + game.getGameTimer().setTimeUp(true); + assertTrue(Utility.isGameOver(game)); + + game.setAllSolved(false); + game.setGameTimer(null); + assertFalse(Utility.isGameOver(game)); + } }