diff --git a/.github/workflows/wiki.yml b/.github/workflows/wiki.yml index f37031f61e9..57f63fe6fb7 100644 --- a/.github/workflows/wiki.yml +++ b/.github/workflows/wiki.yml @@ -1,20 +1,45 @@ name: Deploy to Wiki on: + pull_request: + paths: + - 'wiki/**' push: branches: - develop paths: - 'wiki/**' - # Triggers this workflow when the wiki is changed + # Triggers this workflow when the wiki is changed. # (see https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#gollum). gollum: +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: true + jobs: + table_of_contents_check: + # To verify that the wiki's table of contents matches the headers accurately. + name: Check Wiki Table of Contents + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v2 + + - name: Set up Bazel + uses: abhinavsingh/setup-bazel@v3 + with: + version: 6.5.0 + + - name: Check Wiki Table of Contents + id: checkWikiToc + run: | + bazel run //scripts:wiki_table_of_contents_check -- ${GITHUB_WORKSPACE} + wiki-deploy: runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-20.04] + if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/develop' }} steps: - uses: actions/checkout@v3 with: diff --git a/scripts/BUILD.bazel b/scripts/BUILD.bazel index 6ef9a5d6739..9577dfe834b 100644 --- a/scripts/BUILD.bazel +++ b/scripts/BUILD.bazel @@ -237,6 +237,15 @@ kt_jvm_binary( ], ) +kt_jvm_binary( + name = "wiki_table_of_contents_check", + testonly = True, + main_class = "org.oppia.android.scripts.wiki.WikiTableOfContentsCheckKt", + runtime_deps = [ + "//scripts/src/java/org/oppia/android/scripts/wiki:wiki_table_of_contents_check_lib", + ], +) + kt_jvm_binary( name = "run_coverage", testonly = True, diff --git a/scripts/src/java/org/oppia/android/scripts/wiki/BUILD.bazel b/scripts/src/java/org/oppia/android/scripts/wiki/BUILD.bazel new file mode 100644 index 00000000000..6898d1b8c21 --- /dev/null +++ b/scripts/src/java/org/oppia/android/scripts/wiki/BUILD.bazel @@ -0,0 +1,18 @@ +""" +Libraries corresponding to scripting tools that help with continuous integration workflows. +""" + +load("@io_bazel_rules_kotlin//kotlin:kotlin.bzl", "kt_jvm_library") + +kt_jvm_library( + name = "wiki_table_of_contents_check_lib", + testonly = True, + srcs = [ + "WikiTableOfContentsCheck.kt", + ], + visibility = ["//scripts:oppia_script_binary_visibility"], + deps = [ + "//scripts/src/java/org/oppia/android/scripts/common:bazel_client", + "//scripts/src/java/org/oppia/android/scripts/common:git_client", + ], +) diff --git a/scripts/src/java/org/oppia/android/scripts/wiki/WikiTableOfContentsCheck.kt b/scripts/src/java/org/oppia/android/scripts/wiki/WikiTableOfContentsCheck.kt new file mode 100644 index 00000000000..2f5a0ae3862 --- /dev/null +++ b/scripts/src/java/org/oppia/android/scripts/wiki/WikiTableOfContentsCheck.kt @@ -0,0 +1,82 @@ +package org.oppia.android.scripts.wiki + +import java.io.File + +/** + * Script for ensuring that the table of contents in each wiki page matches with its respective headers. + * + * Usage: + * bazel run //scripts:wiki_table_of_contents_check -- + * + * Arguments: + * - path_to_default_working_directory: The default working directory on the runner for steps, and the default location of repository. + * + * Example: + * bazel run //scripts:wiki_table_of_contents_check -- $(pwd) + */ +fun main(vararg args: String) { + // Path to the repo's wiki. + val wikiDirPath = "${args[0]}/wiki/" + val wikiDir = File(wikiDirPath) + + // Check if the wiki directory exists. + if (wikiDir.exists() && wikiDir.isDirectory) { + processWikiDirectory(wikiDir) + println("WIKI TABLE OF CONTENTS CHECK PASSED") + } else { + println("No contents found in the Wiki directory.") + } +} + +private fun processWikiDirectory(wikiDir: File) { + wikiDir.listFiles()?.forEach { file -> + checkTableOfContents(file) + } +} + +private fun checkTableOfContents(file: File) { + val fileContents = file.readLines() + val tocStartIdx = fileContents.indexOfFirst { + it.contains(Regex("""##\s+Table\s+of\s+Contents""", RegexOption.IGNORE_CASE)) + } + if (tocStartIdx == -1) { + return + } + + // Skipping the blank line after the ## Table of Contents + val tocEndIdx = fileContents.subList(tocStartIdx + 2, fileContents.size).indexOfFirst { + it.startsWith("#") + }.takeIf { it != -1 } + ?: error("Wiki doesn't contain headers referenced in Table of Contents.") + + val tocSpecificLines = fileContents.subList(tocStartIdx, tocStartIdx + tocEndIdx + 1) + + for (line in tocSpecificLines) { + if (line.trimStart().startsWith("- [") && !line.contains("https://")) { + validateTableOfContents(file, line) + } + } +} + +private fun validateTableOfContents(file: File, line: String) { + val titleRegex = "\\[(.*?)\\]".toRegex() + val title = titleRegex.find(line)?.groupValues?.get(1)?.replace('-', ' ') + ?.replace(Regex("[?&./:’'*!,(){}\\[\\]+]"), "") + ?.trim() + + val linkRegex = "\\(#(.*?)\\)".toRegex() + val link = linkRegex.find(line)?.groupValues?.get(1)?.removePrefix("#")?.replace('-', ' ') + ?.replace(Regex("[?&./:’'*!,(){}\\[\\]+]"), "") + ?.trim() + + // Checks if the table of content title matches with the header link text. + val matches = title.equals(link, ignoreCase = true) + if (!matches) { + error( + "\nWIKI TABLE OF CONTENTS CHECK FAILED" + + "\nMismatch of Table of Content with headers in the File: ${file.name}. " + + "\nThe Title: '${titleRegex.find(line)?.groupValues?.get(1)}' " + + "doesn't match with its corresponding Link: '${linkRegex.find(line)?.groupValues?.get(1)}'." + ) + } +} diff --git a/scripts/src/javatests/org/oppia/android/scripts/wiki/BUILD.bazel b/scripts/src/javatests/org/oppia/android/scripts/wiki/BUILD.bazel new file mode 100644 index 00000000000..953b3f7d8d9 --- /dev/null +++ b/scripts/src/javatests/org/oppia/android/scripts/wiki/BUILD.bazel @@ -0,0 +1,16 @@ +""" +Tests corresponding to wiki-related checks. +""" + +load("@io_bazel_rules_kotlin//kotlin:jvm.bzl", "kt_jvm_test") + +kt_jvm_test( + name = "WikiTableOfContentsCheckTest", + srcs = ["WikiTableOfContentsCheckTest.kt"], + deps = [ + "//scripts/src/java/org/oppia/android/scripts/wiki:wiki_table_of_contents_check_lib", + "//testing:assertion_helpers", + "//third_party:com_google_truth_truth", + "//third_party:org_jetbrains_kotlin_kotlin-test-junit", + ], +) diff --git a/scripts/src/javatests/org/oppia/android/scripts/wiki/WikiTableOfContentsCheckTest.kt b/scripts/src/javatests/org/oppia/android/scripts/wiki/WikiTableOfContentsCheckTest.kt new file mode 100644 index 00000000000..d90593dfa2e --- /dev/null +++ b/scripts/src/javatests/org/oppia/android/scripts/wiki/WikiTableOfContentsCheckTest.kt @@ -0,0 +1,190 @@ +package org.oppia.android.scripts.wiki + +import com.google.common.truth.Truth.assertThat +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import org.oppia.android.testing.assertThrows +import java.io.ByteArrayOutputStream +import java.io.PrintStream + +/** Tests for [WikiTableOfContentsCheck]. */ +class WikiTableOfContentsCheckTest { + private val outContent: ByteArrayOutputStream = ByteArrayOutputStream() + private val originalOut: PrintStream = System.out + private val WIKI_TOC_CHECK_PASSED_OUTPUT_INDICATOR = "WIKI TABLE OF CONTENTS CHECK PASSED" + private val WIKI_TOC_CHECK_FAILED_OUTPUT_INDICATOR = "WIKI TABLE OF CONTENTS CHECK FAILED" + + @field:[Rule JvmField] val tempFolder = TemporaryFolder() + + @Before + fun setUp() { + System.setOut(PrintStream(outContent)) + } + + @After + fun tearDown() { + System.setOut(originalOut) + } + + @Test + fun testWikiTOCCheck_noWikiDirExists_printsNoContentFound() { + runScript() + assertThat(outContent.toString().trim()).isEqualTo("No contents found in the Wiki directory.") + } + + @Test + fun testWikiTOCCheck_noWikiDirectory_printsNoContentFound() { + tempFolder.newFile("wiki") + runScript() + assertThat(outContent.toString().trim()).isEqualTo("No contents found in the Wiki directory.") + } + + @Test + fun testWikiTOCCheck_validWikiTOC_checkPass() { + tempFolder.newFolder("wiki") + val file = tempFolder.newFile("wiki/wiki.md") + file.writeText( + """ + ## Table of Contents + + - [Introduction](#introduction) + - [Usage](#usage) + + ## Introduction + Content + + ## Usage + Content + """.trimIndent() + ) + + runScript() + + assertThat(outContent.toString().trim()).contains(WIKI_TOC_CHECK_PASSED_OUTPUT_INDICATOR) + } + + @Test + fun testWikiTOCCheck_missingWikiTOC_returnsNoTOCFound() { + tempFolder.newFolder("wiki") + val file = tempFolder.newFile("wiki/wiki.md") + file.writeText( + """ + - [Introduction](#introduction) + - [Usage](#usage) + + ## Introduction + Content + + ## Usage + Content + """.trimIndent() + ) + + runScript() + + assertThat(outContent.toString().trim()).contains(WIKI_TOC_CHECK_PASSED_OUTPUT_INDICATOR) + } + + @Test + fun testWikiTOCCheck_wikiTOCReference_noHeadersFound_throwsException() { + tempFolder.newFolder("wiki") + val file = tempFolder.newFile("wiki/wiki.md") + file.writeText( + """ + ## Table of Contents + + - [Introduction](#introductions) + + """.trimIndent() + ) + + val exception = assertThrows() { + runScript() + } + + assertThat(exception).hasMessageThat().contains( + "Wiki doesn't contain headers referenced in Table of Contents." + ) + } + + @Test + fun testWikiTOCCheck_mismatchWikiTOC_checkFail() { + tempFolder.newFolder("wiki") + val file = tempFolder.newFile("wiki/wiki.md") + file.writeText( + """ + ## Table of Contents + + - [Introduction](#introductions) + - [Usage](#usage) + + ## Introduction + Content + + ## Usage + Content + """.trimIndent() + ) + + val exception = assertThrows() { + runScript() + } + + assertThat(exception).hasMessageThat().contains(WIKI_TOC_CHECK_FAILED_OUTPUT_INDICATOR) + } + + @Test + fun testWikiTOCCheck_validWikiTOCWithSeparator_checkPass() { + tempFolder.newFolder("wiki") + val file = tempFolder.newFile("wiki/wiki.md") + file.writeText( + """ + ## Table of Contents + + - [Introduction To Wiki](#introduction-to-wiki) + - [Usage Wiki-Content](#usage-wiki-content) + + ## Introduction + Content + + ## Usage + Content + """.trimIndent() + ) + + runScript() + + assertThat(outContent.toString().trim()).contains(WIKI_TOC_CHECK_PASSED_OUTPUT_INDICATOR) + } + + @Test + fun testWikiTOCCheck_validWikiTOCWithSpecialCharacter_checkPass() { + tempFolder.newFolder("wiki") + val file = tempFolder.newFile("wiki/wiki.md") + file.writeText( + """ + ## Table of Contents + + - [Introduction](#introduction?) + - [Usage?](#usage) + + ## Introduction + Content + + ## Usage + Content + """.trimIndent() + ) + + runScript() + + assertThat(outContent.toString().trim()).contains(WIKI_TOC_CHECK_PASSED_OUTPUT_INDICATOR) + } + + private fun runScript() { + main(tempFolder.root.absolutePath) + } +} diff --git a/wiki/Guidance-on-submitting-a-PR.md b/wiki/Guidance-on-submitting-a-PR.md index 0124b44eb90..37b731e6f64 100644 --- a/wiki/Guidance-on-submitting-a-PR.md +++ b/wiki/Guidance-on-submitting-a-PR.md @@ -22,7 +22,7 @@ Note: If your change involves more than around 500 lines of code, we recommend f - [Tips for getting your PR submitted](#tips-for-getting-your-pr-submitted) - [Appendix: Resolving merge conflicts using the terminal](#appendix-resolving-merge-conflicts-using-the-terminal) - [Appendix: Resolving merge conflicts using Android Studio](#appendix-resolving-merge-conflicts-using-android-studio) -- [Step 4: Tidy up and celebrate!](#step-4-tidy-up-and-celebrate-confetti_ball) +- [Step 4: Tidy up and celebrate! :confetti_ball:](#step-4-tidy-up-and-celebrate-confetti_ball) ## Step 1: Making a local code change diff --git a/wiki/Installing-Oppia-Android.md b/wiki/Installing-Oppia-Android.md index ffc19cba8bd..dcf0a4baafb 100644 --- a/wiki/Installing-Oppia-Android.md +++ b/wiki/Installing-Oppia-Android.md @@ -7,7 +7,7 @@ This wiki page explains how to install Oppia Android on your local machine. If y - [Prepare developer environment](#prepare-developer-environment) - [Install oppia-android](#install-oppia-android) - [Run the app from Android Studio](#run-the-app-from-android-studio) -- [Run the tests](#set-up-and-run-tests) +- [Set up and Run tests](#set-up-and-run-tests) - [Step-by-Step guidance for setting up and running app modules robolectric test](#step-by-step-guidance-for-setting-up-and-running-app-modules-robolectric-test) - [For tests that are in non-app modules, such as **domain** or **utility**:](#for-tests-that-are-in-non-app-modules-such-as-domain-or-utility) diff --git a/wiki/Interpreting-CI-Results.md b/wiki/Interpreting-CI-Results.md index 2adf2685b53..00b3c5667c7 100644 --- a/wiki/Interpreting-CI-Results.md +++ b/wiki/Interpreting-CI-Results.md @@ -1,6 +1,6 @@ ## Table of Contents -- [How to find the error message for a Failing CI check](#how-to-find-error-message-for-failing-ci-checks) +- [How to find error message for Failing CI checks](#how-to-find-error-message-for-failing-ci-checks) - [Developer Video - Understanding CI check failures](#developer-video---understanding-ci-check-failures) ## How to find error message for Failing CI checks diff --git a/wiki/Oppia-Android-Code-Coverage.md b/wiki/Oppia-Android-Code-Coverage.md index 499b3591642..0d294887e8f 100644 --- a/wiki/Oppia-Android-Code-Coverage.md +++ b/wiki/Oppia-Android-Code-Coverage.md @@ -4,10 +4,10 @@ - [Understanding Code Coverage](#understanding-code-coverage) - [Why is Code Coverage Important?](#why-is-code-coverage-important) - [How to use the code coverage tool?](#how-to-use-the-code-coverage-tool) - - [Continuous Itegration Checks on Pull Request](#1-continuous-integration-checks-on-pull-requests) - - [Understanding the CI Coverage Report](#11-understanding-the-ci-coverage-report) - - [Local Command Line Interface Tools](#2-local-command-line-interface-cli-tools) - - [Understanding the Html Reports](#21-understanding-the-ci-coverage-report) + - [1. Continuous Integration Checks on Pull Requests](#1-continuous-integration-checks-on-pull-requests) + - [1.1 Understanding the CI Coverage Report](#11-understanding-the-ci-coverage-report) + - [2. Local Command Line Interface (CLI) Tools](#2-local-command-line-interface-cli-tools) + - [2.1 Understanding the CI Coverage Report](#21-understanding-the-ci-coverage-report) - [Increasing Code Coverage Metrics](#increasing-code-coverage-metrics) - [Unit-Centric Coverage Philosophy](#unit-centric-coverage-philosophy) - [Limitations of the code coverage tool](#limitations-of-the-code-coverage-tool) diff --git a/wiki/Writing-tests-with-good-behavioral-coverage.md b/wiki/Writing-tests-with-good-behavioral-coverage.md index db50cfa83a6..93b94ac7375 100644 --- a/wiki/Writing-tests-with-good-behavioral-coverage.md +++ b/wiki/Writing-tests-with-good-behavioral-coverage.md @@ -3,18 +3,18 @@ - [What is Behavioral Coverage?](#what-is-behavioral-coverage) - [Understanding Behavioral Coverage and its Importance](#understanding-behavioral-coverage-and-its-importance) - [Writing Effective Tests](#writing-effective-tests) - - [Understand the Requirements](#1-understanding-the-requirements) - - [Writing Clear and Descriptive Test Cases](#2-writing-clear-and-descriptive-test-cases) - - [Focusing on Specific Test Cases](#3-focusing-on-specific-test-cases) - - [Covering Different Scenarios](#4-covering-different-scenarios) - - [Covering All Branches, Paths, and Conditions](#5-covering-all-branches-paths-and-conditions) - - [Exception and Error Handling](#6-exception-and-error-handling) - - [Absence of Unwanted Output](#7-absence-of-unwanted-output) + - [1. Understanding the Requirements](#1-understanding-the-requirements) + - [2. Writing Clear and Descriptive Test Cases](#2-writing-clear-and-descriptive-test-cases) + - [3. Focusing on Specific Test Cases](#3-focusing-on-specific-test-cases) + - [4. Covering Different Scenarios](#4-covering-different-scenarios) + - [5. Covering All Branches, Paths, and Conditions](#5-covering-all-branches-paths-and-conditions) + - [6. Exception and Error Handling](#6-exception-and-error-handling) + - [7. Absence of Unwanted Output](#7-absence-of-unwanted-output) - [Testing Public APIs](#testing-public-apis) - [Structuring Test Bodies](#structuring-test-bodies) - - [When and How to Divide Responsibilities](#1-when-and-how-to-divide-responsibilities) - - [When Not to Divide Responsibilities](#2-when-not-to-divide-responsibilities) - - [Importance of Descriptive Test Names](#3-importance-of-descriptive-test-names) + - [1. When and How to Divide Responsibilities](#1-when-and-how-to-divide-responsibilities) + - [2. When Not to Divide Responsibilities](#2-when-not-to-divide-responsibilities) + - [3. Importance of Descriptive Test Names](#3-importance-of-descriptive-test-names) - [How to Map a Line of Code to Its Corresponding Behaviors?](#how-to-map-a-line-of-code-to-its-corresponding-behaviors) # What is Behavioral Coverage?