commit d3c18e8912b5c36b3770f1c2c28c63a40381a07b Author: feie9456 Date: Mon Apr 13 07:16:39 2026 +0800 chore: initialize repository with gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4a166e5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,32 @@ +# OS files +.DS_Store +Thumbs.db + +# IDE/editor +.idea/ +*.iml +.vscode/ + +# Logs +*.log + +# Gradle +.gradle/ +**/.gradle/ +build/ +**/build/ +out/ + +# Java/Kotlin compiled files +*.class + +# Android local and generated files +local.properties +**/local.properties +captures/ +.externalNativeBuild/ +.cxx/ + +# Temporary files +tmp/ +**/tmp/ diff --git a/hw1/.gitattributes b/hw1/.gitattributes new file mode 100644 index 0000000..f91f646 --- /dev/null +++ b/hw1/.gitattributes @@ -0,0 +1,12 @@ +# +# https://help.github.com/articles/dealing-with-line-endings/ +# +# Linux start script should use lf +/gradlew text eol=lf + +# These are Windows script files and should use crlf +*.bat text eol=crlf + +# Binary files should be left untouched +*.jar binary + diff --git a/hw1/.gitignore b/hw1/.gitignore new file mode 100644 index 0000000..1b6985c --- /dev/null +++ b/hw1/.gitignore @@ -0,0 +1,5 @@ +# Ignore Gradle project-specific cache directory +.gradle + +# Ignore Gradle build output directory +build diff --git a/hw1/app/build.gradle b/hw1/app/build.gradle new file mode 100644 index 0000000..211bacc --- /dev/null +++ b/hw1/app/build.gradle @@ -0,0 +1,29 @@ +plugins { + id 'application' +} + +repositories { + mavenCentral() +} + +dependencies { + testImplementation libs.junit.jupiter + + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + + implementation libs.guava +} + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } +} + +application { + mainClass = 'triangle.App' +} + +tasks.named('test') { + useJUnitPlatform() +} diff --git a/hw1/app/src/main/java/triangle/App.java b/hw1/app/src/main/java/triangle/App.java new file mode 100644 index 0000000..fab7aa8 --- /dev/null +++ b/hw1/app/src/main/java/triangle/App.java @@ -0,0 +1,50 @@ +package triangle; + +public class App { + + public enum TriangleType { + EQUILATERAL, // 等边三角形 + ISOSCELES, // 等腰三角形 + SCALENE, // 一般三角形 + NOT_A_TRIANGLE // 非三角形 + } + + public static TriangleType classify(int a, int b, int c) { + // 检查边长是否为正整数 + if (a <= 0 || b <= 0 || c <= 0) { + return TriangleType.NOT_A_TRIANGLE; + } + + // 检查三角不等式(任意两边之和大于第三边) + // 使用 long 防止整数溢出 + if ((long) a + b <= c || (long) a + c <= b || (long) b + c <= a) { + return TriangleType.NOT_A_TRIANGLE; + } + + // 判断三角形类型 + if (a == b && b == c) { + return TriangleType.EQUILATERAL; + } else if (a == b || b == c || a == c) { + return TriangleType.ISOSCELES; + } else { + return TriangleType.SCALENE; + } + } + + public static void main(String[] args) { + if (args.length != 3) { + System.out.println("用法: java triangle.App <边1> <边2> <边3>"); + return; + } + int a = Integer.parseInt(args[0]); + int b = Integer.parseInt(args[1]); + int c = Integer.parseInt(args[2]); + TriangleType result = classify(a, b, c); + switch (result) { + case EQUILATERAL -> System.out.println("等边三角形"); + case ISOSCELES -> System.out.println("等腰三角形"); + case SCALENE -> System.out.println("一般三角形"); + case NOT_A_TRIANGLE -> System.out.println("非三角形"); + } + } +} diff --git a/hw1/app/src/test/java/triangle/AppTest.java b/hw1/app/src/test/java/triangle/AppTest.java new file mode 100644 index 0000000..3614931 --- /dev/null +++ b/hw1/app/src/test/java/triangle/AppTest.java @@ -0,0 +1,208 @@ +package triangle; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.DisplayName; +import static org.junit.jupiter.api.Assertions.*; +import triangle.App.TriangleType; + +class AppTest { + + // ======================================================================== + // 等价类划分 + // ======================================================================== + // + // 输入条件: 三个整数 a, b, c 作为三角形的三条边 + // + // 有效等价类: + // EC1: 等边三角形 (a=b=c, 且 a>0) + // EC2: 等腰三角形 (恰好两边相等, 且满足三角不等式) + // EC3: 一般三角形 (三边互不相等, 且满足三角不等式) + // + // 无效等价类: + // EC4: 某边为 0 + // EC5: 某边为负数 + // EC6: 两边之和等于第三边 (退化三角形) + // EC7: 两边之和小于第三边 (不满足三角不等式) + // EC8: 整数溢出边界 (极大值) + // + // ======================================================================== + + // --- 有效等价类测试 --- + + @Test + @DisplayName("EC1: 等边三角形 - 三边相等") + void testEquilateral() { + assertEquals(TriangleType.EQUILATERAL, App.classify(5, 5, 5)); + } + + @Test + @DisplayName("EC2a: 等腰三角形 - a=b≠c") + void testIsoscelesABEqual() { + assertEquals(TriangleType.ISOSCELES, App.classify(5, 5, 3)); + } + + @Test + @DisplayName("EC2b: 等腰三角形 - a=c≠b") + void testIsoscelesACEqual() { + assertEquals(TriangleType.ISOSCELES, App.classify(5, 3, 5)); + } + + @Test + @DisplayName("EC2c: 等腰三角形 - b=c≠a") + void testIsoscelesBCEqual() { + assertEquals(TriangleType.ISOSCELES, App.classify(3, 5, 5)); + } + + @Test + @DisplayName("EC3: 一般三角形 - 三边互不相等") + void testScalene() { + assertEquals(TriangleType.SCALENE, App.classify(3, 4, 5)); + } + + // --- 无效等价类测试 --- + + @Test + @DisplayName("EC4a: 第一边为0") + void testZeroSideA() { + assertEquals(TriangleType.NOT_A_TRIANGLE, App.classify(0, 5, 5)); + } + + @Test + @DisplayName("EC4b: 第二边为0") + void testZeroSideB() { + assertEquals(TriangleType.NOT_A_TRIANGLE, App.classify(5, 0, 5)); + } + + @Test + @DisplayName("EC4c: 第三边为0") + void testZeroSideC() { + assertEquals(TriangleType.NOT_A_TRIANGLE, App.classify(5, 5, 0)); + } + + @Test + @DisplayName("EC5a: 第一边为负数") + void testNegativeSideA() { + assertEquals(TriangleType.NOT_A_TRIANGLE, App.classify(-1, 5, 5)); + } + + @Test + @DisplayName("EC5b: 第二边为负数") + void testNegativeSideB() { + assertEquals(TriangleType.NOT_A_TRIANGLE, App.classify(5, -1, 5)); + } + + @Test + @DisplayName("EC5c: 第三边为负数") + void testNegativeSideC() { + assertEquals(TriangleType.NOT_A_TRIANGLE, App.classify(5, 5, -1)); + } + + @Test + @DisplayName("EC6a: a+b=c 退化三角形") + void testDegenerateABEqualC() { + assertEquals(TriangleType.NOT_A_TRIANGLE, App.classify(1, 2, 3)); + } + + @Test + @DisplayName("EC6b: a+c=b 退化三角形") + void testDegenerateACEqualB() { + assertEquals(TriangleType.NOT_A_TRIANGLE, App.classify(1, 3, 2)); + } + + @Test + @DisplayName("EC6c: b+c=a 退化三角形") + void testDegenerateBCEqualA() { + assertEquals(TriangleType.NOT_A_TRIANGLE, App.classify(3, 1, 2)); + } + + @Test + @DisplayName("EC7a: a+b 退化, 非三角形") + void testMinIsoscelesDegenerate() { + assertEquals(TriangleType.NOT_A_TRIANGLE, App.classify(1, 1, 2)); + } + + @Test + @DisplayName("BV3: 等腰三角形刚好满足不等式 (2,2,3)") + void testIsoscelesJustValid() { + assertEquals(TriangleType.ISOSCELES, App.classify(2, 2, 3)); + } + + @Test + @DisplayName("BV4: 一般三角形刚好满足不等式 (2,3,4)") + void testScaleneJustValid() { + assertEquals(TriangleType.SCALENE, App.classify(2, 3, 4)); + } + + @Test + @DisplayName("BV5: 一般三角形不等式临界 a+b=c+1 (3,4,6)") + void testScaleneBoundary() { + assertEquals(TriangleType.SCALENE, App.classify(3, 4, 6)); + } + + @Test + @DisplayName("BV6: 大等边三角形 (MAX,MAX,MAX)") + void testLargeEquilateral() { + assertEquals(TriangleType.EQUILATERAL, App.classify(Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE)); + } + + @Test + @DisplayName("BV7: 三边全为0") + void testAllZero() { + assertEquals(TriangleType.NOT_A_TRIANGLE, App.classify(0, 0, 0)); + } + + @Test + @DisplayName("BV8: 三边全为负数") + void testAllNegative() { + assertEquals(TriangleType.NOT_A_TRIANGLE, App.classify(-1, -1, -1)); + } + + @Test + @DisplayName("BV9: 边长为1的一般三角形边界 (1,2,2) 等腰") + void testSmallIsosceles() { + assertEquals(TriangleType.ISOSCELES, App.classify(1, 2, 2)); + } + + @Test + @DisplayName("BV10: 大值等腰三角形溢出检测") + void testLargeIsoscelesOverflow() { + assertEquals(TriangleType.ISOSCELES, App.classify(Integer.MAX_VALUE, Integer.MAX_VALUE, 1)); + } +} diff --git a/hw1/gradle.properties b/hw1/gradle.properties new file mode 100644 index 0000000..1385fa5 --- /dev/null +++ b/hw1/gradle.properties @@ -0,0 +1,2 @@ +org.gradle.configuration-cache=true + diff --git a/hw1/gradle/libs.versions.toml b/hw1/gradle/libs.versions.toml new file mode 100644 index 0000000..0fa76cc --- /dev/null +++ b/hw1/gradle/libs.versions.toml @@ -0,0 +1,7 @@ +[versions] +guava = "33.4.6-jre" +junit-jupiter = "5.12.1" + +[libraries] +guava = { module = "com.google.guava:guava", version.ref = "guava" } +junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit-jupiter" } diff --git a/hw1/gradle/wrapper/gradle-wrapper.jar b/hw1/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..8bdaf60 Binary files /dev/null and b/hw1/gradle/wrapper/gradle-wrapper.jar differ diff --git a/hw1/gradle/wrapper/gradle-wrapper.properties b/hw1/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..2a84e18 --- /dev/null +++ b/hw1/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.0.0-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/hw1/gradlew b/hw1/gradlew new file mode 100644 index 0000000..ef07e01 --- /dev/null +++ b/hw1/gradlew @@ -0,0 +1,251 @@ +#!/bin/sh + +# +# Copyright © 2015 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH="\\\"\\\"" + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/hw1/gradlew.bat b/hw1/gradlew.bat new file mode 100644 index 0000000..db3a6ac --- /dev/null +++ b/hw1/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH= + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/hw1/settings.gradle b/hw1/settings.gradle new file mode 100644 index 0000000..4921788 --- /dev/null +++ b/hw1/settings.gradle @@ -0,0 +1,6 @@ +plugins { + id 'org.gradle.toolchains.foojay-resolver-convention' version '1.0.0' +} + +rootProject.name = 'triangle' +include('app') diff --git a/hw2/app/build.gradle b/hw2/app/build.gradle new file mode 100644 index 0000000..19e2004 --- /dev/null +++ b/hw2/app/build.gradle @@ -0,0 +1,29 @@ +plugins { + id 'application' +} + +repositories { + mavenCentral() +} + +dependencies { + testImplementation libs.junit.jupiter + + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + + implementation libs.guava +} + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } +} + +application { + mainClass = 'login.LoginValidator' +} + +tasks.named('test') { + useJUnitPlatform() +} diff --git a/hw2/app/src/main/java/login/LoginValidator.java b/hw2/app/src/main/java/login/LoginValidator.java new file mode 100644 index 0000000..0a51aad --- /dev/null +++ b/hw2/app/src/main/java/login/LoginValidator.java @@ -0,0 +1,57 @@ +package login; + +import java.util.ArrayList; +import java.util.List; + +public class LoginValidator { + + public static List validate(String account, String password) { + List messages = new ArrayList<>(); + + boolean accountValid = isAccountValid(account); + boolean passwordValid = isPasswordValid(password); + + if (accountValid && passwordValid) { + messages.add("输入合法"); + } else { + if (!accountValid) { + messages.add("账号不合法"); + } + if (!passwordValid) { + messages.add("密码不合法"); + } + } + + return messages; + } + + /** + * 账号合法:6-10位自然数(纯数字,不含负号/小数点,长度6~10) + */ + static boolean isAccountValid(String account) { + if (account == null) return false; + int len = account.length(); + if (len < 6 || len > 10) return false; + for (char c : account.toCharArray()) { + if (c < '0' || c > '9') return false; + } + return true; + } + + /** + * 密码合法:恰好8位字符串(非null,长度==8) + */ + static boolean isPasswordValid(String password) { + if (password == null) return false; + return password.length() == 8; + } + + public static void main(String[] args) { + if (args.length != 2) { + System.out.println("用法: java login.LoginValidator <账号> <密码>"); + return; + } + List result = validate(args[0], args[1]); + result.forEach(System.out::println); + } +} diff --git a/hw2/app/src/test/java/login/LoginValidatorTest.java b/hw2/app/src/test/java/login/LoginValidatorTest.java new file mode 100644 index 0000000..d1928d2 --- /dev/null +++ b/hw2/app/src/test/java/login/LoginValidatorTest.java @@ -0,0 +1,212 @@ +package login; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.DisplayName; +import static org.junit.jupiter.api.Assertions.*; +import java.util.List; + +class LoginValidatorTest { + + // ======================================================================== + // 因果图分析 + // ======================================================================== + // + // 原因 (Causes): + // C1: 账号长度 >= 6 + // C2: 账号长度 <= 10 + // C3: 账号为纯数字(自然数) + // C4: 密码长度 = 8 + // + // 中间节点: + // M1 = C1 ∧ C2 ∧ C3 (账号合法) + // + // 结果 (Effects): + // E1: 显示"输入合法" ← M1 ∧ C4 + // E2: 显示"账号不合法" ← ¬M1 + // E3: 显示"密码不合法" ← ¬C4 + // + // ======================================================================== + // 判定表简化后的 4 条规则 → 测试用例 + // ======================================================================== + // + // | 规则 | 账号合法(M1) | 密码合法(C4) | 预期结果 | + // |------|-------------|-------------|------------------------| + // | R1 | T | T | 输入合法 | + // | R2 | T | F | 密码不合法 | + // | R3 | F | T | 账号不合法 | + // | R4 | F | F | 账号不合法 + 密码不合法 | + // + + // --- 规则 R1: 账号合法 ∧ 密码合法 → 输入合法 --- + + @Test + @DisplayName("R1: 账号合法(6位数字) + 密码合法(8位) → 输入合法") + void testR1_validAccountAndPassword() { + List result = LoginValidator.validate("123456", "abcd1234"); + assertEquals(List.of("输入合法"), result); + } + + @Test + @DisplayName("R1 变体: 账号10位数字 + 密码8位 → 输入合法") + void testR1_account10Digits() { + List result = LoginValidator.validate("1234567890", "pass$12#"); + assertEquals(List.of("输入合法"), result); + } + + @Test + @DisplayName("R1 变体: 账号8位数字(中间值) + 密码8位 → 输入合法") + void testR1_accountMiddleLength() { + List result = LoginValidator.validate("12345678", "Pa55w0rd"); + assertEquals(List.of("输入合法"), result); + } + + // --- 规则 R2: 账号合法 ∧ 密码不合法 → 密码不合法 --- + + @Test + @DisplayName("R2: 账号合法 + 密码过短(7位) → 密码不合法") + void testR2_passwordTooShort() { + List result = LoginValidator.validate("123456", "abcd123"); + assertEquals(List.of("密码不合法"), result); + } + + @Test + @DisplayName("R2 变体: 账号合法 + 密码过长(9位) → 密码不合法") + void testR2_passwordTooLong() { + List result = LoginValidator.validate("123456", "abcd12345"); + assertEquals(List.of("密码不合法"), result); + } + + @Test + @DisplayName("R2 变体: 账号合法 + 密码为空 → 密码不合法") + void testR2_passwordEmpty() { + List result = LoginValidator.validate("123456", ""); + assertEquals(List.of("密码不合法"), result); + } + + // --- 规则 R3: 账号不合法 ∧ 密码合法 → 账号不合法 --- + + @Test + @DisplayName("R3a: 账号过短(5位) + 密码合法 → 账号不合法 [C1为假]") + void testR3_accountTooShort() { + List result = LoginValidator.validate("12345", "abcd1234"); + assertEquals(List.of("账号不合法"), result); + } + + @Test + @DisplayName("R3b: 账号过长(11位) + 密码合法 → 账号不合法 [C2为假]") + void testR3_accountTooLong() { + List result = LoginValidator.validate("12345678901", "abcd1234"); + assertEquals(List.of("账号不合法"), result); + } + + @Test + @DisplayName("R3c: 账号含非数字字符 + 密码合法 → 账号不合法 [C3为假]") + void testR3_accountNonNumeric() { + List result = LoginValidator.validate("12345a", "abcd1234"); + assertEquals(List.of("账号不合法"), result); + } + + @Test + @DisplayName("R3d: 账号为空 + 密码合法 → 账号不合法") + void testR3_accountEmpty() { + List result = LoginValidator.validate("", "abcd1234"); + assertEquals(List.of("账号不合法"), result); + } + + // --- 规则 R4: 账号不合法 ∧ 密码不合法 → 账号不合法 + 密码不合法 --- + + @Test + @DisplayName("R4a: 账号过短 + 密码过短 → 账号不合法 + 密码不合法") + void testR4_bothInvalid_shortShort() { + List result = LoginValidator.validate("123", "abc"); + assertEquals(List.of("账号不合法", "密码不合法"), result); + } + + @Test + @DisplayName("R4b: 账号过长 + 密码过长 → 账号不合法 + 密码不合法") + void testR4_bothInvalid_longLong() { + List result = LoginValidator.validate("12345678901", "abcde12345"); + assertEquals(List.of("账号不合法", "密码不合法"), result); + } + + @Test + @DisplayName("R4c: 账号含字母 + 密码为空 → 账号不合法 + 密码不合法") + void testR4_bothInvalid_nonNumericEmpty() { + List result = LoginValidator.validate("abcdef", ""); + assertEquals(List.of("账号不合法", "密码不合法"), result); + } + + // ======================================================================== + // 补充: 覆盖各个原因条件的边界情况 + // ======================================================================== + + @Test + @DisplayName("边界: 账号恰好6位(下界) + 密码8位") + void testBoundary_account6() { + List result = LoginValidator.validate("100000", "12345678"); + assertEquals(List.of("输入合法"), result); + } + + @Test + @DisplayName("边界: 账号恰好10位(上界) + 密码8位") + void testBoundary_account10() { + List result = LoginValidator.validate("1000000000", "12345678"); + assertEquals(List.of("输入合法"), result); + } + + @Test + @DisplayName("边界: 账号5位(下界-1) + 密码8位") + void testBoundary_account5() { + List result = LoginValidator.validate("10000", "12345678"); + assertEquals(List.of("账号不合法"), result); + } + + @Test + @DisplayName("边界: 账号11位(上界+1) + 密码8位") + void testBoundary_account11() { + List result = LoginValidator.validate("10000000001", "12345678"); + assertEquals(List.of("账号不合法"), result); + } + + @Test + @DisplayName("边界: 密码恰好8位") + void testBoundary_password8() { + List result = LoginValidator.validate("123456", "12345678"); + assertEquals(List.of("输入合法"), result); + } + + @Test + @DisplayName("边界: 密码7位(8-1)") + void testBoundary_password7() { + List result = LoginValidator.validate("123456", "1234567"); + assertEquals(List.of("密码不合法"), result); + } + + @Test + @DisplayName("边界: 密码9位(8+1)") + void testBoundary_password9() { + List result = LoginValidator.validate("123456", "123456789"); + assertEquals(List.of("密码不合法"), result); + } + + @Test + @DisplayName("C3: 账号含空格") + void testAccountWithSpace() { + List result = LoginValidator.validate("123 56", "12345678"); + assertEquals(List.of("账号不合法"), result); + } + + @Test + @DisplayName("C3: 账号含负号") + void testAccountWithMinus() { + List result = LoginValidator.validate("-12345", "12345678"); + assertEquals(List.of("账号不合法"), result); + } + + @Test + @DisplayName("C3: 账号含小数点") + void testAccountWithDot() { + List result = LoginValidator.validate("123.56", "12345678"); + assertEquals(List.of("账号不合法"), result); + } +} diff --git a/hw2/diagrams/cause-effect.puml b/hw2/diagrams/cause-effect.puml new file mode 100644 index 0000000..1854d8f --- /dev/null +++ b/hw2/diagrams/cause-effect.puml @@ -0,0 +1,46 @@ +@startuml cause-effect +title 因果图 — 登录功能输入验证 + +' ========== 布局 ========== +left to right direction +skinparam defaultTextAlignment center +skinparam rectangle { + RoundCorner 15 +} + +' ========== 原因 (Causes) ========== +rectangle "原因" as causes { + rectangle "C1\n账号长度 ≥ 6" as C1 #LightBlue + rectangle "C2\n账号长度 ≤ 10" as C2 #LightBlue + rectangle "C3\n账号为纯数字\n(自然数)" as C3 #LightBlue + rectangle "C4\n密码长度 = 8" as C4 #LightBlue +} + +' ========== 中间节点 ========== +rectangle "M1\n账号合法\n(C1 ∧ C2 ∧ C3)" as M1 #Wheat + +' ========== 结果 (Effects) ========== +rectangle "结果" as effects { + rectangle "E1\n显示\"输入合法\"" as E1 #LightGreen + rectangle "E2\n显示\"账号不合法\"" as E2 #Salmon + rectangle "E3\n显示\"密码不合法\"" as E3 #Salmon +} + +' ========== 关系 ========== +C1 --> M1 : AND +C2 --> M1 : AND +C3 --> M1 : AND + +M1 --> E1 : AND +C4 --> E1 : AND + +M1 ..> E2 : NOT\n(~M1 → E2) +C4 ..> E3 : NOT\n(~C4 → E3) + +' ========== 约束 ========== +note bottom of causes + 约束: C1, C2, C3 相互独立 + C4 独立于 C1/C2/C3 +end note + +@enduml diff --git a/hw2/diagrams/decision-table.puml b/hw2/diagrams/decision-table.puml new file mode 100644 index 0000000..9912828 --- /dev/null +++ b/hw2/diagrams/decision-table.puml @@ -0,0 +1,36 @@ +@startsalt +title 判定表 — 登录功能输入验证 +{+ + +{# +**条件/动作** | **R1** | **R2** | **R3** | **R4** | **R5** | **R6** | **R7** | **R8** +. | . | . | . | . | . | . | . | . +**C1: 账号长度>=6** | T | T | T | T | F | F | F | F +**C2: 账号长度<=10** | T | T | F | F | - | - | - | - +**C3: 账号为纯数字** | T | T | - | - | - | - | - | - +**C4: 密码长度=8** | T | F | T | F | T | F | T | F +. | . | . | . | . | . | . | . | . +**E1: 输入合法** | V | | | | | | | +**E2: 账号不合法** | | | V | V | V | V | V | V +**E3: 密码不合法** | | V | | V | | V | | V +} +-- +{ +T = 条件为真, F = 条件为假, "-" = 不影响结果 +V = 触发该动作 +.. +**简化后 4 条规则:** +{# +**规则** | **账号合法** | **密码合法** | **结果** +R1 | T | T | 输入合法 +R2 | T | F | 密码不合法 +R3 | F | T | 账号不合法 +R4 | F | F | 账号不合法 + 密码不合法 +} +} +} +@endsalt diff --git a/hw2/gradle.properties b/hw2/gradle.properties new file mode 100644 index 0000000..5ad6974 --- /dev/null +++ b/hw2/gradle.properties @@ -0,0 +1 @@ +org.gradle.configuration-cache=true diff --git a/hw2/gradle/libs.versions.toml b/hw2/gradle/libs.versions.toml new file mode 100644 index 0000000..0fa76cc --- /dev/null +++ b/hw2/gradle/libs.versions.toml @@ -0,0 +1,7 @@ +[versions] +guava = "33.4.6-jre" +junit-jupiter = "5.12.1" + +[libraries] +guava = { module = "com.google.guava:guava", version.ref = "guava" } +junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit-jupiter" } diff --git a/hw2/gradle/wrapper/gradle-wrapper.jar b/hw2/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..8bdaf60 Binary files /dev/null and b/hw2/gradle/wrapper/gradle-wrapper.jar differ diff --git a/hw2/gradle/wrapper/gradle-wrapper.properties b/hw2/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..2a84e18 --- /dev/null +++ b/hw2/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.0.0-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/hw2/gradlew b/hw2/gradlew new file mode 100644 index 0000000..ef07e01 --- /dev/null +++ b/hw2/gradlew @@ -0,0 +1,251 @@ +#!/bin/sh + +# +# Copyright © 2015 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH="\\\"\\\"" + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/hw2/gradlew.bat b/hw2/gradlew.bat new file mode 100644 index 0000000..db3a6ac --- /dev/null +++ b/hw2/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH= + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/hw2/settings.gradle b/hw2/settings.gradle new file mode 100644 index 0000000..bfedd3b --- /dev/null +++ b/hw2/settings.gradle @@ -0,0 +1,6 @@ +plugins { + id 'org.gradle.toolchains.foojay-resolver-convention' version '1.0.0' +} + +rootProject.name = 'login' +include('app') diff --git a/hw3/app/build.gradle b/hw3/app/build.gradle new file mode 100644 index 0000000..211bacc --- /dev/null +++ b/hw3/app/build.gradle @@ -0,0 +1,29 @@ +plugins { + id 'application' +} + +repositories { + mavenCentral() +} + +dependencies { + testImplementation libs.junit.jupiter + + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + + implementation libs.guava +} + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } +} + +application { + mainClass = 'triangle.App' +} + +tasks.named('test') { + useJUnitPlatform() +} diff --git a/hw3/app/src/main/java/triangle/App.java b/hw3/app/src/main/java/triangle/App.java new file mode 100644 index 0000000..fab7aa8 --- /dev/null +++ b/hw3/app/src/main/java/triangle/App.java @@ -0,0 +1,50 @@ +package triangle; + +public class App { + + public enum TriangleType { + EQUILATERAL, // 等边三角形 + ISOSCELES, // 等腰三角形 + SCALENE, // 一般三角形 + NOT_A_TRIANGLE // 非三角形 + } + + public static TriangleType classify(int a, int b, int c) { + // 检查边长是否为正整数 + if (a <= 0 || b <= 0 || c <= 0) { + return TriangleType.NOT_A_TRIANGLE; + } + + // 检查三角不等式(任意两边之和大于第三边) + // 使用 long 防止整数溢出 + if ((long) a + b <= c || (long) a + c <= b || (long) b + c <= a) { + return TriangleType.NOT_A_TRIANGLE; + } + + // 判断三角形类型 + if (a == b && b == c) { + return TriangleType.EQUILATERAL; + } else if (a == b || b == c || a == c) { + return TriangleType.ISOSCELES; + } else { + return TriangleType.SCALENE; + } + } + + public static void main(String[] args) { + if (args.length != 3) { + System.out.println("用法: java triangle.App <边1> <边2> <边3>"); + return; + } + int a = Integer.parseInt(args[0]); + int b = Integer.parseInt(args[1]); + int c = Integer.parseInt(args[2]); + TriangleType result = classify(a, b, c); + switch (result) { + case EQUILATERAL -> System.out.println("等边三角形"); + case ISOSCELES -> System.out.println("等腰三角形"); + case SCALENE -> System.out.println("一般三角形"); + case NOT_A_TRIANGLE -> System.out.println("非三角形"); + } + } +} diff --git a/hw3/app/src/test/java/triangle/AppTest.java b/hw3/app/src/test/java/triangle/AppTest.java new file mode 100644 index 0000000..d985f55 --- /dev/null +++ b/hw3/app/src/test/java/triangle/AppTest.java @@ -0,0 +1,249 @@ +package triangle; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import static org.junit.jupiter.api.Assertions.*; +import triangle.App.TriangleType; + +class AppTest { + + // ======================================================================== + // 控制流图分析 + // ======================================================================== + // + // classify(int a, int b, int c) 的判定节点: + // + // D1: a <= 0 || b <= 0 || c <= 0 + // 条件: c1(a<=0), c2(b<=0), c3(c<=0) + // + // D2: (long)a+b <= c || (long)a+c <= b || (long)b+c <= a + // 条件: c4(a+b<=c), c5(a+c<=b), c6(b+c<=a) + // + // D3: a == b && b == c + // 条件: c7(a==b), c8(b==c) + // + // D4: a == b || b == c || a == c + // 条件: c9(a==b), c10(b==c), c11(a==c) + // + // 环形复杂度 V(G) = 5(5个判定出口路径) + // + // ======================================================================== + + // ==================================================================== + // (2) 语句覆盖 — 每条语句至少执行一次 + // ==================================================================== + // + // 需覆盖所有 5 个 return 语句,最少 5 个测试用例: + // + // | TC | 输入 (a,b,c) | 覆盖语句 | 预期结果 | + // |-----|-------------|-------------------|----------------| + // | SC1 | (-1, 5, 5) | D1=T→return | NOT_A_TRIANGLE | + // | SC2 | (1, 2, 10) | D1=F,D2=T→return | NOT_A_TRIANGLE | + // | SC3 | (5, 5, 5) | D1=F,D2=F,D3=T→return | EQUILATERAL | + // | SC4 | (5, 5, 3) | D1=F,D2=F,D3=F,D4=T→return | ISOSCELES | + // | SC5 | (3, 4, 5) | D1=F,D2=F,D3=F,D4=F→return | SCALENE | + + @Nested + @DisplayName("(2) 语句覆盖") + class StatementCoverage { + + @Test + @DisplayName("SC1: 边<=0 → NOT_A_TRIANGLE (覆盖D1真分支return)") + void sc1_negativeSide() { + assertEquals(TriangleType.NOT_A_TRIANGLE, App.classify(-1, 5, 5)); + } + + @Test + @DisplayName("SC2: 不满足三角不等式 → NOT_A_TRIANGLE (覆盖D2真分支return)") + void sc2_inequalityViolation() { + assertEquals(TriangleType.NOT_A_TRIANGLE, App.classify(1, 2, 10)); + } + + @Test + @DisplayName("SC3: 等边三角形 → EQUILATERAL (覆盖D3真分支return)") + void sc3_equilateral() { + assertEquals(TriangleType.EQUILATERAL, App.classify(5, 5, 5)); + } + + @Test + @DisplayName("SC4: 等腰三角形 → ISOSCELES (覆盖D4真分支return)") + void sc4_isosceles() { + assertEquals(TriangleType.ISOSCELES, App.classify(5, 5, 3)); + } + + @Test + @DisplayName("SC5: 一般三角形 → SCALENE (覆盖D4假分支return)") + void sc5_scalene() { + assertEquals(TriangleType.SCALENE, App.classify(3, 4, 5)); + } + } + + // ==================================================================== + // (3) 判定-条件覆盖 + // ==================================================================== + // + // 要求: 每个判定取 T/F,每个条件取 T/F,至少各一次。 + // + // 条件清单: + // D1: c1(a<=0) c2(b<=0) c3(c<=0) + // D2: c4(a+b<=c) c5(a+c<=b) c6(b+c<=a) + // D3: c7(a==b) c8(b==c) + // D4: c9(a==b) c10(b==c) c11(a==c) + // + // | TC | 输入 | D1 | c1 c2 c3 | D2 | c4 c5 c6 | D3 | c7 c8 | D4 | c9 c10 c11 | 结果 | + // |------|------------|-----|----------|-----|----------|-----|-------|-----|------------|----------------| + // | DC1 | (-1, 5, 5) | T | T F F | - | - | - | - | - | - | NOT_A_TRIANGLE | + // | DC2 | (5, -1, 5) | T | F T F | - | - | - | - | - | - | NOT_A_TRIANGLE | + // | DC3 | (5, 5, -1) | T | F F T | - | - | - | - | - | - | NOT_A_TRIANGLE | + // | DC4 | (1, 2, 10) | F | F F F | T | T F F | - | - | - | - | NOT_A_TRIANGLE | + // | DC5 | (1, 10, 2) | F | F F F | T | F T F | - | - | - | - | NOT_A_TRIANGLE | + // | DC6 | (10, 1, 2) | F | F F F | T | F F T | - | - | - | - | NOT_A_TRIANGLE | + // | DC7 | (5, 5, 5) | F | F F F | F | F F F | T | T T | - | - | EQUILATERAL | + // | DC8 | (5, 5, 3) | F | F F F | F | F F F | F | T F | T | T F F | ISOSCELES | + // | DC9 | (3, 5, 5) | F | F F F | F | F F F | F | F T | T | F T F | ISOSCELES | + // | DC10 | (5, 3, 5) | F | F F F | F | F F F | F | F F | T | F F T | ISOSCELES | + // | DC11 | (3, 4, 5) | F | F F F | F | F F F | F | F F | F | F F F | SCALENE | + // + // 覆盖验证: + // c1: T(DC1) F(DC2~11) ✓ c2: T(DC2) F(DC1,DC3~11) ✓ c3: T(DC3) F(DC1,DC2,DC4~11) ✓ + // c4: T(DC4) F(DC5~11) ✓ c5: T(DC5) F(DC4,DC6~11) ✓ c6: T(DC6) F(DC4,DC5,DC7~11) ✓ + // c7: T(DC7,DC8) F(DC9~11) ✓ c8: T(DC7,DC9) F(DC8,DC10,DC11) ✓ + // c9: T(DC8) F(DC9~11) ✓ c10: T(DC9) F(DC8,DC10,DC11) ✓ c11: T(DC10) F(DC8,DC9,DC11) ✓ + // D1: T(DC1~3) F(DC4~11) D2: T(DC4~6) F(DC7~11) D3: T(DC7) F(DC8~11) D4: T(DC8~10) F(DC11) ✓ + + @Nested + @DisplayName("(3) 判定-条件覆盖") + class DecisionConditionCoverage { + + @Test + @DisplayName("DC1: a<=0=T → D1=T → NOT_A_TRIANGLE") + void dc1_aNegative() { + assertEquals(TriangleType.NOT_A_TRIANGLE, App.classify(-1, 5, 5)); + } + + @Test + @DisplayName("DC2: b<=0=T → D1=T → NOT_A_TRIANGLE") + void dc2_bNegative() { + assertEquals(TriangleType.NOT_A_TRIANGLE, App.classify(5, -1, 5)); + } + + @Test + @DisplayName("DC3: c<=0=T → D1=T → NOT_A_TRIANGLE") + void dc3_cNegative() { + assertEquals(TriangleType.NOT_A_TRIANGLE, App.classify(5, 5, -1)); + } + + @Test + @DisplayName("DC4: a+b<=c=T → D2=T → NOT_A_TRIANGLE") + void dc4_sumABLeC() { + assertEquals(TriangleType.NOT_A_TRIANGLE, App.classify(1, 2, 10)); + } + + @Test + @DisplayName("DC5: a+c<=b=T → D2=T → NOT_A_TRIANGLE") + void dc5_sumACLeB() { + assertEquals(TriangleType.NOT_A_TRIANGLE, App.classify(1, 10, 2)); + } + + @Test + @DisplayName("DC6: b+c<=a=T → D2=T → NOT_A_TRIANGLE") + void dc6_sumBCLeA() { + assertEquals(TriangleType.NOT_A_TRIANGLE, App.classify(10, 1, 2)); + } + + @Test + @DisplayName("DC7: a==b=T, b==c=T → D3=T → EQUILATERAL") + void dc7_equilateral() { + assertEquals(TriangleType.EQUILATERAL, App.classify(5, 5, 5)); + } + + @Test + @DisplayName("DC8: a==b=T, b==c=F → D3=F; a==b=T → D4=T → ISOSCELES") + void dc8_isoscelesAB() { + assertEquals(TriangleType.ISOSCELES, App.classify(5, 5, 3)); + } + + @Test + @DisplayName("DC9: a==b=F, b==c=T → D3=F; b==c=T → D4=T → ISOSCELES") + void dc9_isoscelesBC() { + assertEquals(TriangleType.ISOSCELES, App.classify(3, 5, 5)); + } + + @Test + @DisplayName("DC10: a==b=F, b==c=F → D3=F; a==c=T → D4=T → ISOSCELES") + void dc10_isoscelesAC() { + assertEquals(TriangleType.ISOSCELES, App.classify(5, 3, 5)); + } + + @Test + @DisplayName("DC11: 所有条件F → D3=F, D4=F → SCALENE") + void dc11_scalene() { + assertEquals(TriangleType.SCALENE, App.classify(3, 4, 5)); + } + } + + // ==================================================================== + // (4) 基本路径覆盖 + // ==================================================================== + // + // 控制流图 (简化判定节点): + // N1(Start) → N2(D1) →T→ N3(return NOT_A_TRIANGLE) → N11(End) + // →F→ N4(D2) →T→ N5(return NOT_A_TRIANGLE) → N11 + // →F→ N6(D3) →T→ N7(return EQUILATERAL) → N11 + // →F→ N8(D4) →T→ N9(return ISOSCELES) → N11 + // →F→ N10(return SCALENE) → N11 + // + // 节点数 N=11, 边数 E=14 + // 环形复杂度 V(G) = E - N + 2 = 14 - 11 + 2 = 5 + // + // 5 条独立基本路径: + // Path1: N1→N2(T)→N3→N11 (边<=0, 非三角形) + // Path2: N1→N2(F)→N4(T)→N5→N11 (三角不等式不满足, 非三角形) + // Path3: N1→N2(F)→N4(F)→N6(T)→N7→N11 (等边三角形) + // Path4: N1→N2(F)→N4(F)→N6(F)→N8(T)→N9→N11 (等腰三角形) + // Path5: N1→N2(F)→N4(F)→N6(F)→N8(F)→N10→N11 (一般三角形) + // + // | TC | 路径 | 输入 (a,b,c) | 预期结果 | + // |------|--------|-------------|----------------| + // | BP1 | Path1 | (0, 5, 5) | NOT_A_TRIANGLE | + // | BP2 | Path2 | (1, 2, 4) | NOT_A_TRIANGLE | + // | BP3 | Path3 | (6, 6, 6) | EQUILATERAL | + // | BP4 | Path4 | (3, 3, 5) | ISOSCELES | + // | BP5 | Path5 | (3, 4, 6) | SCALENE | + + @Nested + @DisplayName("(4) 基本路径覆盖 (V(G)=5)") + class BasicPathCoverage { + + @Test + @DisplayName("BP1/Path1: N1→N2(T)→N3→End — 边<=0") + void bp1_path1_nonPositive() { + assertEquals(TriangleType.NOT_A_TRIANGLE, App.classify(0, 5, 5)); + } + + @Test + @DisplayName("BP2/Path2: N1→N2(F)→N4(T)→N5→End — 三角不等式不满足") + void bp2_path2_inequalityFail() { + assertEquals(TriangleType.NOT_A_TRIANGLE, App.classify(1, 2, 4)); + } + + @Test + @DisplayName("BP3/Path3: N1→N2(F)→N4(F)→N6(T)→N7→End — 等边") + void bp3_path3_equilateral() { + assertEquals(TriangleType.EQUILATERAL, App.classify(6, 6, 6)); + } + + @Test + @DisplayName("BP4/Path4: N1→N2(F)→N4(F)→N6(F)→N8(T)→N9→End — 等腰") + void bp4_path4_isosceles() { + assertEquals(TriangleType.ISOSCELES, App.classify(3, 3, 5)); + } + + @Test + @DisplayName("BP5/Path5: N1→N2(F)→N4(F)→N6(F)→N8(F)→N10→End — 一般") + void bp5_path5_scalene() { + assertEquals(TriangleType.SCALENE, App.classify(3, 4, 6)); + } + } +} diff --git a/hw3/diagrams/flowchart.puml b/hw3/diagrams/flowchart.puml new file mode 100644 index 0000000..698f289 --- /dev/null +++ b/hw3/diagrams/flowchart.puml @@ -0,0 +1,34 @@ +@startuml flowchart +title 三角形分类程序流程图 — classify(a, b, c) + +start + +:输入三条边 a, b, c; + +if (a <= 0 || b <= 0 || c <= 0 ?) then (是) + :返回 NOT_A_TRIANGLE; + stop +else (否) +endif + +if ((long)a+b <= c ||\n(long)a+c <= b ||\n(long)b+c <= a ?) then (是) + :返回 NOT_A_TRIANGLE; + stop +else (否) +endif + +if (a == b && b == c ?) then (是) + :返回 EQUILATERAL\n(等边三角形); + stop +else (否) +endif + +if (a == b || b == c || a == c ?) then (是) + :返回 ISOSCELES\n(等腰三角形); + stop +else (否) + :返回 SCALENE\n(一般三角形); + stop +endif + +@enduml diff --git a/hw3/gradle.properties b/hw3/gradle.properties new file mode 100644 index 0000000..1385fa5 --- /dev/null +++ b/hw3/gradle.properties @@ -0,0 +1,2 @@ +org.gradle.configuration-cache=true + diff --git a/hw3/gradle/libs.versions.toml b/hw3/gradle/libs.versions.toml new file mode 100644 index 0000000..0fa76cc --- /dev/null +++ b/hw3/gradle/libs.versions.toml @@ -0,0 +1,7 @@ +[versions] +guava = "33.4.6-jre" +junit-jupiter = "5.12.1" + +[libraries] +guava = { module = "com.google.guava:guava", version.ref = "guava" } +junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit-jupiter" } diff --git a/hw3/gradle/wrapper/gradle-wrapper.jar b/hw3/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..8bdaf60 Binary files /dev/null and b/hw3/gradle/wrapper/gradle-wrapper.jar differ diff --git a/hw3/gradle/wrapper/gradle-wrapper.properties b/hw3/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..2a84e18 --- /dev/null +++ b/hw3/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.0.0-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/hw3/gradlew b/hw3/gradlew new file mode 100644 index 0000000..ef07e01 --- /dev/null +++ b/hw3/gradlew @@ -0,0 +1,251 @@ +#!/bin/sh + +# +# Copyright © 2015 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH="\\\"\\\"" + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/hw3/gradlew.bat b/hw3/gradlew.bat new file mode 100644 index 0000000..db3a6ac --- /dev/null +++ b/hw3/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH= + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/hw3/settings.gradle b/hw3/settings.gradle new file mode 100644 index 0000000..4921788 --- /dev/null +++ b/hw3/settings.gradle @@ -0,0 +1,6 @@ +plugins { + id 'org.gradle.toolchains.foojay-resolver-convention' version '1.0.0' +} + +rootProject.name = 'triangle' +include('app') diff --git a/hw4/task1-register-form/app/build.gradle b/hw4/task1-register-form/app/build.gradle new file mode 100644 index 0000000..87f0ab7 --- /dev/null +++ b/hw4/task1-register-form/app/build.gradle @@ -0,0 +1,49 @@ +apply plugin: 'com.android.application' + +configurations.configureEach { + exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-jdk7' + exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-jdk8' +} + +android { + namespace 'com.example.task1registerform' + compileSdk 34 + + defaultConfig { + applicationId 'com.example.task1registerform' + minSdk 26 + targetSdk 34 + versionCode 1 + versionName '1.0' + testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' + } + + buildTypes { + release { + minifyEnabled false + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + + testOptions { + animationsDisabled true + } +} + +dependencies { + implementation 'androidx.appcompat:appcompat:1.7.0' + implementation 'androidx.core:core:1.13.1' + + androidTestImplementation 'androidx.test:core:1.5.0' + androidTestImplementation 'androidx.test:runner:1.5.2' + androidTestImplementation 'androidx.test:rules:1.5.0' + androidTestImplementation 'androidx.test.ext:junit:1.1.5' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' + androidTestImplementation 'androidx.test.espresso:espresso-intents:3.5.1' + androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.5.1' + androidTestImplementation 'junit:junit:4.13.2' +} diff --git a/hw4/task1-register-form/app/src/androidTest/java/com/example/task1registerform/MainActivityFormTest.java b/hw4/task1-register-form/app/src/androidTest/java/com/example/task1registerform/MainActivityFormTest.java new file mode 100644 index 0000000..e49a027 --- /dev/null +++ b/hw4/task1-register-form/app/src/androidTest/java/com/example/task1registerform/MainActivityFormTest.java @@ -0,0 +1,127 @@ +package com.example.task1registerform; + +import static androidx.test.espresso.Espresso.onView; +import static androidx.test.espresso.action.ViewActions.click; +import static androidx.test.espresso.action.ViewActions.closeSoftKeyboard; +import static androidx.test.espresso.action.ViewActions.replaceText; +import static androidx.test.espresso.assertion.ViewAssertions.matches; +import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; +import static androidx.test.espresso.matcher.ViewMatchers.withId; +import static androidx.test.espresso.matcher.ViewMatchers.withText; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; + +import android.app.UiAutomation; +import android.view.accessibility.AccessibilityEvent; + +import androidx.test.core.app.ActivityScenario; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.platform.app.InstrumentationRegistry; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.concurrent.TimeoutException; + +@RunWith(AndroidJUnit4.class) +public class MainActivityFormTest { + + private ActivityScenario activityScenario; + + @Before + public void setUp() { + activityScenario = ActivityScenario.launch(MainActivity.class); + } + + @After + public void tearDown() { + activityScenario.close(); + } + + /** + * 任意输入框为空时,应提示完善信息并保持输入框内容不变。 + */ + @Test + public void testEmptyFields_ShowIncompleteToast_AndKeepEmptyInputs() { + onView(withId(R.id.et_name)).perform(replaceText(""), closeSoftKeyboard()); + onView(withId(R.id.et_phone)).perform(replaceText(""), closeSoftKeyboard()); + onView(withId(R.id.et_email)).perform(replaceText(""), closeSoftKeyboard()); + + clickAndAssertToast(R.id.btn_register, "请完善所有注册信息"); + onView(withId(R.id.et_name)).check(matches(withText(""))); + onView(withId(R.id.et_phone)).check(matches(withText(""))); + onView(withId(R.id.et_email)).check(matches(withText(""))); + } + + /** + * 手机号不满足 11 位数字规则时,应提示手机号错误并保留当前输入。 + */ + @Test + public void testInvalidPhone_ShowPhoneToast_AndKeepInputs() { + fillForm("张三", "123456", "zhangsan@example.com"); + + clickAndAssertToast(R.id.btn_register, "请输入正确的11位手机号"); + onView(withId(R.id.et_name)).check(matches(withText("张三"))); + onView(withId(R.id.et_phone)).check(matches(withText("123456"))); + onView(withId(R.id.et_email)).check(matches(withText("zhangsan@example.com"))); + } + + /** + * 邮箱格式错误时,应提示邮箱错误并保留用户已经输入的数据。 + */ + @Test + public void testInvalidEmail_ShowEmailToast_AndKeepInputs() { + fillForm("李四", "13812345678", "invalid-email"); + + clickAndAssertToast(R.id.btn_register, "请输入正确的邮箱地址"); + onView(withId(R.id.et_name)).check(matches(withText("李四"))); + onView(withId(R.id.et_phone)).check(matches(withText("13812345678"))); + onView(withId(R.id.et_email)).check(matches(withText("invalid-email"))); + } + + /** + * 所有输入均合法时,应提示注册成功并清空全部输入框。 + */ + @Test + public void testRegisterSuccess_ShowSuccessToast_AndClearInputs() { + fillForm("王五", "13912345678", "wangwu@example.com"); + + clickAndAssertToast(R.id.btn_register, "注册成功"); + onView(withId(R.id.et_name)).check(matches(withText(""))); + onView(withId(R.id.et_phone)).check(matches(withText(""))); + onView(withId(R.id.et_email)).check(matches(withText(""))); + } + + private void fillForm(String name, String phone, String email) { + onView(withId(R.id.et_name)).perform(replaceText(name), closeSoftKeyboard()); + onView(withId(R.id.et_phone)).perform(replaceText(phone), closeSoftKeyboard()); + onView(withId(R.id.et_email)).perform(replaceText(email), closeSoftKeyboard()); + } + + private void clickAndAssertToast(int viewId, String message) { + UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation(); + + try { + AccessibilityEvent event = uiAutomation.executeAndWaitForEvent( + () -> onView(withId(viewId)).perform(click()), + candidate -> candidate.getEventType() == AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED + && containsText(candidate, message), + 5000 + ); + assertNotNull(event); + } catch (TimeoutException exception) { + fail("Toast not shown: " + message); + } + } + + private boolean containsText(AccessibilityEvent event, String message) { + for (CharSequence text : event.getText()) { + if (message.contentEquals(text)) { + return true; + } + } + return false; + } +} \ No newline at end of file diff --git a/hw4/task1-register-form/app/src/main/AndroidManifest.xml b/hw4/task1-register-form/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..5aa7c0e --- /dev/null +++ b/hw4/task1-register-form/app/src/main/AndroidManifest.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + diff --git a/hw4/task1-register-form/app/src/main/java/com/example/task1registerform/MainActivity.java b/hw4/task1-register-form/app/src/main/java/com/example/task1registerform/MainActivity.java new file mode 100644 index 0000000..0230040 --- /dev/null +++ b/hw4/task1-register-form/app/src/main/java/com/example/task1registerform/MainActivity.java @@ -0,0 +1,70 @@ +package com.example.task1registerform; + +import android.os.Bundle; +import android.util.Patterns; +import android.view.View; +import android.widget.Button; +import android.widget.EditText; +import android.widget.Toast; + +import androidx.appcompat.app.AppCompatActivity; + +public class MainActivity extends AppCompatActivity { + + private EditText etName; + private EditText etPhone; + private EditText etEmail; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + etName = findViewById(R.id.et_name); + etPhone = findViewById(R.id.et_phone); + etEmail = findViewById(R.id.et_email); + Button btnRegister = findViewById(R.id.btn_register); + + btnRegister.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + handleRegister(); + } + }); + } + + private void handleRegister() { + String name = etName.getText().toString().trim(); + String phone = etPhone.getText().toString().trim(); + String email = etEmail.getText().toString().trim(); + + // 按实验要求顺序完成表单校验,确保 Toast 与场景一一对应。 + if (name.isEmpty() || phone.isEmpty() || email.isEmpty()) { + showToast(R.string.toast_complete_info); + return; + } + + if (!phone.matches("^\\d{11}$")) { + showToast(R.string.toast_invalid_phone); + return; + } + + if (!Patterns.EMAIL_ADDRESS.matcher(email).matches()) { + showToast(R.string.toast_invalid_email); + return; + } + + showToast(R.string.toast_register_success); + clearInputs(); + } + + private void clearInputs() { + etName.setText(""); + etPhone.setText(""); + etEmail.setText(""); + } + + private void showToast(int messageResId) { + Toast.makeText(this, getString(messageResId), Toast.LENGTH_SHORT).show(); + } +} diff --git a/hw4/task1-register-form/app/src/main/res/layout/activity_main.xml b/hw4/task1-register-form/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..1e8bd79 --- /dev/null +++ b/hw4/task1-register-form/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + +