From 17472e7eb0f5f10c937dfb7973bd6f8365e16ebe Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Tue, 14 Dec 2021 15:35:29 -0800 Subject: [PATCH 001/162] Copy proto-based changes from #2173. --- app/BUILD.bazel | 12 +- .../oppia/android/app/activity/BUILD.bazel | 2 +- config/config_proto_assets.bzl | 4 +- .../java/org/oppia/android/config/BUILD.bazel | 4 +- data/BUILD.bazel | 2 +- .../android/data/backends/gae/BUILD.bazel | 2 +- .../android/data/persistence/BUILD.bazel | 2 +- domain/BUILD.bazel | 12 +- domain/domain_assets.bzl | 24 +- .../oppia/android/domain/audio/BUILD.bazel | 2 +- .../oppia/android/domain/locale/BUILD.bazel | 6 +- .../android/domain/onboarding/BUILD.bazel | 2 +- .../android/domain/oppialogger/BUILD.bazel | 2 +- .../domain/oppialogger/analytics/BUILD.bazel | 2 +- .../domain/oppialogger/exceptions/BUILD.bazel | 2 +- .../oppia/android/domain/state/BUILD.bazel | 8 +- .../android/domain/translation/BUILD.bazel | 12 +- .../org/oppia/android/domain/util/BUILD.bazel | 4 +- .../oppia/android/domain/locale/BUILD.bazel | 6 +- .../android/domain/translation/BUILD.bazel | 2 +- model/BUILD.bazel | 269 +----------------- model/oppia_proto_library.bzl | 19 ++ model/src/main/proto/BUILD.bazel | 257 +++++++++++++++++ .../proto/format_import_proto_library.bzl | 44 --- model/text_proto_assets.bzl | 4 +- scripts/script_assets.bzl | 14 +- .../oppia/android/scripts/common/BUILD.bazel | 2 +- .../oppia/android/testing/junit/BUILD.bazel | 2 +- .../oppia/android/testing/data/BUILD.bazel | 2 +- utility/BUILD.bazel | 6 +- .../org/oppia/android/util/locale/BUILD.bazel | 4 +- .../oppia/android/util/logging/BUILD.bazel | 4 +- .../android/util/logging/firebase/BUILD.bazel | 4 +- .../android/util/caching/testing/BUILD.bazel | 2 +- .../org/oppia/android/util/locale/BUILD.bazel | 2 +- 35 files changed, 357 insertions(+), 390 deletions(-) create mode 100644 model/oppia_proto_library.bzl create mode 100644 model/src/main/proto/BUILD.bazel delete mode 100644 model/src/main/proto/format_import_proto_library.bzl diff --git a/app/BUILD.bazel b/app/BUILD.bazel index 0ae4da3c1ce..3e09ece2557 100644 --- a/app/BUILD.bazel +++ b/app/BUILD.bazel @@ -546,8 +546,8 @@ android_library( ":views", "//app/src/main/java/org/oppia/android/app/translation:app_language_activity_injector_provider", "//app/src/main/java/org/oppia/android/app/translation:app_language_resource_handler", - "//model:interaction_object_java_proto_lite", - "//model:thumbnail_java_proto_lite", + "//model/src/main/proto:interaction_object_java_proto_lite", + "//model/src/main/proto:thumbnail_java_proto_lite", "//third_party:androidx_annotation_annotation", "//third_party:androidx_constraintlayout_constraintlayout", "//third_party:androidx_core_core", @@ -579,8 +579,8 @@ kt_android_library( deps = [ ":dagger", "//domain/src/main/java/org/oppia/android/domain/audio:cellular_audio_dialog_controller", - "//model:question_java_proto_lite", - "//model:topic_java_proto_lite", + "//model/src/main/proto:question_java_proto_lite", + "//model/src/main/proto:topic_java_proto_lite", "//third_party:androidx_recyclerview_recyclerview", ], ) @@ -682,7 +682,7 @@ android_library( ":view_models", "//app/src/main/java/org/oppia/android/app/translation:app_language_activity_injector_provider", "//app/src/main/java/org/oppia/android/app/translation:app_language_resource_handler", - "//model:thumbnail_java_proto_lite", + "//model/src/main/proto:thumbnail_java_proto_lite", "//third_party:androidx_annotation_annotation", "//third_party:androidx_constraintlayout_constraintlayout", "//third_party:androidx_lifecycle_lifecycle-livedata-core", @@ -735,7 +735,7 @@ kt_android_library( "//domain/src/main/java/org/oppia/android/domain/oppialogger:startup_listener", "//domain/src/main/java/org/oppia/android/domain/oppialogger/exceptions:logger_module", "//domain/src/main/java/org/oppia/android/domain/oppialogger/loguploader:worker_module", - "//model:arguments_java_proto_lite", + "//model/src/main/proto:arguments_java_proto_lite", "//app/src/main/java/org/oppia/android/app/testing/activity:test_activity", "//third_party:androidx_databinding_databinding-adapters", "//third_party:androidx_databinding_databinding-common", diff --git a/app/src/main/java/org/oppia/android/app/activity/BUILD.bazel b/app/src/main/java/org/oppia/android/app/activity/BUILD.bazel index 8b959707002..3c09724c453 100644 --- a/app/src/main/java/org/oppia/android/app/activity/BUILD.bazel +++ b/app/src/main/java/org/oppia/android/app/activity/BUILD.bazel @@ -75,6 +75,6 @@ kt_android_library( "//app:app_visibility", ], deps = [ - "//model:profile_java_proto_lite", + "//model/src/main/proto:profile_java_proto_lite", ], ) diff --git a/config/config_proto_assets.bzl b/config/config_proto_assets.bzl index f3073c33163..c45099babd2 100644 --- a/config/config_proto_assets.bzl +++ b/config/config_proto_assets.bzl @@ -23,8 +23,8 @@ def generate_supported_languages_configuration_from_text_proto( names = [supported_language_text_proto_file_name], proto_dep_name = "languages", proto_type_name = "SupportedLanguages", - name_prefix = name, + name_prefix = "supported_languages", asset_dir = "languages", - proto_dep_bazel_target_prefix = "//model", + proto_dep_bazel_target_prefix = "//model/src/main/proto", proto_package = "model", ) diff --git a/config/src/java/org/oppia/android/config/BUILD.bazel b/config/src/java/org/oppia/android/config/BUILD.bazel index 36079416378..8cc9ae4591c 100644 --- a/config/src/java/org/oppia/android/config/BUILD.bazel +++ b/config/src/java/org/oppia/android/config/BUILD.bazel @@ -10,7 +10,7 @@ _SUPPORTED_LANGUAGES_CONFIG_ASSETS = generate_proto_binary_assets( asset_dir = "languages", name_prefix = "supported_languages_config_assets", names = ["supported_languages"], - proto_dep_bazel_target_prefix = "//model", + proto_dep_bazel_target_prefix = "//model/src/main/proto", proto_dep_name = "languages", proto_package = "model", proto_type_name = "SupportedLanguages", @@ -21,7 +21,7 @@ _SUPPORTED_REGIONS_CONFIG_ASSETS = generate_proto_binary_assets( asset_dir = "languages", name_prefix = "supported_regions_config_assets", names = ["supported_regions"], - proto_dep_bazel_target_prefix = "//model", + proto_dep_bazel_target_prefix = "//model/src/main/proto", proto_dep_name = "languages", proto_package = "model", proto_type_name = "SupportedRegions", diff --git a/data/BUILD.bazel b/data/BUILD.bazel index 3dc0dde487c..18fe1fd9127 100644 --- a/data/BUILD.bazel +++ b/data/BUILD.bazel @@ -14,7 +14,7 @@ TEST_DEPS = [ "//data/src/main/java/org/oppia/android/data/backends/gae:prod_module", "//data/src/main/java/org/oppia/android/data/backends/gae/model", "//data/src/main/java/org/oppia/android/data/persistence:cache_store", - "//model:test_models", + "//model/src/main/proto:test_models", "//testing", "//testing/src/main/java/org/oppia/android/testing/network", "//testing/src/main/java/org/oppia/android/testing/network:test_module", diff --git a/data/src/main/java/org/oppia/android/data/backends/gae/BUILD.bazel b/data/src/main/java/org/oppia/android/data/backends/gae/BUILD.bazel index 991bb9cb694..c5859673b0f 100644 --- a/data/src/main/java/org/oppia/android/data/backends/gae/BUILD.bazel +++ b/data/src/main/java/org/oppia/android/data/backends/gae/BUILD.bazel @@ -17,7 +17,7 @@ kt_android_library( deps = [ ":constants", ":network_config_annotations", - "//model:arguments_java_proto_lite", + "//model/src/main/proto:arguments_java_proto_lite", "//third_party:com_squareup_okhttp3_okhttp", "//third_party:javax_inject_javax_inject", "//utility/src/main/java/org/oppia/android/util/extensions:context_extensions", diff --git a/data/src/main/java/org/oppia/android/data/persistence/BUILD.bazel b/data/src/main/java/org/oppia/android/data/persistence/BUILD.bazel index 0ead7ddfd08..db67d9fb798 100644 --- a/data/src/main/java/org/oppia/android/data/persistence/BUILD.bazel +++ b/data/src/main/java/org/oppia/android/data/persistence/BUILD.bazel @@ -12,7 +12,7 @@ kt_android_library( visibility = ["//:oppia_api_visibility"], deps = [ ":dagger", - "//model:profile_java_proto_lite", + "//model/src/main/proto:profile_java_proto_lite", "//utility", "//utility/src/main/java/org/oppia/android/util/data:async_data_subscription_manager", "//utility/src/main/java/org/oppia/android/util/data:async_result", diff --git a/domain/BUILD.bazel b/domain/BUILD.bazel index 0edd663c6e5..503c519682e 100755 --- a/domain/BUILD.bazel +++ b/domain/BUILD.bazel @@ -112,11 +112,11 @@ kt_android_library( "//domain/src/main/java/org/oppia/android/domain/util:asset", "//domain/src/main/java/org/oppia/android/domain/util:extensions", "//domain/src/main/java/org/oppia/android/domain/util:retriever", - "//model:exploration_checkpoint_java_proto_lite", - "//model:onboarding_java_proto_lite", - "//model:platform_parameter_java_proto_lite", - "//model:question_java_proto_lite", - "//model:topic_java_proto_lite", + "//model/src/main/proto:exploration_checkpoint_java_proto_lite", + "//model/src/main/proto:onboarding_java_proto_lite", + "//model/src/main/proto:platform_parameter_java_proto_lite", + "//model/src/main/proto:question_java_proto_lite", + "//model/src/main/proto:topic_java_proto_lite", "//third_party:androidx_work_work-runtime-ktx", "//utility/src/main/java/org/oppia/android/util/caching:topic_list_to_cache", "//utility/src/main/java/org/oppia/android/util/data:data_providers", @@ -149,7 +149,7 @@ kt_android_library( "src/test/java/org/oppia/android/domain/classify/InteractionObjectTestBuilder.kt", ], deps = [ - "//model:question_java_proto_lite", + "//model/src/main/proto:question_java_proto_lite", ], ) diff --git a/domain/domain_assets.bzl b/domain/domain_assets.bzl index dff28e34f9e..89a3ae008a3 100644 --- a/domain/domain_assets.bzl +++ b/domain/domain_assets.bzl @@ -32,53 +32,53 @@ def generate_assets_list_from_text_protos( names = topic_list_file_names, proto_dep_name = "topic", proto_type_name = "TopicIdList", - name_prefix = name, + name_prefix = "topic_id_list", asset_dir = "src/main/assets", - proto_dep_bazel_target_prefix = "//model", + proto_dep_bazel_target_prefix = "//model/src/main/proto", proto_package = "model", ) + generate_proto_binary_assets( name = name, names = topic_file_names, proto_dep_name = "topic", proto_type_name = "TopicRecord", - name_prefix = name, + name_prefix = "topic_record", asset_dir = "src/main/assets", - proto_dep_bazel_target_prefix = "//model", + proto_dep_bazel_target_prefix = "//model/src/main/proto", proto_package = "model", ) + generate_proto_binary_assets( name = name, names = subtopic_file_names, proto_dep_name = "topic", proto_type_name = "SubtopicRecord", - name_prefix = name, + name_prefix = "subtopic_record", asset_dir = "src/main/assets", - proto_dep_bazel_target_prefix = "//model", + proto_dep_bazel_target_prefix = "//model/src/main/proto", proto_package = "model", ) + generate_proto_binary_assets( name = name, names = story_file_names, proto_dep_name = "topic", proto_type_name = "StoryRecord", - name_prefix = name, + name_prefix = "story_record", asset_dir = "src/main/assets", - proto_dep_bazel_target_prefix = "//model", + proto_dep_bazel_target_prefix = "//model/src/main/proto", proto_package = "model", ) + generate_proto_binary_assets( name = name, names = skills_file_names, proto_dep_name = "topic", proto_type_name = "ConceptCardList", - name_prefix = name, + name_prefix = "concept_card_list", asset_dir = "src/main/assets", - proto_dep_bazel_target_prefix = "//model", + proto_dep_bazel_target_prefix = "//model/src/main/proto", proto_package = "model", ) + generate_proto_binary_assets( name = name, names = exploration_file_names, proto_dep_name = "exploration", proto_type_name = "Exploration", - name_prefix = name, + name_prefix = "exploration", asset_dir = "src/main/assets", - proto_dep_bazel_target_prefix = "//model", + proto_dep_bazel_target_prefix = "//model/src/main/proto", proto_package = "model", ) diff --git a/domain/src/main/java/org/oppia/android/domain/audio/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/audio/BUILD.bazel index 440876b4862..c7141fc8c91 100644 --- a/domain/src/main/java/org/oppia/android/domain/audio/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/audio/BUILD.bazel @@ -33,7 +33,7 @@ kt_android_library( deps = [ "//data/src/main/java/org/oppia/android/data/persistence:cache_store", "//domain/src/main/java/org/oppia/android/domain/oppialogger:oppia_logger", - "//model:topic_java_proto_lite", + "//model/src/main/proto:topic_java_proto_lite", "//third_party:javax_inject_javax_inject", "//utility/src/main/java/org/oppia/android/util/data:data_provider", ], diff --git a/domain/src/main/java/org/oppia/android/domain/locale/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/locale/BUILD.bazel index afc2f8b9fca..a62092b664e 100644 --- a/domain/src/main/java/org/oppia/android/domain/locale/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/locale/BUILD.bazel @@ -17,7 +17,7 @@ kt_android_library( ":display_locale_impl", ":language_config_retriever", "//domain/src/main/java/org/oppia/android/domain/oppialogger:oppia_logger", - "//model:languages_java_proto_lite", + "//model/src/main/proto:languages_java_proto_lite", "//utility/src/main/java/org/oppia/android/util/data:async_result", "//utility/src/main/java/org/oppia/android/util/data:data_provider", "//utility/src/main/java/org/oppia/android/util/locale:oppia_locale", @@ -64,7 +64,7 @@ kt_android_library( "//domain:domain_testing_visibility", ], deps = [ - "//model:languages_java_proto_lite", + "//model/src/main/proto:languages_java_proto_lite", "//utility/src/main/java/org/oppia/android/util/locale:oppia_locale", ], ) @@ -80,7 +80,7 @@ kt_android_library( deps = [ ":dagger", "//config/src/java/org/oppia/android/config:languages_config", - "//model:languages_java_proto_lite", + "//model/src/main/proto:languages_java_proto_lite", "//utility/src/main/java/org/oppia/android/util/caching:annotations", "//utility/src/main/java/org/oppia/android/util/caching:asset_repository", ], diff --git a/domain/src/main/java/org/oppia/android/domain/onboarding/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/onboarding/BUILD.bazel index eb14efb6c0a..f3ba2025c05 100644 --- a/domain/src/main/java/org/oppia/android/domain/onboarding/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/onboarding/BUILD.bazel @@ -15,7 +15,7 @@ kt_android_library( ":exploration_meta_data_retriever", "//data/src/main/java/org/oppia/android/data/persistence:cache_store", "//domain/src/main/java/org/oppia/android/domain/oppialogger:oppia_logger", - "//model:onboarding_java_proto_lite", + "//model/src/main/proto:onboarding_java_proto_lite", "//third_party:javax_inject_javax_inject", "//utility/src/main/java/org/oppia/android/util/data:data_provider", "//utility/src/main/java/org/oppia/android/util/data:data_providers", diff --git a/domain/src/main/java/org/oppia/android/domain/oppialogger/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/oppialogger/BUILD.bazel index e01fb432ed4..38af73c4546 100644 --- a/domain/src/main/java/org/oppia/android/domain/oppialogger/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/oppialogger/BUILD.bazel @@ -13,7 +13,7 @@ kt_android_library( visibility = ["//domain:__subpackages__"], deps = [ "//domain/src/main/java/org/oppia/android/domain/oppialogger/analytics:controller", - "//model:event_logger_java_proto_lite", + "//model/src/main/proto:event_logger_java_proto_lite", "//utility/src/main/java/org/oppia/android/util/logging:console_logger", ], ) diff --git a/domain/src/main/java/org/oppia/android/domain/oppialogger/analytics/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/oppialogger/analytics/BUILD.bazel index 320025217d9..0166ef20c36 100644 --- a/domain/src/main/java/org/oppia/android/domain/oppialogger/analytics/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/oppialogger/analytics/BUILD.bazel @@ -13,7 +13,7 @@ kt_android_library( deps = [ "//data/src/main/java/org/oppia/android/data/persistence:cache_store", "//domain/src/main/java/org/oppia/android/domain/oppialogger:storage_module", - "//model:event_logger_java_proto_lite", + "//model/src/main/proto:event_logger_java_proto_lite", "//utility/src/main/java/org/oppia/android/util/data:data_provider", "//utility/src/main/java/org/oppia/android/util/logging:console_logger", "//utility/src/main/java/org/oppia/android/util/logging:event_logger", diff --git a/domain/src/main/java/org/oppia/android/domain/oppialogger/exceptions/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/oppialogger/exceptions/BUILD.bazel index 0cd3f1b4a2b..af13183d30c 100644 --- a/domain/src/main/java/org/oppia/android/domain/oppialogger/exceptions/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/oppialogger/exceptions/BUILD.bazel @@ -14,7 +14,7 @@ kt_android_library( deps = [ "//data/src/main/java/org/oppia/android/data/persistence:cache_store", "//domain/src/main/java/org/oppia/android/domain/oppialogger:storage_module", - "//model:event_logger_java_proto_lite", + "//model/src/main/proto:event_logger_java_proto_lite", "//utility/src/main/java/org/oppia/android/util/data:data_provider", "//utility/src/main/java/org/oppia/android/util/logging:console_logger", "//utility/src/main/java/org/oppia/android/util/logging:exception_logger", diff --git a/domain/src/main/java/org/oppia/android/domain/state/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/state/BUILD.bazel index 5568e6221f2..aa32d0a3110 100644 --- a/domain/src/main/java/org/oppia/android/domain/state/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/state/BUILD.bazel @@ -11,7 +11,7 @@ kt_android_library( ], visibility = ["//:oppia_api_visibility"], deps = [ - "//model:exploration_checkpoint_java_proto_lite", + "//model/src/main/proto:exploration_checkpoint_java_proto_lite", ], ) @@ -22,7 +22,7 @@ kt_android_library( ], visibility = ["//:oppia_api_visibility"], deps = [ - "//model:exploration_checkpoint_java_proto_lite", + "//model/src/main/proto:exploration_checkpoint_java_proto_lite", ], ) @@ -33,7 +33,7 @@ kt_android_library( ], visibility = ["//:oppia_api_visibility"], deps = [ - "//model:exploration_java_proto_lite", - "//model:question_java_proto_lite", + "//model/src/main/proto:exploration_java_proto_lite", + "//model/src/main/proto:question_java_proto_lite", ], ) diff --git a/domain/src/main/java/org/oppia/android/domain/translation/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/translation/BUILD.bazel index 4096a7f922f..700a8b0ad15 100644 --- a/domain/src/main/java/org/oppia/android/domain/translation/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/translation/BUILD.bazel @@ -13,12 +13,12 @@ kt_android_library( visibility = ["//:oppia_api_visibility"], deps = [ "//domain/src/main/java/org/oppia/android/domain/locale:locale_controller", - "//model:interaction_object_java_proto_lite", - "//model:languages_java_proto_lite", - "//model:profile_java_proto_lite", - "//model:subtitled_html_java_proto_lite", - "//model:subtitled_unicode_java_proto_lite", - "//model:translation_java_proto_lite", + "//model/src/main/proto:interaction_object_java_proto_lite", + "//model/src/main/proto:languages_java_proto_lite", + "//model/src/main/proto:profile_java_proto_lite", + "//model/src/main/proto:subtitled_html_java_proto_lite", + "//model/src/main/proto:subtitled_unicode_java_proto_lite", + "//model/src/main/proto:translation_java_proto_lite", "//utility/src/main/java/org/oppia/android/util/data:async_result", "//utility/src/main/java/org/oppia/android/util/data:data_provider", "//utility/src/main/java/org/oppia/android/util/data:data_providers", diff --git a/domain/src/main/java/org/oppia/android/domain/util/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/util/BUILD.bazel index 923a0a54e53..26dc7fb7027 100644 --- a/domain/src/main/java/org/oppia/android/domain/util/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/util/BUILD.bazel @@ -31,7 +31,7 @@ kt_android_library( ], visibility = ["//domain:__subpackages__"], deps = [ - "//model:question_java_proto_lite", + "//model/src/main/proto:question_java_proto_lite", "//third_party:androidx_work_work-runtime-ktx", ], ) @@ -44,7 +44,7 @@ kt_android_library( visibility = ["//domain:__subpackages__"], deps = [ ":extensions", - "//model:question_java_proto_lite", + "//model/src/main/proto:question_java_proto_lite", "//third_party:javax_inject_javax_inject", ], ) diff --git a/domain/src/test/java/org/oppia/android/domain/locale/BUILD.bazel b/domain/src/test/java/org/oppia/android/domain/locale/BUILD.bazel index 6d992879342..b1a8a187562 100644 --- a/domain/src/test/java/org/oppia/android/domain/locale/BUILD.bazel +++ b/domain/src/test/java/org/oppia/android/domain/locale/BUILD.bazel @@ -34,7 +34,7 @@ oppia_android_test( deps = [ ":dagger", "//domain/src/main/java/org/oppia/android/domain/locale:content_locale_impl", - "//model:languages_java_proto_lite", + "//model/src/main/proto:languages_java_proto_lite", "//third_party:androidx_test_ext_junit", "//third_party:com_google_truth_extensions_truth-liteproto-extension", "//third_party:com_google_truth_truth", @@ -54,7 +54,7 @@ oppia_android_test( ":dagger", "//domain:test_resources", "//domain/src/main/java/org/oppia/android/domain/locale:display_locale_impl", - "//model:languages_java_proto_lite", + "//model/src/main/proto:languages_java_proto_lite", "//testing", "//testing/src/main/java/org/oppia/android/testing/time:test_module", "//third_party:androidx_test_ext_junit", @@ -121,7 +121,7 @@ oppia_android_test( deps = [ ":dagger", "//domain/src/main/java/org/oppia/android/domain/locale:locale_controller", - "//model:languages_java_proto_lite", + "//model/src/main/proto:languages_java_proto_lite", "//testing", "//testing/src/main/java/org/oppia/android/testing/data:data_provider_test_monitor", "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module", diff --git a/domain/src/test/java/org/oppia/android/domain/translation/BUILD.bazel b/domain/src/test/java/org/oppia/android/domain/translation/BUILD.bazel index 0877354b02c..d4733f05076 100644 --- a/domain/src/test/java/org/oppia/android/domain/translation/BUILD.bazel +++ b/domain/src/test/java/org/oppia/android/domain/translation/BUILD.bazel @@ -15,7 +15,7 @@ oppia_android_test( ":dagger", "//domain/src/main/java/org/oppia/android/domain/locale:locale_controller", "//domain/src/main/java/org/oppia/android/domain/translation:translation_controller", - "//model:languages_java_proto_lite", + "//model/src/main/proto:languages_java_proto_lite", "//testing", "//testing/src/main/java/org/oppia/android/testing/data:data_provider_test_monitor", "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module", diff --git a/model/BUILD.bazel b/model/BUILD.bazel index d64d7e2a340..1755f53361e 100644 --- a/model/BUILD.bazel +++ b/model/BUILD.bazel @@ -1,270 +1,3 @@ -# TODO(#1532): Rename file to 'BUILD' post-Gradle. """ -This library contains all protos used in the app and is a dependency for all other modules. -In Bazel, proto files are built using the proto_library() and java_lite_proto_library() rules. -The proto_library() rule creates a proto file library to be used in multiple languages. -The java_lite_proto_library() rule takes in a proto_library target and generates java code. +TODO: add docs """ - -load("@rules_java//java:defs.bzl", "java_lite_proto_library") -load("@rules_proto//proto:defs.bzl", "proto_library") -load("//model:src/main/proto/format_import_proto_library.bzl", "format_import_proto_library") - -# NOTE TO DEVELOPERS: When adding new protos, each proto will need to have both a proto_library -# and java_lite_proto_library. See the examples below for context. Further, once the proto lite -# library is added, it should be included in the exports list in the model library at the -# bottom of this file so that other parts of the app get access to it. If protos import other -# protos, they need to use format_import_proto_library (again, see examples below for how to do -# this). -# -# For example, if adding a new proto file called 'important_structure.proto', add these: -# proto_library( -# name = "important_structure_proto", -# srcs = ["src/main/proto/important_structure.proto"], -# ) -# -# java_lite_proto_library( -# name = "important_structure_java_proto_lite", -# deps = [":important_structure_proto"], -# ) -# -# And change the 'model' library at the bottom of the file, e.g.: -# android_library( -# name = "model", -# exports = [ -# ... -# ":important_structure_java_proto_lite", -# ... -# ], -# ... -# ) - -proto_library( - name = "arguments_proto", - srcs = ["src/main/proto/arguments.proto"], -) - -java_lite_proto_library( - name = "arguments_java_proto_lite", - visibility = ["//:oppia_api_visibility"], - deps = [":arguments_proto"], -) - -proto_library( - name = "event_logger_proto", - srcs = ["src/main/proto/oppia_logger.proto"], -) - -java_lite_proto_library( - name = "event_logger_java_proto_lite", - visibility = ["//visibility:public"], - deps = [":event_logger_proto"], -) - -format_import_proto_library( - name = "exploration_checkpoint", - src = "src/main/proto/exploration_checkpoint.proto", - deps = [":exploration_proto"], -) - -java_lite_proto_library( - name = "exploration_checkpoint_java_proto_lite", - visibility = ["//:oppia_api_visibility"], - deps = [":exploration_checkpoint_proto"], -) - -proto_library( - name = "interaction_object_proto", - srcs = ["src/main/proto/interaction_object.proto"], -) - -java_lite_proto_library( - name = "interaction_object_java_proto_lite", - visibility = ["//visibility:public"], - deps = [":interaction_object_proto"], -) - -proto_library( - name = "languages_proto", - srcs = ["src/main/proto/languages.proto"], - visibility = ["//visibility:public"], -) - -java_lite_proto_library( - name = "languages_java_proto_lite", - visibility = ["//visibility:public"], - deps = [":languages_proto"], -) - -proto_library( - name = "onboarding_proto", - srcs = ["src/main/proto/onboarding.proto"], -) - -java_lite_proto_library( - name = "onboarding_java_proto_lite", - visibility = ["//visibility:public"], - deps = [":onboarding_proto"], -) - -proto_library( - name = "profile_proto", - srcs = ["src/main/proto/profile.proto"], -) - -java_lite_proto_library( - name = "profile_java_proto_lite", - visibility = ["//:oppia_api_visibility"], - deps = [":profile_proto"], -) - -proto_library( - name = "subtitled_html_proto", - srcs = ["src/main/proto/subtitled_html.proto"], -) - -java_lite_proto_library( - name = "subtitled_html_java_proto_lite", - visibility = ["//:oppia_api_visibility"], - deps = [":subtitled_html_proto"], -) - -proto_library( - name = "subtitled_unicode_proto", - srcs = ["src/main/proto/subtitled_unicode.proto"], -) - -java_lite_proto_library( - name = "subtitled_unicode_java_proto_lite", - visibility = ["//:oppia_api_visibility"], - deps = [":subtitled_unicode_proto"], -) - -proto_library( - name = "test_proto", - srcs = ["src/main/proto/test.proto"], -) - -java_lite_proto_library( - name = "test_java_proto_lite", - deps = [":test_proto"], -) - -proto_library( - name = "thumbnail_proto", - srcs = ["src/main/proto/thumbnail.proto"], -) - -java_lite_proto_library( - name = "thumbnail_java_proto_lite", - visibility = ["//:oppia_api_visibility"], - deps = [":thumbnail_proto"], -) - -proto_library( - name = "translation_proto", - srcs = ["src/main/proto/translation.proto"], -) - -java_lite_proto_library( - name = "translation_java_proto_lite", - visibility = ["//:oppia_api_visibility"], - deps = [":translation_proto"], -) - -proto_library( - name = "voiceover_proto", - srcs = ["src/main/proto/voiceover.proto"], -) - -java_lite_proto_library( - name = "voiceover_java_proto_lite", - deps = [":voiceover_proto"], -) - -format_import_proto_library( - name = "feedback_reporting", - src = "src/main/proto/feedback_reporting.proto", - deps = [ - ":profile_proto", - ], -) - -java_lite_proto_library( - name = "feedback_reporting_java_proto_lite", - deps = [":feedback_reporting_proto"], -) - -format_import_proto_library( - name = "question", - src = "src/main/proto/question.proto", - deps = [ - ":exploration_proto", - ":subtitled_html_proto", - ":subtitled_unicode_proto", - ], -) - -java_lite_proto_library( - name = "question_java_proto_lite", - visibility = ["//:oppia_api_visibility"], - deps = [":question_proto"], -) - -format_import_proto_library( - name = "topic", - src = "src/main/proto/topic.proto", - visibility = ["//visibility:public"], - deps = [ - ":subtitled_html_proto", - ":subtitled_unicode_proto", - ":thumbnail_proto", - ":translation_proto", - ":voiceover_proto", - ], -) - -java_lite_proto_library( - name = "topic_java_proto_lite", - visibility = ["//:oppia_api_visibility"], - deps = [":topic_proto"], -) - -format_import_proto_library( - name = "exploration", - src = "src/main/proto/exploration.proto", - visibility = ["//visibility:public"], - deps = [ - ":interaction_object_proto", - ":subtitled_html_proto", - ":subtitled_unicode_proto", - ":translation_proto", - ":voiceover_proto", - ], -) - -java_lite_proto_library( - name = "exploration_java_proto_lite", - visibility = ["//visibility:public"], - deps = [":exploration_proto"], -) - -format_import_proto_library( - name = "platform_parameter", - src = "src/main/proto/platform_parameter.proto", -) - -java_lite_proto_library( - name = "platform_parameter_java_proto_lite", - visibility = ["//:oppia_api_visibility"], - deps = [":platform_parameter_proto"], -) - -android_library( - name = "test_models", - testonly = True, - visibility = ["//visibility:public"], - exports = [ - ":test_java_proto_lite", - ], -) diff --git a/model/oppia_proto_library.bzl b/model/oppia_proto_library.bzl new file mode 100644 index 00000000000..8f6ac753135 --- /dev/null +++ b/model/oppia_proto_library.bzl @@ -0,0 +1,19 @@ +""" +TODO: add docs +""" + +load("@rules_proto//proto:defs.bzl", "proto_library") + +# TODO: add regex check +# TODO: add TODO to remove +# TODO: maybe close format proto issue with this PR? + +def oppia_proto_library(name, strip_import_prefix = "", **kwargs): + """ + TODO: add docs + """ + proto_library( + name = name, + strip_import_prefix = strip_import_prefix, + **kwargs + ) diff --git a/model/src/main/proto/BUILD.bazel b/model/src/main/proto/BUILD.bazel new file mode 100644 index 00000000000..90da6e48b77 --- /dev/null +++ b/model/src/main/proto/BUILD.bazel @@ -0,0 +1,257 @@ +# TODO(#1532): Rename file to 'BUILD' post-Gradle. +""" +This library contains all protos used in the app and is a dependency for all other modules. +In Bazel, proto files are built using the oppia_proto_library() and java_lite_proto_library() rules. +The oppia_proto_library() rule creates a proto file library to be used in multiple languages. +The java_lite_proto_library() rule takes in a proto_library target and generates java code. +""" + +load("@rules_java//java:defs.bzl", "java_lite_proto_library") +load("//model:oppia_proto_library.bzl", "oppia_proto_library") + +# NOTE TO DEVELOPERS: When adding new protos, each proto will need to have both a proto_library +# and java_lite_proto_library. +# +# For example, if adding a new proto file called 'important_structure.proto', add these: +# oppia_proto_library( +# name = "important_structure_proto", +# srcs = ["src/main/proto/important_structure.proto"], +# ) +# +# java_lite_proto_library( +# name = "important_structure_java_proto_lite", +# deps = [":important_structure_proto"], +# ) + +oppia_proto_library( + name = "arguments_proto", + srcs = ["arguments.proto"], +) + +java_lite_proto_library( + name = "arguments_java_proto_lite", + visibility = ["//:oppia_api_visibility"], + deps = [":arguments_proto"], +) + +oppia_proto_library( + name = "event_logger_proto", + srcs = ["oppia_logger.proto"], +) + +java_lite_proto_library( + name = "event_logger_java_proto_lite", + visibility = ["//:oppia_api_visibility"], + deps = [":event_logger_proto"], +) + +oppia_proto_library( + name = "exploration_checkpoint_proto", + srcs = ["exploration_checkpoint.proto"], + deps = [":exploration_proto"], +) + +java_lite_proto_library( + name = "exploration_checkpoint_java_proto_lite", + visibility = ["//:oppia_api_visibility"], + deps = [":exploration_checkpoint_proto"], +) + +oppia_proto_library( + name = "interaction_object_proto", + srcs = ["interaction_object.proto"], + visibility = ["//:oppia_api_visibility"], +) + +java_lite_proto_library( + name = "interaction_object_java_proto_lite", + visibility = ["//:oppia_api_visibility"], + deps = [":interaction_object_proto"], +) + +oppia_proto_library( + name = "languages_proto", + srcs = ["languages.proto"], + visibility = ["//:oppia_api_visibility"], +) + +java_lite_proto_library( + name = "languages_java_proto_lite", + visibility = ["//:oppia_api_visibility"], + deps = [":languages_proto"], +) + +oppia_proto_library( + name = "onboarding_proto", + srcs = ["onboarding.proto"], +) + +java_lite_proto_library( + name = "onboarding_java_proto_lite", + visibility = ["//:oppia_api_visibility"], + deps = [":onboarding_proto"], +) + +oppia_proto_library( + name = "profile_proto", + srcs = ["profile.proto"], +) + +java_lite_proto_library( + name = "profile_java_proto_lite", + visibility = ["//:oppia_api_visibility"], + deps = [":profile_proto"], +) + +oppia_proto_library( + name = "subtitled_html_proto", + srcs = ["subtitled_html.proto"], + visibility = ["//:oppia_api_visibility"], +) + +java_lite_proto_library( + name = "subtitled_html_java_proto_lite", + visibility = ["//:oppia_api_visibility"], + deps = [":subtitled_html_proto"], +) + +oppia_proto_library( + name = "subtitled_unicode_proto", + srcs = ["subtitled_unicode.proto"], + visibility = ["//:oppia_api_visibility"], +) + +java_lite_proto_library( + name = "subtitled_unicode_java_proto_lite", + visibility = ["//:oppia_api_visibility"], + deps = [":subtitled_unicode_proto"], +) + +oppia_proto_library( + name = "test_proto", + srcs = ["test.proto"], +) + +java_lite_proto_library( + name = "test_java_proto_lite", + deps = [":test_proto"], +) + +oppia_proto_library( + name = "thumbnail_proto", + srcs = ["thumbnail.proto"], +) + +java_lite_proto_library( + name = "thumbnail_java_proto_lite", + visibility = ["//:oppia_api_visibility"], + deps = [":thumbnail_proto"], +) + +oppia_proto_library( + name = "translation_proto", + srcs = ["translation.proto"], + visibility = ["//:oppia_api_visibility"], +) + +java_lite_proto_library( + name = "translation_java_proto_lite", + visibility = ["//:oppia_api_visibility"], + deps = [":translation_proto"], +) + +oppia_proto_library( + name = "voiceover_proto", + srcs = ["voiceover.proto"], + visibility = ["//:oppia_api_visibility"], +) + +java_lite_proto_library( + name = "voiceover_java_proto_lite", + deps = [":voiceover_proto"], +) + +oppia_proto_library( + name = "feedback_reporting_proto", + srcs = ["feedback_reporting.proto"], + deps = [":profile_proto"], +) + +java_lite_proto_library( + name = "feedback_reporting_java_proto_lite", + deps = [":feedback_reporting_proto"], +) + +oppia_proto_library( + name = "question_proto", + srcs = ["question.proto"], + deps = [ + ":exploration_proto", + ":subtitled_html_proto", + ":subtitled_unicode_proto", + ], +) + +java_lite_proto_library( + name = "question_java_proto_lite", + visibility = ["//:oppia_api_visibility"], + deps = [":question_proto"], +) + +oppia_proto_library( + name = "topic_proto", + srcs = ["topic.proto"], + visibility = ["//:oppia_api_visibility"], + deps = [ + ":subtitled_html_proto", + ":subtitled_unicode_proto", + ":thumbnail_proto", + ":translation_proto", + ":voiceover_proto", + ], +) + +java_lite_proto_library( + name = "topic_java_proto_lite", + visibility = ["//:oppia_api_visibility"], + deps = [":topic_proto"], +) + +oppia_proto_library( + name = "exploration_proto", + srcs = ["exploration.proto"], + visibility = ["//:oppia_api_visibility"], + deps = [ + ":interaction_object_proto", + ":subtitled_html_proto", + ":subtitled_unicode_proto", + ":translation_proto", + ":voiceover_proto", + ], +) + +java_lite_proto_library( + name = "exploration_java_proto_lite", + visibility = ["//:oppia_api_visibility"], + deps = [":exploration_proto"], +) + +oppia_proto_library( + name = "platform_parameter_proto", + srcs = ["platform_parameter.proto"], +) + +java_lite_proto_library( + name = "platform_parameter_java_proto_lite", + visibility = ["//:oppia_api_visibility"], + deps = [":platform_parameter_proto"], +) + +android_library( + name = "test_models", + testonly = True, + visibility = ["//:oppia_api_visibility"], + exports = [ + ":test_java_proto_lite", + ], +) diff --git a/model/src/main/proto/format_import_proto_library.bzl b/model/src/main/proto/format_import_proto_library.bzl deleted file mode 100644 index 77ab61f0fdf..00000000000 --- a/model/src/main/proto/format_import_proto_library.bzl +++ /dev/null @@ -1,44 +0,0 @@ -""" -Container for macros to fix proto files. -""" - -load("@rules_proto//proto:defs.bzl", "proto_library") - -def format_import_proto_library(name, src, deps = [], **kwargs): - """ - Creates a new proto library with corrected imports. - - This macro exists as a way to build proto files that contain import statements in both Gradle - and Bazel. This macro formats the src file's import statements to contain a full path to the - file in order for Bazel to properly locate file. - - Args: - name: str. The name of the .proto file without the '.proto' suffix. This will be the root for - the name of the proto library created. Ex: If name = 'topic', then the src file is - 'topic.proto' and the proto library created will be named 'topic_proto'. - src: str. The name of the .proto file to be built into a proto_library. - deps: list of str. The list of dependencies needed to build the src file. This list will - contain all of the proto_library targets for the files imported into src. - **kwargs: additional parameters passed in. - """ - - # TODO(#1543): Ensure this function works on Windows systems. - # TODO(#1617): Remove genrules post-gradle - native.genrule( - name = name, - srcs = [src], - outs = ["processed_" + src], - cmd = """ - cat $< | - sed 's/import "/import "model\\/src\\/main\\/proto\\//g' | - sed 's/"model\\/src\\/main\\/proto\\/exploration/"model\\/processed_src\\/main\\/proto\\/exploration/g' | - sed 's/"model\\/src\\/main\\/proto\\/topic/"model\\/processed_src\\/main\\/proto\\/topic/g' | - sed 's/"model\\/src\\/main\\/proto\\/question/"model\\/processed_src\\/main\\/proto\\/question/g' > $@ - """, - ) - proto_library( - name = name + "_proto", - srcs = ["processed_" + src], - deps = deps, - **kwargs - ) diff --git a/model/text_proto_assets.bzl b/model/text_proto_assets.bzl index 326dd00933a..ace61c6a81b 100644 --- a/model/text_proto_assets.bzl +++ b/model/text_proto_assets.bzl @@ -29,9 +29,11 @@ def _gen_binary_proto_from_text_impl(ctx): # proto to binary, and expected stdin/stdout configurations. Note that the actual proto files # are passed to the compiler since it requires them in order to transcode the text proto file. command_path = ctx.executable._protoc_tool.path + proto_directory_path_args = ["--proto_path=%s" % file.dirname for file in input_proto_files] + proto_file_names = [file.basename for file in input_proto_files] arguments = [command_path] + [ "--encode %s" % ctx.attr.proto_type_name, - ] + [file.path for file in input_proto_files] + [ + ] + proto_directory_path_args + proto_file_names + [ "< %s" % input_file, "> %s" % output_file.path, ] diff --git a/scripts/script_assets.bzl b/scripts/script_assets.bzl index b5c3fb389e4..363455fb553 100644 --- a/scripts/script_assets.bzl +++ b/scripts/script_assets.bzl @@ -24,7 +24,7 @@ def generate_regex_assets_list_from_text_protos( names = filepath_pattern_validation_file_names, proto_dep_name = "filename_pattern_validation_checks", proto_type_name = "FilenameChecks", - name_prefix = name, + name_prefix = "filename_checks", asset_dir = "assets", proto_dep_bazel_target_prefix = "//scripts/src/java/org/oppia/android/scripts/proto", proto_package = "proto", @@ -33,7 +33,7 @@ def generate_regex_assets_list_from_text_protos( names = file_content_validation_file_names, proto_dep_name = "file_content_validation_checks", proto_type_name = "FileContentChecks", - name_prefix = name, + name_prefix = "file_content_checks", asset_dir = "assets", proto_dep_bazel_target_prefix = "//scripts/src/java/org/oppia/android/scripts/proto", proto_package = "proto", @@ -57,7 +57,7 @@ def generate_test_file_assets_list_from_text_protos( names = test_file_exemptions_name, proto_dep_name = "script_exemptions", proto_type_name = "TestFileExemptions", - name_prefix = name, + name_prefix = "test_file_exemptions", asset_dir = "assets", proto_dep_bazel_target_prefix = "//scripts/src/java/org/oppia/android/scripts/proto", proto_package = "proto", @@ -82,7 +82,7 @@ def generate_maven_assets_list_from_text_protos( names = maven_dependency_filenames, proto_dep_name = "maven_dependencies", proto_type_name = "MavenDependencyList", - name_prefix = name, + name_prefix = "maven_dependency_list", asset_dir = "assets", proto_dep_bazel_target_prefix = "//scripts/src/java/org/oppia/android/scripts/proto", proto_package = "proto", @@ -107,7 +107,7 @@ def generate_accessibility_label_assets_list_from_text_protos( names = accessibility_label_exemptions_name, proto_dep_name = "script_exemptions", proto_type_name = "AccessibilityLabelExemptions", - name_prefix = name, + name_prefix = "accessibility_label_exemptions", asset_dir = "assets", proto_dep_bazel_target_prefix = "//scripts/src/java/org/oppia/android/scripts/proto", proto_package = "proto", @@ -131,7 +131,7 @@ def generate_kdoc_validity_assets_list_from_text_protos( names = kdoc_validity_exemptions_name, proto_dep_name = "script_exemptions", proto_type_name = "KdocValidityExemptions", - name_prefix = name, + name_prefix = "kdoc_validity_exemptions", asset_dir = "assets", proto_dep_bazel_target_prefix = "//scripts/src/java/org/oppia/android/scripts/proto", proto_package = "proto", @@ -155,7 +155,7 @@ def generate_todo_assets_list_from_text_protos( names = todo_exemptions_name, proto_dep_name = "script_exemptions", proto_type_name = "TodoOpenExemptions", - name_prefix = name, + name_prefix = "todo_open_exemptions", asset_dir = "assets", proto_dep_bazel_target_prefix = "//scripts/src/java/org/oppia/android/scripts/proto", proto_package = "proto", diff --git a/scripts/src/javatests/org/oppia/android/scripts/common/BUILD.bazel b/scripts/src/javatests/org/oppia/android/scripts/common/BUILD.bazel index b4559459055..e47fc741ab6 100644 --- a/scripts/src/javatests/org/oppia/android/scripts/common/BUILD.bazel +++ b/scripts/src/javatests/org/oppia/android/scripts/common/BUILD.bazel @@ -43,7 +43,7 @@ kt_jvm_test( name = "ProtoStringEncoderTest", srcs = ["ProtoStringEncoderTest.kt"], deps = [ - "//model:test_models", + "//model/src/main/proto:test_models", "//scripts/src/java/org/oppia/android/scripts/common:proto_string_encoder", "//testing:assertion_helpers", "//third_party:com_google_truth_extensions_truth-liteproto-extension", diff --git a/testing/src/main/java/org/oppia/android/testing/junit/BUILD.bazel b/testing/src/main/java/org/oppia/android/testing/junit/BUILD.bazel index e2c54937b89..e8d62702d99 100644 --- a/testing/src/main/java/org/oppia/android/testing/junit/BUILD.bazel +++ b/testing/src/main/java/org/oppia/android/testing/junit/BUILD.bazel @@ -25,7 +25,7 @@ kt_android_library( ":define_app_language_locale_context", "//domain/src/main/java/org/oppia/android/domain/locale:locale_application_injector", "//domain/src/main/java/org/oppia/android/domain/locale:locale_application_injector_provider", - "//model:languages_java_proto_lite", + "//model/src/main/proto:languages_java_proto_lite", "//third_party:androidx_test_core", "//third_party:junit_junit", ], diff --git a/testing/src/test/java/org/oppia/android/testing/data/BUILD.bazel b/testing/src/test/java/org/oppia/android/testing/data/BUILD.bazel index 4b84736203b..bfc5a1ab0ac 100644 --- a/testing/src/test/java/org/oppia/android/testing/data/BUILD.bazel +++ b/testing/src/test/java/org/oppia/android/testing/data/BUILD.bazel @@ -16,7 +16,7 @@ oppia_android_test( ":dagger", "//domain/src/main/java/org/oppia/android/domain/locale:locale_controller", "//domain/src/main/java/org/oppia/android/domain/translation:translation_controller", - "//model:languages_java_proto_lite", + "//model/src/main/proto:languages_java_proto_lite", "//testing", "//testing/src/main/java/org/oppia/android/testing/data:data_provider_test_monitor", "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module", diff --git a/utility/BUILD.bazel b/utility/BUILD.bazel index 98a35c2ae8f..99cc8627c08 100644 --- a/utility/BUILD.bazel +++ b/utility/BUILD.bazel @@ -54,8 +54,8 @@ kt_android_library( ":resources", "//app:crashlytics", "//app:crashlytics_deps", - "//model:event_logger_java_proto_lite", - "//model:platform_parameter_java_proto_lite", + "//model/src/main/proto:event_logger_java_proto_lite", + "//model/src/main/proto:platform_parameter_java_proto_lite", "//third_party:androidx_appcompat_appcompat", "//third_party:androidx_room_room-runtime", "//third_party:androidx_work_work-runtime", @@ -91,7 +91,7 @@ TEST_DEPS = [ ":utility", "//app:crashlytics", "//app:crashlytics_deps", - "//model:test_models", + "//model/src/main/proto:test_models", "//testing", "//testing/src/main/java/org/oppia/android/testing/mockito", "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module", diff --git a/utility/src/main/java/org/oppia/android/util/locale/BUILD.bazel b/utility/src/main/java/org/oppia/android/util/locale/BUILD.bazel index 1522fe1a28a..0c57bde04d0 100644 --- a/utility/src/main/java/org/oppia/android/util/locale/BUILD.bazel +++ b/utility/src/main/java/org/oppia/android/util/locale/BUILD.bazel @@ -36,7 +36,7 @@ kt_android_library( visibility = ["//:oppia_api_visibility"], deps = [ ":oppia_locale_context_extensions", - "//model:languages_java_proto_lite", + "//model/src/main/proto:languages_java_proto_lite", "//third_party:androidx_annotation_annotation", ], ) @@ -63,7 +63,7 @@ kt_android_library( "OppiaLocaleContextExtensions.kt", ], deps = [ - "//model:languages_java_proto_lite", + "//model/src/main/proto:languages_java_proto_lite", ], ) diff --git a/utility/src/main/java/org/oppia/android/util/logging/BUILD.bazel b/utility/src/main/java/org/oppia/android/util/logging/BUILD.bazel index 3ea1f8bbe89..21b2fa9154c 100644 --- a/utility/src/main/java/org/oppia/android/util/logging/BUILD.bazel +++ b/utility/src/main/java/org/oppia/android/util/logging/BUILD.bazel @@ -39,7 +39,7 @@ kt_android_library( ], visibility = ["//:oppia_api_visibility"], deps = [ - "//model:event_logger_java_proto_lite", + "//model/src/main/proto:event_logger_java_proto_lite", ], ) @@ -50,7 +50,7 @@ kt_android_library( ], visibility = ["//:oppia_api_visibility"], deps = [ - "//model:event_logger_java_proto_lite", + "//model/src/main/proto:event_logger_java_proto_lite", ], ) diff --git a/utility/src/main/java/org/oppia/android/util/logging/firebase/BUILD.bazel b/utility/src/main/java/org/oppia/android/util/logging/firebase/BUILD.bazel index 3a69f05bcf2..7c5e92b2d2d 100644 --- a/utility/src/main/java/org/oppia/android/util/logging/firebase/BUILD.bazel +++ b/utility/src/main/java/org/oppia/android/util/logging/firebase/BUILD.bazel @@ -23,7 +23,7 @@ kt_android_library( "FirebaseLogUploader.kt", ], deps = [ - "//model:event_logger_java_proto_lite", + "//model/src/main/proto:event_logger_java_proto_lite", "//third_party:androidx_work_work-runtime", "//third_party:androidx_work_work-runtime-ktx", "//third_party:com_google_firebase_firebase-analytics", @@ -62,7 +62,7 @@ kt_android_library( "//app:__pkg__", ], deps = [ - "//model:event_logger_java_proto_lite", + "//model/src/main/proto:event_logger_java_proto_lite", "//third_party:javax_inject_javax_inject", "//utility/src/main/java/org/oppia/android/util/logging:event_logger", ], diff --git a/utility/src/test/java/org/oppia/android/util/caching/testing/BUILD.bazel b/utility/src/test/java/org/oppia/android/util/caching/testing/BUILD.bazel index a4187f0dede..f9cf0e1a190 100644 --- a/utility/src/test/java/org/oppia/android/util/caching/testing/BUILD.bazel +++ b/utility/src/test/java/org/oppia/android/util/caching/testing/BUILD.bazel @@ -31,7 +31,7 @@ oppia_android_test( test_manifest = "//utility:test_manifest", deps = [ ":dagger", - "//model:test_models", + "//model/src/main/proto:test_models", "//testing", "//third_party:androidx_test_ext_junit", "//third_party:com_google_truth_extensions_truth-liteproto-extension", diff --git a/utility/src/test/java/org/oppia/android/util/locale/BUILD.bazel b/utility/src/test/java/org/oppia/android/util/locale/BUILD.bazel index 43c6ea60c0c..075455e6297 100644 --- a/utility/src/test/java/org/oppia/android/util/locale/BUILD.bazel +++ b/utility/src/test/java/org/oppia/android/util/locale/BUILD.bazel @@ -87,7 +87,7 @@ oppia_android_test( test_manifest = "//utility:test_manifest", deps = [ ":dagger", - "//model:languages_java_proto_lite", + "//model/src/main/proto:languages_java_proto_lite", "//third_party:androidx_test_ext_junit", "//third_party:com_google_truth_extensions_truth-liteproto-extension", "//third_party:junit_junit", From fe73a2f8083dbd48ab207068a7931ff4733bd9f4 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Tue, 14 Dec 2021 17:34:44 -0800 Subject: [PATCH 002/162] Introduce math.proto & refactor math extensions. Much of this is copied from #2173. --- ...atioExpressionInputInteractionViewModel.kt | 2 +- domain/BUILD.bazel | 1 + ...AndInSimplestFormRuleClassifierProvider.kt | 6 +++--- ...putIsEquivalentToRuleClassifierProvider.kt | 4 ++-- ...nputIsGreaterThanRuleClassifierProvider.kt | 2 +- ...onInputIsLessThanRuleClassifierProvider.kt | 2 +- ...ithUnitsIsEqualToRuleClassifierProvider.kt | 2 +- ...itsIsEquivalentToRuleClassifierProvider.kt | 4 ++-- ...umericInputEqualsRuleClassifierProvider.kt | 2 +- ...InputIsEquivalentRuleClassifierProvider.kt | 2 +- .../org/oppia/android/domain/util/BUILD.bazel | 4 +--- .../util/InteractionObjectExtensions.kt | 1 + model/src/main/proto/BUILD.bazel | 13 ++++++++++++ model/src/main/proto/interaction_object.proto | 17 ++------------- model/src/main/proto/math.proto | 21 +++++++++++++++++++ utility/BUILD.bazel | 1 + .../org/oppia/android/util/math/BUILD.bazel | 20 ++++++++++++++++++ .../android/util/math}/FloatExtensions.kt | 4 ++-- .../android/util/math}/FractionExtensions.kt | 2 +- .../android/util/math}/RatioExtensions.kt | 2 +- 20 files changed, 77 insertions(+), 35 deletions(-) create mode 100644 model/src/main/proto/math.proto create mode 100644 utility/src/main/java/org/oppia/android/util/math/BUILD.bazel rename {domain/src/main/java/org/oppia/android/domain/util => utility/src/main/java/org/oppia/android/util/math}/FloatExtensions.kt (86%) rename {domain/src/main/java/org/oppia/android/domain/util => utility/src/main/java/org/oppia/android/util/math}/FractionExtensions.kt (96%) rename {domain/src/main/java/org/oppia/android/domain/util => utility/src/main/java/org/oppia/android/util/math}/RatioExtensions.kt (94%) diff --git a/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/RatioExpressionInputInteractionViewModel.kt b/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/RatioExpressionInputInteractionViewModel.kt index 064b7fc3f60..6f916b0f4d0 100644 --- a/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/RatioExpressionInputInteractionViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/RatioExpressionInputInteractionViewModel.kt @@ -16,7 +16,7 @@ import org.oppia.android.app.player.state.answerhandling.InteractionAnswerHandle import org.oppia.android.app.translation.AppLanguageResourceHandler import org.oppia.android.app.utility.toAccessibleAnswerString import org.oppia.android.domain.translation.TranslationController -import org.oppia.android.domain.util.toAnswerString +import org.oppia.android.util.math.toAnswerString /** [StateItemViewModel] for the ratio expression input interaction. */ class RatioExpressionInputInteractionViewModel( diff --git a/domain/BUILD.bazel b/domain/BUILD.bazel index 503c519682e..cf4b8098c8e 100755 --- a/domain/BUILD.bazel +++ b/domain/BUILD.bazel @@ -124,6 +124,7 @@ kt_android_library( "//utility/src/main/java/org/oppia/android/util/extensions:context_extensions", "//utility/src/main/java/org/oppia/android/util/logging:event_logger", "//utility/src/main/java/org/oppia/android/util/logging:log_uploader", + "//utility/src/main/java/org/oppia/android/util/math:extensions", "//utility/src/main/java/org/oppia/android/util/networking:network_connection_util", "//utility/src/main/java/org/oppia/android/util/profile:directory_management_util", ], diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProvider.kt index 76ed9cb0f72..598d7a71ad1 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProvider.kt @@ -6,9 +6,9 @@ import org.oppia.android.app.model.WrittenTranslationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider -import org.oppia.android.domain.util.approximatelyEquals -import org.oppia.android.domain.util.toFloat -import org.oppia.android.domain.util.toSimplestForm +import org.oppia.android.util.math.approximatelyEquals +import org.oppia.android.util.math.toFloat +import org.oppia.android.util.math.toSimplestForm import javax.inject.Inject /** diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToRuleClassifierProvider.kt index d7f8c597461..0c47a5d0657 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToRuleClassifierProvider.kt @@ -6,8 +6,8 @@ import org.oppia.android.app.model.WrittenTranslationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider -import org.oppia.android.domain.util.approximatelyEquals -import org.oppia.android.domain.util.toFloat +import org.oppia.android.util.math.approximatelyEquals +import org.oppia.android.util.math.toFloat import javax.inject.Inject /** diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsGreaterThanRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsGreaterThanRuleClassifierProvider.kt index 740ab078eee..a44dedfcfdf 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsGreaterThanRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsGreaterThanRuleClassifierProvider.kt @@ -6,7 +6,7 @@ import org.oppia.android.app.model.WrittenTranslationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider -import org.oppia.android.domain.util.toFloat +import org.oppia.android.util.math.toFloat import javax.inject.Inject /** diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsLessThanRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsLessThanRuleClassifierProvider.kt index 3fbf98e8fac..52d1396d9f1 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsLessThanRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsLessThanRuleClassifierProvider.kt @@ -6,7 +6,7 @@ import org.oppia.android.app.model.WrittenTranslationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider -import org.oppia.android.domain.util.toFloat +import org.oppia.android.util.math.toFloat import javax.inject.Inject /** diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEqualToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEqualToRuleClassifierProvider.kt index e8375af7173..9a225cc41ca 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEqualToRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEqualToRuleClassifierProvider.kt @@ -7,7 +7,7 @@ import org.oppia.android.app.model.WrittenTranslationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider -import org.oppia.android.domain.util.approximatelyEquals +import org.oppia.android.util.math.approximatelyEquals import javax.inject.Inject /** diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEquivalentToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEquivalentToRuleClassifierProvider.kt index daab9473b4e..99bad528bb4 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEquivalentToRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEquivalentToRuleClassifierProvider.kt @@ -6,8 +6,8 @@ import org.oppia.android.app.model.WrittenTranslationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider -import org.oppia.android.domain.util.approximatelyEquals -import org.oppia.android.domain.util.toFloat +import org.oppia.android.util.math.approximatelyEquals +import org.oppia.android.util.math.toFloat import javax.inject.Inject /** diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputEqualsRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputEqualsRuleClassifierProvider.kt index f5c84525281..2c7a6dc5212 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputEqualsRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputEqualsRuleClassifierProvider.kt @@ -5,7 +5,7 @@ import org.oppia.android.app.model.WrittenTranslationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider -import org.oppia.android.domain.util.approximatelyEquals +import org.oppia.android.util.math.approximatelyEquals import javax.inject.Inject /** diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputIsEquivalentRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputIsEquivalentRuleClassifierProvider.kt index 57aba25a07c..f9b9b2e9df8 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputIsEquivalentRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputIsEquivalentRuleClassifierProvider.kt @@ -6,7 +6,7 @@ import org.oppia.android.app.model.WrittenTranslationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider -import org.oppia.android.domain.util.toSimplestForm +import org.oppia.android.util.math.toSimplestForm import javax.inject.Inject /** diff --git a/domain/src/main/java/org/oppia/android/domain/util/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/util/BUILD.bazel index 26dc7fb7027..7c1dc1e6ce2 100644 --- a/domain/src/main/java/org/oppia/android/domain/util/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/util/BUILD.bazel @@ -21,11 +21,8 @@ kt_android_library( kt_android_library( name = "extensions", srcs = [ - "FloatExtensions.kt", - "FractionExtensions.kt", "InteractionObjectExtensions.kt", "JsonExtensions.kt", - "RatioExtensions.kt", "StringExtensions.kt", "WorkDataExtensions.kt", ], @@ -33,6 +30,7 @@ kt_android_library( deps = [ "//model/src/main/proto:question_java_proto_lite", "//third_party:androidx_work_work-runtime-ktx", + "//utility/src/main/java/org/oppia/android/util/math:extensions", ], ) diff --git a/domain/src/main/java/org/oppia/android/domain/util/InteractionObjectExtensions.kt b/domain/src/main/java/org/oppia/android/domain/util/InteractionObjectExtensions.kt index a4d813c6ec8..2e37e34e0f7 100644 --- a/domain/src/main/java/org/oppia/android/domain/util/InteractionObjectExtensions.kt +++ b/domain/src/main/java/org/oppia/android/domain/util/InteractionObjectExtensions.kt @@ -29,6 +29,7 @@ import org.oppia.android.app.model.SetOfTranslatableHtmlContentIds import org.oppia.android.app.model.StringList import org.oppia.android.app.model.TranslatableHtmlContentId import org.oppia.android.app.model.TranslatableSetOfNormalizedString +import org.oppia.android.util.math.toAnswerString /** * Returns a parsable string representation of a user-submitted answer version of this diff --git a/model/src/main/proto/BUILD.bazel b/model/src/main/proto/BUILD.bazel index 90da6e48b77..f28661f7341 100644 --- a/model/src/main/proto/BUILD.bazel +++ b/model/src/main/proto/BUILD.bazel @@ -61,6 +61,7 @@ oppia_proto_library( name = "interaction_object_proto", srcs = ["interaction_object.proto"], visibility = ["//:oppia_api_visibility"], + deps = [":math_proto"], ) java_lite_proto_library( @@ -81,6 +82,18 @@ java_lite_proto_library( deps = [":languages_proto"], ) +oppia_proto_library( + name = "math_proto", + srcs = ["math.proto"], + strip_import_prefix = "", +) + +java_lite_proto_library( + name = "math_java_proto_lite", + visibility = ["//:oppia_api_visibility"], + deps = [":math_proto"], +) + oppia_proto_library( name = "onboarding_proto", srcs = ["onboarding.proto"], diff --git a/model/src/main/proto/interaction_object.proto b/model/src/main/proto/interaction_object.proto index c444f316431..bb6154f9255 100644 --- a/model/src/main/proto/interaction_object.proto +++ b/model/src/main/proto/interaction_object.proto @@ -2,6 +2,8 @@ syntax = "proto3"; package model; +import "math.proto"; + option java_package = "org.oppia.android.app.model"; option java_multiple_files = true; @@ -35,13 +37,6 @@ message StringList { repeated string html = 1; } -// Structure containing a ratio object for eg - [1,2,3] for 1:2:3. -message RatioExpression { - // List of components in a ratio. It's expected that list should have more than - // 1 element. - repeated uint32 ratio_component = 1; -} - // Structure for a number with units object. message NumberWithUnits { oneof number_type { @@ -57,14 +52,6 @@ message NumberUnit { int32 exponent = 2; } -// Structure for a fraction object. -message Fraction { - bool is_negative = 1; - int32 whole_number = 2; - int32 numerator = 3; - int32 denominator = 4; -} - // Structure for a ListOfString object. message ListOfSetsOfHtmlStrings { repeated StringList set_of_html_strings = 1; diff --git a/model/src/main/proto/math.proto b/model/src/main/proto/math.proto new file mode 100644 index 00000000000..0288db3148b --- /dev/null +++ b/model/src/main/proto/math.proto @@ -0,0 +1,21 @@ +syntax = "proto3"; + +package model; + +option java_package = "org.oppia.android.app.model"; +option java_multiple_files = true; + +// Structure for a fraction object. +message Fraction { + bool is_negative = 1; + int32 whole_number = 2; + int32 numerator = 3; + int32 denominator = 4; +} + +// Structure containing a ratio object for eg - [1,2,3] for 1:2:3. +message RatioExpression { + // List of components in a ratio. It's expected that list should have more than + // 1 element. + repeated uint32 ratio_component = 1; +} diff --git a/utility/BUILD.bazel b/utility/BUILD.bazel index 99cc8627c08..513122f87ef 100644 --- a/utility/BUILD.bazel +++ b/utility/BUILD.bazel @@ -19,6 +19,7 @@ MIGRATED_PROD_FILES = glob([ "src/main/java/org/oppia/android/util/extensions/*.kt", "src/main/java/org/oppia/android/util/gcsresource/*.kt", "src/main/java/org/oppia/android/util/logging/*.kt", + "src/main/java/org/oppia/android/util/math/**/*.kt", "src/main/java/org/oppia/android/util/networking/*.kt", "src/main/java/org/oppia/android/util/profile/*.kt", "src/main/java/org/oppia/android/util/statusbar/*.kt", diff --git a/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel b/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel new file mode 100644 index 00000000000..4b84961d297 --- /dev/null +++ b/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel @@ -0,0 +1,20 @@ +""" +TODO: document +""" + +load("@io_bazel_rules_kotlin//kotlin:kotlin.bzl", "kt_android_library") + +kt_android_library( + name = "extensions", + srcs = [ + "FloatExtensions.kt", + "FractionExtensions.kt", + "RatioExtensions.kt", + ], + visibility = [ + "//:oppia_api_visibility", + ], + deps = [ + "//model/src/main/proto:math_java_proto_lite", + ], +) diff --git a/domain/src/main/java/org/oppia/android/domain/util/FloatExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/FloatExtensions.kt similarity index 86% rename from domain/src/main/java/org/oppia/android/domain/util/FloatExtensions.kt rename to utility/src/main/java/org/oppia/android/util/math/FloatExtensions.kt index 5ce8d4b0c10..62504046c78 100644 --- a/domain/src/main/java/org/oppia/android/domain/util/FloatExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/FloatExtensions.kt @@ -1,9 +1,9 @@ -package org.oppia.android.domain.util +package org.oppia.android.util.math import kotlin.math.abs /** The error margin used for float equality by [Float.approximatelyEquals]. */ -public const val FLOAT_EQUALITY_INTERVAL = 1e-5 +const val FLOAT_EQUALITY_INTERVAL = 1e-5 /** Returns whether this float approximately equals another based on a consistent epsilon value. */ fun Float.approximatelyEquals(other: Float): Boolean { diff --git a/domain/src/main/java/org/oppia/android/domain/util/FractionExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt similarity index 96% rename from domain/src/main/java/org/oppia/android/domain/util/FractionExtensions.kt rename to utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt index 878576da012..69b57d5be39 100644 --- a/domain/src/main/java/org/oppia/android/domain/util/FractionExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt @@ -1,4 +1,4 @@ -package org.oppia.android.domain.util +package org.oppia.android.util.math import org.oppia.android.app.model.Fraction diff --git a/domain/src/main/java/org/oppia/android/domain/util/RatioExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/RatioExtensions.kt similarity index 94% rename from domain/src/main/java/org/oppia/android/domain/util/RatioExtensions.kt rename to utility/src/main/java/org/oppia/android/util/math/RatioExtensions.kt index 821fa274e31..123a24e2958 100644 --- a/domain/src/main/java/org/oppia/android/domain/util/RatioExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/RatioExtensions.kt @@ -1,4 +1,4 @@ -package org.oppia.android.domain.util +package org.oppia.android.util.math import org.oppia.android.app.model.RatioExpression From d17e3dc68fb98dc2c0b666194145afa397a5a09a Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Tue, 14 Dec 2021 17:49:47 -0800 Subject: [PATCH 003/162] Migrate tests & remove unneeded prefix. --- model/src/main/proto/BUILD.bazel | 1 - .../org/oppia/android/util/math/BUILD.bazel | 22 +++++++++++++++++++ .../android/util/math}/RatioExtensionsTest.kt | 2 +- 3 files changed, 23 insertions(+), 2 deletions(-) create mode 100644 utility/src/test/java/org/oppia/android/util/math/BUILD.bazel rename {domain/src/test/java/org/oppia/android/domain/util => utility/src/test/java/org/oppia/android/util/math}/RatioExtensionsTest.kt (97%) diff --git a/model/src/main/proto/BUILD.bazel b/model/src/main/proto/BUILD.bazel index f28661f7341..a88f7ec5ed0 100644 --- a/model/src/main/proto/BUILD.bazel +++ b/model/src/main/proto/BUILD.bazel @@ -85,7 +85,6 @@ java_lite_proto_library( oppia_proto_library( name = "math_proto", srcs = ["math.proto"], - strip_import_prefix = "", ) java_lite_proto_library( diff --git a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel new file mode 100644 index 00000000000..493d89d66a0 --- /dev/null +++ b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel @@ -0,0 +1,22 @@ +""" +TODO: document +""" + +load("//:oppia_android_test.bzl", "oppia_android_test") + +oppia_android_test( + name = "RatioExtensionsTest", + srcs = ["RatioExtensionsTest.kt"], + custom_package = "org.oppia.android.util.math", + test_class = "org.oppia.android.util.math.RatioExtensionsTest", + test_manifest = "//utility:test_manifest", + deps = [ + "//model/src/main/proto:math_java_proto_lite", + "//third_party:androidx_test_ext_junit", + "//third_party:com_google_truth_truth", + "//third_party:junit_junit", + "//third_party:org_robolectric_robolectric", + "//third_party:robolectric_android-all", + "//utility/src/main/java/org/oppia/android/util/math:extensions", + ], +) diff --git a/domain/src/test/java/org/oppia/android/domain/util/RatioExtensionsTest.kt b/utility/src/test/java/org/oppia/android/util/math/RatioExtensionsTest.kt similarity index 97% rename from domain/src/test/java/org/oppia/android/domain/util/RatioExtensionsTest.kt rename to utility/src/test/java/org/oppia/android/util/math/RatioExtensionsTest.kt index dffb8e65b2f..ca380220b0b 100644 --- a/domain/src/test/java/org/oppia/android/domain/util/RatioExtensionsTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/RatioExtensionsTest.kt @@ -1,4 +1,4 @@ -package org.oppia.android.domain.util +package org.oppia.android.util.math import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat From fb61e39bf1f598946e534a2d77a8c456d07f149a Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Tue, 14 Dec 2021 17:55:46 -0800 Subject: [PATCH 004/162] Add needed newline. --- .../src/main/java/org/oppia/android/util/math/RatioExtensions.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/utility/src/main/java/org/oppia/android/util/math/RatioExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/RatioExtensions.kt index 123a24e2958..83d85e9098c 100644 --- a/utility/src/main/java/org/oppia/android/util/math/RatioExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/RatioExtensions.kt @@ -13,6 +13,7 @@ fun RatioExpression.toSimplestForm(): List { this.ratioComponentList.map { x -> x / gcdComponentResult } } } + /** * Returns this Ratio in string format. * E.g. [1, 2, 3] will yield to 1:2:3 From acab98bd74477a78a55605364df20b4ef029050f Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Tue, 14 Dec 2021 18:36:04 -0800 Subject: [PATCH 005/162] Some needed Fraction changes. --- ...tToAndInSimplestFormRuleClassifierProvider.kt | 5 +++-- ...nInputIsEquivalentToRuleClassifierProvider.kt | 4 ++-- ...onInputIsGreaterThanRuleClassifierProvider.kt | 4 ++-- ...ctionInputIsLessThanRuleClassifierProvider.kt | 4 ++-- ...hUnitsIsEquivalentToRuleClassifierProvider.kt | 4 ++-- .../android/util/math/FractionExtensions.kt | 16 +++++++++------- 6 files changed, 20 insertions(+), 17 deletions(-) diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProvider.kt index 598d7a71ad1..af698227520 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProvider.kt @@ -7,7 +7,7 @@ import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider import org.oppia.android.util.math.approximatelyEquals -import org.oppia.android.util.math.toFloat +import org.oppia.android.util.math.toDouble import org.oppia.android.util.math.toSimplestForm import javax.inject.Inject @@ -36,6 +36,7 @@ class FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProvider input: Fraction, writtenTranslationContext: WrittenTranslationContext ): Boolean { - return answer.toFloat().approximatelyEquals(input.toFloat()) && answer == input.toSimplestForm() + return answer.toDouble().approximatelyEquals(input.toDouble()) + && answer == input.toSimplestForm() } } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToRuleClassifierProvider.kt index 0c47a5d0657..e2c42f7ec67 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToRuleClassifierProvider.kt @@ -7,7 +7,7 @@ import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider import org.oppia.android.util.math.approximatelyEquals -import org.oppia.android.util.math.toFloat +import org.oppia.android.util.math.toDouble import javax.inject.Inject /** @@ -34,6 +34,6 @@ class FractionInputIsEquivalentToRuleClassifierProvider @Inject constructor( input: Fraction, writtenTranslationContext: WrittenTranslationContext ): Boolean { - return answer.toFloat().approximatelyEquals(input.toFloat()) + return answer.toDouble().approximatelyEquals(input.toDouble()) } } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsGreaterThanRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsGreaterThanRuleClassifierProvider.kt index a44dedfcfdf..89d83f1e3d6 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsGreaterThanRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsGreaterThanRuleClassifierProvider.kt @@ -6,7 +6,7 @@ import org.oppia.android.app.model.WrittenTranslationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider -import org.oppia.android.util.math.toFloat +import org.oppia.android.util.math.toDouble import javax.inject.Inject /** @@ -33,6 +33,6 @@ class FractionInputIsGreaterThanRuleClassifierProvider @Inject constructor( input: Fraction, writtenTranslationContext: WrittenTranslationContext ): Boolean { - return answer.toFloat() > input.toFloat() + return answer.toDouble() > input.toDouble() } } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsLessThanRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsLessThanRuleClassifierProvider.kt index 52d1396d9f1..02d4b9766c9 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsLessThanRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsLessThanRuleClassifierProvider.kt @@ -6,7 +6,7 @@ import org.oppia.android.app.model.WrittenTranslationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider -import org.oppia.android.util.math.toFloat +import org.oppia.android.util.math.toDouble import javax.inject.Inject /** @@ -33,6 +33,6 @@ class FractionInputIsLessThanRuleClassifierProvider @Inject constructor( input: Fraction, writtenTranslationContext: WrittenTranslationContext ): Boolean { - return answer.toFloat() < input.toFloat() + return answer.toDouble() < input.toDouble() } } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEquivalentToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEquivalentToRuleClassifierProvider.kt index 99bad528bb4..e94fc9191e7 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEquivalentToRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEquivalentToRuleClassifierProvider.kt @@ -7,7 +7,7 @@ import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider import org.oppia.android.util.math.approximatelyEquals -import org.oppia.android.util.math.toFloat +import org.oppia.android.util.math.toDouble import javax.inject.Inject /** @@ -47,7 +47,7 @@ class NumberWithUnitsIsEquivalentToRuleClassifierProvider @Inject constructor( private fun extractRealValue(number: NumberWithUnits): Double { return when (number.numberTypeCase) { NumberWithUnits.NumberTypeCase.REAL -> number.real - NumberWithUnits.NumberTypeCase.FRACTION -> number.fraction.toFloat().toDouble() + NumberWithUnits.NumberTypeCase.FRACTION -> number.fraction.toDouble() else -> throw IllegalArgumentException("Invalid number type: ${number.numberTypeCase.name}") } } diff --git a/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt index 69b57d5be39..d4a57faf9be 100644 --- a/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt @@ -3,14 +3,14 @@ package org.oppia.android.util.math import org.oppia.android.app.model.Fraction /** - * Returns a float version of this fraction. + * Returns a [Double] version of this fraction. * * See: https://github.com/oppia/oppia/blob/37285a/core/templates/dev/head/domain/objects/FractionObjectFactory.ts#L73. */ -fun Fraction.toFloat(): Float { - val totalParts = ((wholeNumber * denominator) + numerator).toFloat() - val floatVal = totalParts / denominator.toFloat() - return if (isNegative) -floatVal else floatVal +fun Fraction.toDouble(): Double { + val totalParts = ((wholeNumber.toDouble() * denominator.toDouble()) + numerator.toDouble()) + val doubleVal = totalParts / denominator.toDouble() + return if (isNegative) -doubleVal else doubleVal } /** @@ -20,8 +20,10 @@ fun Fraction.toFloat(): Float { */ fun Fraction.toSimplestForm(): Fraction { val commonDenominator = gcd(numerator, denominator) - return toBuilder().setNumerator(numerator / commonDenominator) - .setDenominator(denominator / commonDenominator).build() + return toBuilder().apply { + numerator = this@toSimplestForm.numerator / commonDenominator + denominator = this@toSimplestForm.denominator / commonDenominator + }.build() } /** Returns the greatest common divisor between two integers. */ From 02e930fb825149b24eda7eacfa16fa7fe0b4a131 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Tue, 14 Dec 2021 18:46:49 -0800 Subject: [PATCH 006/162] Introduce math expression + equation protos. Also adds testing libraries for both + fractions & reals (new structure). Most of this is copied from #2173. --- model/src/main/proto/math.proto | 77 +++++++ testing/BUILD.bazel | 1 + .../oppia/android/testing/math/BUILD.bazel | 75 +++++++ .../android/testing/math/FractionSubject.kt | 30 +++ .../testing/math/MathEquationSubject.kt | 21 ++ .../testing/math/MathExpressionSubject.kt | 206 ++++++++++++++++++ .../oppia/android/testing/math/RealSubject.kt | 41 ++++ 7 files changed, 451 insertions(+) create mode 100644 testing/src/main/java/org/oppia/android/testing/math/BUILD.bazel create mode 100644 testing/src/main/java/org/oppia/android/testing/math/FractionSubject.kt create mode 100644 testing/src/main/java/org/oppia/android/testing/math/MathEquationSubject.kt create mode 100644 testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt create mode 100644 testing/src/main/java/org/oppia/android/testing/math/RealSubject.kt diff --git a/model/src/main/proto/math.proto b/model/src/main/proto/math.proto index 0288db3148b..7dcbf780370 100644 --- a/model/src/main/proto/math.proto +++ b/model/src/main/proto/math.proto @@ -13,9 +13,86 @@ message Fraction { int32 denominator = 4; } +message Real { + oneof real_type { + Fraction rational = 1; + // Represents a decimal value. Technically these can sometimes be rational, but given IEEE-754 + // rounding errors we need to treat these values as irrational and non-factorable. + double irrational = 2; + int32 integer = 3; + } +} + // Structure containing a ratio object for eg - [1,2,3] for 1:2:3. message RatioExpression { // List of components in a ratio. It's expected that list should have more than // 1 element. repeated uint32 ratio_component = 1; } + +// Represents a mathematical expression such as 1+2. The only expression currently supported is a +// binary operation. +message MathExpression { + // TODO: document inclusive + int32 parse_start_index = 1; + // TODO: document exclusive + int32 parse_end_index = 2; + + oneof expression_type { + Real constant = 3; + string variable = 4; + MathBinaryOperation binary_operation = 5; + MathUnaryOperation unary_operation = 6; + MathFunctionCall function_call = 7; + MathExpression group = 8; + } +} + +message MathBinaryOperation { + enum Operator { + OPERATOR_UNSPECIFIED = 0; + // Represents adding two values, e.g.: 1+x. + ADD = 1; + // Represents subtracting two values, e.g.: x-2. + SUBTRACT = 2; + // Represents multiplying two values, e.g.: x*y. + MULTIPLY = 3; + // Represents dividing two values, e.g.: 1/x. + DIVIDE = 4; + // Represents taking the exponentiation of one value by another, e.g.: x^2. + EXPONENTIATE = 5; + } + + Operator operator = 1; + MathExpression left_operand = 2; + MathExpression right_operand = 3; + bool is_implicit = 4; +} + +message MathUnaryOperation { + enum Operator { + OPERATOR_UNSPECIFIED = 0; + // Represents negating a value, e.g.: -y. + NEGATE = 1; + // Represents indicating a value as positive, e.g.: +y. + POSITIVE = 2; + } + + Operator operator = 1; + MathExpression operand = 2; +} + +message MathFunctionCall { + enum FunctionType { + FUNCTION_UNSPECIFIED = 0; + SQUARE_ROOT = 1; + } + + FunctionType function_type = 1; + MathExpression argument = 2; +} + +message MathEquation { + MathExpression left_side = 1; + MathExpression right_side = 2; +} diff --git a/testing/BUILD.bazel b/testing/BUILD.bazel index 2d7d6122afc..87734adaad8 100644 --- a/testing/BUILD.bazel +++ b/testing/BUILD.bazel @@ -12,6 +12,7 @@ load("//testing:testing_test.bzl", "testing_test") # globs here to ensure new files added to migrated packages don't accidentally get included in the # top-level module library. MIGRATED_PROD_FILES = glob([ + "src/main/java/org/oppia/android/testing/math/*.kt", "src/main/java/org/oppia/android/testing/mockito/*.kt", "src/main/java/org/oppia/android/testing/networking/*.kt", "src/test/java/org/oppia/android/testing/platformparameter/*.kt", diff --git a/testing/src/main/java/org/oppia/android/testing/math/BUILD.bazel b/testing/src/main/java/org/oppia/android/testing/math/BUILD.bazel new file mode 100644 index 00000000000..a1453dd4496 --- /dev/null +++ b/testing/src/main/java/org/oppia/android/testing/math/BUILD.bazel @@ -0,0 +1,75 @@ +""" +TODO: document +""" + +load("@io_bazel_rules_kotlin//kotlin:kotlin.bzl", "kt_android_library") + +# TODO(#2747): Move these libraries to be under utility/.../math/testing. + +kt_android_library( + name = "fraction_subject", + testonly = True, + srcs = [ + "FractionSubject.kt", + ], + visibility = [ + "//:oppia_testing_visibility", + ], + deps = [ + "//model/src/main/proto:math_java_proto_lite", + "//third_party:com_google_truth_extensions_truth-liteproto-extension", + "//third_party:com_google_truth_truth", + "//utility/src/main/java/org/oppia/android/util/math:extensions", + ], +) + +kt_android_library( + name = "math_equation_subject", + testonly = True, + srcs = [ + "MathEquationSubject.kt", + ], + visibility = [ + "//:oppia_testing_visibility", + ], + deps = [ + ":math_expression_subject", + "//model/src/main/proto:math_java_proto_lite", + "//third_party:com_google_truth_extensions_truth-liteproto-extension", + "//third_party:com_google_truth_truth", + ], +) + +kt_android_library( + name = "math_expression_subject", + testonly = True, + srcs = [ + "MathExpressionSubject.kt", + ], + visibility = [ + "//:oppia_testing_visibility", + ], + deps = [ + ":real_subject", + "//model/src/main/proto:math_java_proto_lite", + "//third_party:com_google_truth_extensions_truth-liteproto-extension", + "//third_party:com_google_truth_truth", + ], +) + +kt_android_library( + name = "real_subject", + testonly = True, + srcs = [ + "RealSubject.kt", + ], + visibility = [ + "//:oppia_testing_visibility", + ], + deps = [ + ":fraction_subject", + "//model/src/main/proto:math_java_proto_lite", + "//third_party:com_google_truth_extensions_truth-liteproto-extension", + "//third_party:com_google_truth_truth", + ], +) diff --git a/testing/src/main/java/org/oppia/android/testing/math/FractionSubject.kt b/testing/src/main/java/org/oppia/android/testing/math/FractionSubject.kt new file mode 100644 index 00000000000..b256d7fd555 --- /dev/null +++ b/testing/src/main/java/org/oppia/android/testing/math/FractionSubject.kt @@ -0,0 +1,30 @@ +package org.oppia.android.testing.math + +import com.google.common.truth.BooleanSubject +import com.google.common.truth.DoubleSubject +import com.google.common.truth.FailureMetadata +import com.google.common.truth.IntegerSubject +import com.google.common.truth.Truth.assertAbout +import com.google.common.truth.Truth.assertThat +import com.google.common.truth.extensions.proto.LiteProtoSubject +import org.oppia.android.app.model.Fraction +import org.oppia.android.util.math.toDouble + +class FractionSubject( + metadata: FailureMetadata, + private val actual: Fraction +) : LiteProtoSubject(metadata, actual) { + fun hasNegativePropertyThat(): BooleanSubject = assertThat(actual.isNegative) + + fun hasWholeNumberThat(): IntegerSubject = assertThat(actual.wholeNumber) + + fun hasNumeratorThat(): IntegerSubject = assertThat(actual.numerator) + + fun hasDenominatorThat(): IntegerSubject = assertThat(actual.denominator) + + fun evaluatesToRealThat(): DoubleSubject = assertThat(actual.toDouble()) + + companion object { + fun assertThat(actual: Fraction): FractionSubject = assertAbout(::FractionSubject).that(actual) + } +} diff --git a/testing/src/main/java/org/oppia/android/testing/math/MathEquationSubject.kt b/testing/src/main/java/org/oppia/android/testing/math/MathEquationSubject.kt new file mode 100644 index 00000000000..ce24e1e08cc --- /dev/null +++ b/testing/src/main/java/org/oppia/android/testing/math/MathEquationSubject.kt @@ -0,0 +1,21 @@ +package org.oppia.android.testing.math + +import com.google.common.truth.FailureMetadata +import com.google.common.truth.Truth.assertAbout +import com.google.common.truth.extensions.proto.LiteProtoSubject +import org.oppia.android.app.model.MathEquation +import org.oppia.android.testing.math.MathExpressionSubject.Companion.assertThat + +class MathEquationSubject( + metadata: FailureMetadata, + private val actual: MathEquation +) : LiteProtoSubject(metadata, actual) { + fun hasLeftHandSideThat(): MathExpressionSubject = assertThat(actual.leftSide) + + fun hasRightHandSideThat(): MathExpressionSubject = assertThat(actual.rightSide) + + companion object { + fun assertThat(actual: MathEquation): MathEquationSubject = + assertAbout(::MathEquationSubject).that(actual) + } +} diff --git a/testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt b/testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt new file mode 100644 index 00000000000..c9be134e209 --- /dev/null +++ b/testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt @@ -0,0 +1,206 @@ +package org.oppia.android.testing.math + +import com.google.common.truth.FailureMetadata +import com.google.common.truth.StringSubject +import com.google.common.truth.Truth.assertAbout +import com.google.common.truth.Truth.assertThat +import com.google.common.truth.Truth.assertWithMessage +import com.google.common.truth.extensions.proto.LiteProtoSubject +import org.oppia.android.app.model.MathBinaryOperation +import org.oppia.android.app.model.MathExpression +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.BINARY_OPERATION +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.CONSTANT +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.FUNCTION_CALL +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.UNARY_OPERATION +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.VARIABLE +import org.oppia.android.app.model.MathFunctionCall +import org.oppia.android.app.model.MathUnaryOperation +import org.oppia.android.app.model.Real +import org.oppia.android.testing.math.RealSubject.Companion.assertThat + +// See: https://kotlinlang.org/docs/type-safe-builders.html. +class MathExpressionSubject( + metadata: FailureMetadata, + private val actual: MathExpression +) : LiteProtoSubject(metadata, actual) { + fun hasStructureThatMatches(init: ExpressionComparator.() -> Unit) { + // TODO: maybe verify that all aspects are verified? + ExpressionComparator.createFromExpression(actual).also(init) + } + + // TODO: update DSL to not have return values (since it's unnecessary). + @ExpressionComparatorMarker + class ExpressionComparator private constructor(private val expression: MathExpression) { + // TODO: convert to constant comparator? + fun constant(init: ConstantComparator.() -> Unit): ConstantComparator = + ConstantComparator.createFromExpression(expression).also(init) + + fun variable(init: VariableComparator.() -> Unit): VariableComparator = + VariableComparator.createFromExpression(expression).also(init) + + fun addition(init: BinaryOperationComparator.() -> Unit): BinaryOperationComparator { + return BinaryOperationComparator.createFromExpression( + expression, + expectedOperator = MathBinaryOperation.Operator.ADD + ).also(init) + } + + fun subtraction(init: BinaryOperationComparator.() -> Unit): BinaryOperationComparator { + return BinaryOperationComparator.createFromExpression( + expression, + expectedOperator = MathBinaryOperation.Operator.SUBTRACT + ).also(init) + } + + fun multiplication(init: BinaryOperationComparator.() -> Unit): BinaryOperationComparator { + return BinaryOperationComparator.createFromExpression( + expression, + expectedOperator = MathBinaryOperation.Operator.MULTIPLY + ).also(init) + } + + fun division(init: BinaryOperationComparator.() -> Unit): BinaryOperationComparator { + return BinaryOperationComparator.createFromExpression( + expression, + expectedOperator = MathBinaryOperation.Operator.DIVIDE + ).also(init) + } + + fun exponentiation(init: BinaryOperationComparator.() -> Unit): BinaryOperationComparator { + return BinaryOperationComparator.createFromExpression( + expression, + expectedOperator = MathBinaryOperation.Operator.EXPONENTIATE + ).also(init) + } + + fun negation(init: UnaryOperationComparator.() -> Unit): UnaryOperationComparator { + return UnaryOperationComparator.createFromExpression( + expression, + expectedOperator = MathUnaryOperation.Operator.NEGATE + ).also(init) + } + + fun positive(init: UnaryOperationComparator.() -> Unit): UnaryOperationComparator { + return UnaryOperationComparator.createFromExpression( + expression, + expectedOperator = MathUnaryOperation.Operator.POSITIVE + ).also(init) + } + + fun functionCallTo( + type: MathFunctionCall.FunctionType, + init: FunctionCallComparator.() -> Unit + ): FunctionCallComparator { + return FunctionCallComparator.createFromExpression( + expression, + expectedFunctionType = type + ).also(init) + } + + fun group(init: ExpressionComparator.() -> Unit): ExpressionComparator { + return createFromExpression(expression.group).also(init) + } + + internal companion object { + fun createFromExpression(expression: MathExpression): ExpressionComparator = + ExpressionComparator(expression) + } + } + + @ExpressionComparatorMarker + class ConstantComparator private constructor(private val constant: Real) { + fun withValueThat(): RealSubject = assertThat(constant) + + internal companion object { + fun createFromExpression(expression: MathExpression): ConstantComparator { + assertThat(expression.expressionTypeCase).isEqualTo(CONSTANT) + return ConstantComparator(expression.constant) + } + } + } + + @ExpressionComparatorMarker + class VariableComparator private constructor(private val variableName: String) { + fun withNameThat(): StringSubject = assertThat(variableName) + + internal companion object { + fun createFromExpression(expression: MathExpression): VariableComparator { + assertThat(expression.expressionTypeCase).isEqualTo(VARIABLE) + return VariableComparator(expression.variable) + } + } + } + + @ExpressionComparatorMarker + class BinaryOperationComparator private constructor( + private val operation: MathBinaryOperation + ) { + fun leftOperand(init: ExpressionComparator.() -> Unit): ExpressionComparator = + ExpressionComparator.createFromExpression(operation.leftOperand).also(init) + + fun rightOperand(init: ExpressionComparator.() -> Unit): ExpressionComparator = + ExpressionComparator.createFromExpression(operation.rightOperand).also(init) + + internal companion object { + fun createFromExpression( + expression: MathExpression, + expectedOperator: MathBinaryOperation.Operator + ): BinaryOperationComparator { + assertThat(expression.expressionTypeCase).isEqualTo(BINARY_OPERATION) + assertWithMessage("Expected binary operation with operator: $expectedOperator") + .that(expression.binaryOperation.operator) + .isEqualTo(expectedOperator) + return BinaryOperationComparator(expression.binaryOperation) + } + } + } + + @ExpressionComparatorMarker + class UnaryOperationComparator private constructor( + private val operation: MathUnaryOperation + ) { + fun operand(init: ExpressionComparator.() -> Unit): ExpressionComparator = + ExpressionComparator.createFromExpression(operation.operand).also(init) + + internal companion object { + fun createFromExpression( + expression: MathExpression, + expectedOperator: MathUnaryOperation.Operator + ): UnaryOperationComparator { + assertThat(expression.expressionTypeCase).isEqualTo(UNARY_OPERATION) + assertWithMessage("Expected unary operation with operator: $expectedOperator") + .that(expression.unaryOperation.operator) + .isEqualTo(expectedOperator) + return UnaryOperationComparator(expression.unaryOperation) + } + } + } + + @ExpressionComparatorMarker + class FunctionCallComparator private constructor( + private val functionCall: MathFunctionCall + ) { + fun argument(init: ExpressionComparator.() -> Unit): ExpressionComparator = + ExpressionComparator.createFromExpression(functionCall.argument).also(init) + + internal companion object { + fun createFromExpression( + expression: MathExpression, + expectedFunctionType: MathFunctionCall.FunctionType + ): FunctionCallComparator { + assertThat(expression.expressionTypeCase).isEqualTo(FUNCTION_CALL) + assertWithMessage("Expected function call to: $expectedFunctionType") + .that(expression.functionCall.functionType) + .isEqualTo(expectedFunctionType) + return FunctionCallComparator(expression.functionCall) + } + } + } + + companion object { + @DslMarker private annotation class ExpressionComparatorMarker + + fun assertThat(actual: MathExpression): MathExpressionSubject = + assertAbout(::MathExpressionSubject).that(actual) + } +} diff --git a/testing/src/main/java/org/oppia/android/testing/math/RealSubject.kt b/testing/src/main/java/org/oppia/android/testing/math/RealSubject.kt new file mode 100644 index 00000000000..8f9edddda0b --- /dev/null +++ b/testing/src/main/java/org/oppia/android/testing/math/RealSubject.kt @@ -0,0 +1,41 @@ +package org.oppia.android.testing.math + +import com.google.common.truth.DoubleSubject +import com.google.common.truth.FailureMetadata +import com.google.common.truth.IntegerSubject +import com.google.common.truth.Truth.assertAbout +import com.google.common.truth.Truth.assertThat +import com.google.common.truth.Truth.assertWithMessage +import com.google.common.truth.extensions.proto.LiteProtoSubject +import org.oppia.android.app.model.Real +import org.oppia.android.testing.math.FractionSubject.Companion.assertThat + +class RealSubject( + metadata: FailureMetadata, + private val actual: Real +) : LiteProtoSubject(metadata, actual) { + fun isRationalThat(): FractionSubject { + verifyTypeToBe(Real.RealTypeCase.RATIONAL) + return assertThat(actual.rational) + } + + fun isIrrationalThat(): DoubleSubject { + verifyTypeToBe(Real.RealTypeCase.IRRATIONAL) + return assertThat(actual.irrational) + } + + fun isIntegerThat(): IntegerSubject { + verifyTypeToBe(Real.RealTypeCase.INTEGER) + return assertThat(actual.integer) + } + + private fun verifyTypeToBe(expected: Real.RealTypeCase) { + assertWithMessage("Expected real type to be $expected, not: ${actual.realTypeCase}") + .that(actual.realTypeCase) + .isEqualTo(expected) + } + + companion object { + fun assertThat(actual: Real): RealSubject = assertAbout(::RealSubject).that(actual) + } +} From e45635d0adbd38c5f44521323e4b8f5329f3b8f3 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Tue, 14 Dec 2021 19:20:25 -0800 Subject: [PATCH 007/162] Add protos + testing lib for commutative exprs. --- model/src/main/proto/math.proto | 53 ++++++ .../oppia/android/testing/math/BUILD.bazel | 17 ++ .../math/ComparableOperationListSubject.kt | 172 ++++++++++++++++++ 3 files changed, 242 insertions(+) create mode 100644 testing/src/main/java/org/oppia/android/testing/math/ComparableOperationListSubject.kt diff --git a/model/src/main/proto/math.proto b/model/src/main/proto/math.proto index 7dcbf780370..168db946cd6 100644 --- a/model/src/main/proto/math.proto +++ b/model/src/main/proto/math.proto @@ -96,3 +96,56 @@ message MathEquation { MathExpression left_side = 1; MathExpression right_side = 2; } + +// Represents a list of comparable mathematics operations. 'Comparable' here means that this +// structure provides a trivial way to compare commutative operations (i.e. by extracting terms from +// multiple subsequent commutative operations into lists that can be deterministically sorted). This +// structure is meant to provide a means to compare two expressions without considering +// associativity or commutativity (though the latter requires the operation lists stored within this +// structure to be sorted before using standard proto equals checking). +message ComparableOperationList { + message ComparableOperation { + // Treat this operation (e.g. x) as negated (e.g. -x). + bool is_negated = 1; + + // Treat this operation (e.g. x) as a multiplicative inverse (e.g. 1/x). + bool is_inverted = 2; + + oneof comparison_type { + CommutativeAccumulation commutative_accumulation = 3; + NonCommutativeOperation non_commutative_operation = 4; + Real constant_term = 5; + string variable_term = 6; + } + } + // Represents an accumulation of operations (such as a summation or product). This helps simplify + // comparison across commutative boundaries by collecting terms into sortable lists, such as the + // expression 1+2+3 becoming [1,2,3] and trivially comparable to [3,2,1] from 3+2+1. + // + // Subsequent subtractions are treated as additions with each term arithmetically negated (i.e. + // f(x)=-x). Similarly, divisions are considered multiplications with each divisor being + // multiplicatively inverted (i.e. the reciprocal function: f(x)=1/x). + message CommutativeAccumulation { + enum AccumulationType { + ACCUMULATION_TYPE_UNSPECIFIED = 0; + SUMMATION = 1; + PRODUCT = 2; + } + + AccumulationType accumulation_type = 1; + repeated ComparableOperation combined_operations = 2; + } + message NonCommutativeOperation { + oneof operation_type { + BinaryOperation exponentiation = 1; + ComparableOperation square_root = 2; + } + + message BinaryOperation { + ComparableOperation left_operand = 1; + ComparableOperation right_operand = 2; + } + } + + ComparableOperation root_operation = 1; +} diff --git a/testing/src/main/java/org/oppia/android/testing/math/BUILD.bazel b/testing/src/main/java/org/oppia/android/testing/math/BUILD.bazel index a1453dd4496..ed7e885c3c0 100644 --- a/testing/src/main/java/org/oppia/android/testing/math/BUILD.bazel +++ b/testing/src/main/java/org/oppia/android/testing/math/BUILD.bazel @@ -6,6 +6,23 @@ load("@io_bazel_rules_kotlin//kotlin:kotlin.bzl", "kt_android_library") # TODO(#2747): Move these libraries to be under utility/.../math/testing. +kt_android_library( + name = "comparable_operation_list_subject", + testonly = True, + srcs = [ + "ComparableOperationListSubject.kt", + ], + visibility = [ + "//:oppia_testing_visibility", + ], + deps = [ + ":real_subject", + "//model/src/main/proto:math_java_proto_lite", + "//third_party:com_google_truth_extensions_truth-liteproto-extension", + "//third_party:com_google_truth_truth", + ], +) + kt_android_library( name = "fraction_subject", testonly = True, diff --git a/testing/src/main/java/org/oppia/android/testing/math/ComparableOperationListSubject.kt b/testing/src/main/java/org/oppia/android/testing/math/ComparableOperationListSubject.kt new file mode 100644 index 00000000000..ce32ec28d2b --- /dev/null +++ b/testing/src/main/java/org/oppia/android/testing/math/ComparableOperationListSubject.kt @@ -0,0 +1,172 @@ +package org.oppia.android.testing.math + +import com.google.common.truth.BooleanSubject +import com.google.common.truth.FailureMetadata +import com.google.common.truth.IntegerSubject +import com.google.common.truth.StringSubject +import com.google.common.truth.Truth.assertAbout +import com.google.common.truth.Truth.assertThat +import com.google.common.truth.extensions.proto.LiteProtoSubject +import org.oppia.android.app.model.ComparableOperationList +import org.oppia.android.app.model.ComparableOperationList.ComparableOperation +import org.oppia.android.app.model.ComparableOperationList.ComparableOperation.ComparisonTypeCase +import org.oppia.android.app.model.Real +import org.oppia.android.testing.math.RealSubject.Companion.assertThat + +class ComparableOperationListSubject( + metadata: FailureMetadata, + private val actual: ComparableOperationList +) : LiteProtoSubject(metadata, actual) { + fun hasStructureThatMatches(init: ComparableOperationComparator.() -> Unit) { + ComparableOperationComparator.createFrom(actual.rootOperation).also(init) + } + + @ComparableOperationComparatorMarker + class ComparableOperationComparator private constructor( + private val operation: ComparableOperation + ) { + fun hasNegatedPropertyThat(): BooleanSubject = assertThat(operation.isNegated) + + fun hasInvertedPropertyThat(): BooleanSubject = assertThat(operation.isInverted) + + fun commutativeAccumulationWithType( + type: ComparableOperationList.CommutativeAccumulation.AccumulationType, + init: CommutativeAccumulationComparator.() -> Unit + ): CommutativeAccumulationComparator = + CommutativeAccumulationComparator.createFrom(type, operation).also(init) + + fun nonCommutativeOperation( + init: NonCommutativeOperationComparator.() -> Unit + ): NonCommutativeOperationComparator = + NonCommutativeOperationComparator.createFrom(operation).also(init) + + fun constantTerm(init: ConstantTermComparator.() -> Unit): ConstantTermComparator = + ConstantTermComparator.createFrom(operation).also(init) + + fun variableTerm(init: VariableTermComparator.() -> Unit): VariableTermComparator = + VariableTermComparator.createFrom(operation).also(init) + + internal companion object { + fun createFrom(operation: ComparableOperation): ComparableOperationComparator = + ComparableOperationComparator(operation) + } + } + + @ComparableOperationComparatorMarker + class CommutativeAccumulationComparator private constructor( + private val accumulation: ComparableOperationList.CommutativeAccumulation + ) { + fun hasOperandCountThat(): IntegerSubject = assertThat(accumulation.combinedOperationsCount) + + fun index( + index: Int, + init: ComparableOperationComparator.() -> Unit + ): ComparableOperationComparator { + return ComparableOperationComparator.createFrom( + accumulation.combinedOperationsList[index] + ).also(init) + } + + internal companion object { + fun createFrom( + type: ComparableOperationList.CommutativeAccumulation.AccumulationType, + operation: ComparableOperation + ): CommutativeAccumulationComparator { + assertThat(operation.comparisonTypeCase) + .isEqualTo(ComparisonTypeCase.COMMUTATIVE_ACCUMULATION) + assertThat(operation.commutativeAccumulation.accumulationType).isEqualTo(type) + return CommutativeAccumulationComparator(operation.commutativeAccumulation) + } + } + } + + @ComparableOperationComparatorMarker + class NonCommutativeOperationComparator private constructor( + private val operation: ComparableOperationList.NonCommutativeOperation + ) { + fun exponentiation(init: BinaryOperationComparator.() -> Unit): BinaryOperationComparator { + verifyTypeAs( + ComparableOperationList.NonCommutativeOperation.OperationTypeCase.EXPONENTIATION + ) + return BinaryOperationComparator.createFrom(operation.exponentiation).also(init) + } + + fun squareRootWithArgument( + init: ComparableOperationComparator.() -> Unit + ): ComparableOperationComparator { + verifyTypeAs(ComparableOperationList.NonCommutativeOperation.OperationTypeCase.SQUARE_ROOT) + return ComparableOperationComparator.createFrom(operation.squareRoot).also(init) + } + + private fun verifyTypeAs( + type: ComparableOperationList.NonCommutativeOperation.OperationTypeCase + ) { + assertThat(operation.operationTypeCase).isEqualTo(type) + } + + internal companion object { + fun createFrom(operation: ComparableOperation): NonCommutativeOperationComparator { + assertThat(operation.comparisonTypeCase) + .isEqualTo(ComparisonTypeCase.NON_COMMUTATIVE_OPERATION) + return NonCommutativeOperationComparator(operation.nonCommutativeOperation) + } + } + } + + @ComparableOperationComparatorMarker + class BinaryOperationComparator private constructor( + private val operation: ComparableOperationList.NonCommutativeOperation.BinaryOperation + ) { + fun leftOperand( + init: ComparableOperationComparator.() -> Unit + ): ComparableOperationComparator = + ComparableOperationComparator.createFrom(operation.leftOperand).also(init) + + fun rightOperand( + init: ComparableOperationComparator.() -> Unit + ): ComparableOperationComparator = + ComparableOperationComparator.createFrom(operation.rightOperand).also(init) + + internal companion object { + fun createFrom( + operation: ComparableOperationList.NonCommutativeOperation.BinaryOperation + ): BinaryOperationComparator = BinaryOperationComparator(operation) + } + } + + @ComparableOperationComparatorMarker + class ConstantTermComparator private constructor( + private val constant: Real + ) { + fun withValueThat(): RealSubject = assertThat(constant) + + internal companion object { + fun createFrom(operation: ComparableOperation): ConstantTermComparator { + assertThat(operation.comparisonTypeCase).isEqualTo(ComparisonTypeCase.CONSTANT_TERM) + return ConstantTermComparator(operation.constantTerm) + } + } + } + + @ComparableOperationComparatorMarker + class VariableTermComparator private constructor( + private val variableName: String + ) { + fun withNameThat(): StringSubject = assertThat(variableName) + + internal companion object { + fun createFrom(operation: ComparableOperation): VariableTermComparator { + assertThat(operation.comparisonTypeCase).isEqualTo(ComparisonTypeCase.VARIABLE_TERM) + return VariableTermComparator(operation.variableTerm) + } + } + } + + companion object { + // See: https://kotlinlang.org/docs/type-safe-builders.html. + @DslMarker private annotation class ComparableOperationComparatorMarker + + fun assertThat(actual: ComparableOperationList): ComparableOperationListSubject = + assertAbout(::ComparableOperationListSubject).that(actual) + } +} From da5d72d1cc5bf3e7a1493be452a40ec3d4bc06b9 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Tue, 14 Dec 2021 19:50:42 -0800 Subject: [PATCH 008/162] Add protos & test libs for polynomials. --- .../util/InteractionObjectExtensions.kt | 9 --- model/src/main/proto/math.proto | 14 ++++ .../oppia/android/testing/math/BUILD.bazel | 18 +++++ .../android/testing/math/PolynomialSubject.kt | 79 +++++++++++++++++++ .../org/oppia/android/util/math/BUILD.bazel | 2 + .../android/util/math/FloatExtensions.kt | 2 + .../android/util/math/FractionExtensions.kt | 59 ++++++++++++++ .../android/util/math/PolynomialExtensions.kt | 56 +++++++++++++ .../oppia/android/util/math/RealExtensions.kt | 53 +++++++++++++ 9 files changed, 283 insertions(+), 9 deletions(-) create mode 100644 testing/src/main/java/org/oppia/android/testing/math/PolynomialSubject.kt create mode 100644 utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt create mode 100644 utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt diff --git a/domain/src/main/java/org/oppia/android/domain/util/InteractionObjectExtensions.kt b/domain/src/main/java/org/oppia/android/domain/util/InteractionObjectExtensions.kt index 2e37e34e0f7..11d3af018a9 100644 --- a/domain/src/main/java/org/oppia/android/domain/util/InteractionObjectExtensions.kt +++ b/domain/src/main/java/org/oppia/android/domain/util/InteractionObjectExtensions.kt @@ -107,15 +107,6 @@ private fun ImageWithRegions.toAnswerString(): String = private fun ClickOnImage.toAnswerString(): String = "[(${clickedRegionsList.joinToString()}), (${clickPosition.x}, ${clickPosition.y})]" -// https://github.com/oppia/oppia/blob/37285a/core/templates/dev/head/domain/objects/FractionObjectFactory.ts#L47 -private fun Fraction.toAnswerString(): String { - val fractionString = if (numerator != 0) "$numerator/$denominator" else "" - val mixedString = if (wholeNumber != 0) "$wholeNumber $fractionString" else "" - val positiveFractionString = if (mixedString.isNotEmpty()) mixedString else fractionString - val negativeString = if (isNegative) "-" else "" - return if (positiveFractionString.isNotEmpty()) "$negativeString$positiveFractionString" else "0" -} - private fun TranslatableHtmlContentId.toAnswerString(): String { return "content_id=$contentId" } diff --git a/model/src/main/proto/math.proto b/model/src/main/proto/math.proto index 168db946cd6..95127692db6 100644 --- a/model/src/main/proto/math.proto +++ b/model/src/main/proto/math.proto @@ -149,3 +149,17 @@ message ComparableOperationList { ComparableOperation root_operation = 1; } + +message Polynomial { + repeated Term term = 1; + + message Term { + Real coefficient = 1; + repeated Variable variable = 2; + + message Variable { + string name = 1; + uint32 power = 2; + } + } +} diff --git a/testing/src/main/java/org/oppia/android/testing/math/BUILD.bazel b/testing/src/main/java/org/oppia/android/testing/math/BUILD.bazel index ed7e885c3c0..275084d309b 100644 --- a/testing/src/main/java/org/oppia/android/testing/math/BUILD.bazel +++ b/testing/src/main/java/org/oppia/android/testing/math/BUILD.bazel @@ -74,6 +74,24 @@ kt_android_library( ], ) +kt_android_library( + name = "polynomial_subject", + testonly = True, + srcs = [ + "PolynomialSubject.kt", + ], + visibility = [ + "//:oppia_testing_visibility", + ], + deps = [ + ":real_subject", + "//model/src/main/proto:math_java_proto_lite", + "//third_party:com_google_truth_extensions_truth-liteproto-extension", + "//third_party:com_google_truth_truth", + "//utility/src/main/java/org/oppia/android/util/math:extensions", + ], +) + kt_android_library( name = "real_subject", testonly = True, diff --git a/testing/src/main/java/org/oppia/android/testing/math/PolynomialSubject.kt b/testing/src/main/java/org/oppia/android/testing/math/PolynomialSubject.kt new file mode 100644 index 00000000000..6b05db139a5 --- /dev/null +++ b/testing/src/main/java/org/oppia/android/testing/math/PolynomialSubject.kt @@ -0,0 +1,79 @@ +package org.oppia.android.testing.math + +import com.google.common.truth.FailureMetadata +import com.google.common.truth.IntegerSubject +import com.google.common.truth.StringSubject +import com.google.common.truth.Truth.assertAbout +import com.google.common.truth.Truth.assertThat +import com.google.common.truth.Truth.assertWithMessage +import com.google.common.truth.extensions.proto.LiteProtoSubject +import org.oppia.android.app.model.Polynomial +import org.oppia.android.testing.math.RealSubject.Companion.assertThat +import org.oppia.android.util.math.getConstant +import org.oppia.android.util.math.isConstant +import org.oppia.android.util.math.toPlainText + +class PolynomialSubject( + metadata: FailureMetadata, + private val actual: Polynomial? +) : LiteProtoSubject(metadata, actual) { + private val nonNullActual by lazy { + checkNotNull(actual) { + "Expected polynomial to be defined, not null (is the expression/equation not a valid" + + " polynomial?)" + } + } + + fun isNotValidPolynomial() { + // TODO: use toPlainText here. + assertWithMessage( + "Expected polynomial to be undefined, but was: ${actual?.toPlainText()}" + ).that(actual).isNull() + } + + fun isConstantThat(): RealSubject { + // TODO: use toPlainText here. + assertWithMessage("Expected polynomial to be constant, but was: $nonNullActual") + .that(nonNullActual.isConstant()) + .isTrue() + return assertThat(nonNullActual.getConstant()) + } + + fun hasTermCountThat(): IntegerSubject = assertThat(nonNullActual.termCount) + + fun term(index: Int): PolynomialTermSubject = assertThat(nonNullActual.termList[index]) + + fun evaluatesToPlainTextThat(): StringSubject = assertThat(nonNullActual.toPlainText()) + + companion object { + fun assertThat(actual: Polynomial?): PolynomialSubject = + assertAbout(::PolynomialSubject).that(actual) + + private fun assertThat(actual: Polynomial.Term): PolynomialTermSubject = + assertAbout(::PolynomialTermSubject).that(actual) + + private fun assertThat(actual: Polynomial.Term.Variable): PolynomialTermVariableSubject = + assertAbout(::PolynomialTermVariableSubject).that(actual) + } + + class PolynomialTermSubject( + metadata: FailureMetadata, + private val actual: Polynomial.Term + ) : LiteProtoSubject(metadata, actual) { + fun hasCoefficientThat(): RealSubject = assertThat(actual.coefficient) + + fun hasVariableCountThat(): IntegerSubject = assertThat(actual.variableCount) + + fun variable(index: Int): PolynomialTermVariableSubject = + assertThat(actual.variableList[index]) + } + + class PolynomialTermVariableSubject( + metadata: FailureMetadata, + private val actual: Polynomial.Term.Variable + ) : LiteProtoSubject(metadata, actual) { + fun hasNameThat(): StringSubject = assertThat(actual.name) + + fun hasPowerThat(): IntegerSubject = assertThat(actual.power) + } +} diff --git a/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel b/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel index 4b84961d297..054b7df78bb 100644 --- a/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel @@ -9,7 +9,9 @@ kt_android_library( srcs = [ "FloatExtensions.kt", "FractionExtensions.kt", + "PolynomialExtensions.kt", "RatioExtensions.kt", + "RealExtensions.kt", ], visibility = [ "//:oppia_api_visibility", diff --git a/utility/src/main/java/org/oppia/android/util/math/FloatExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/FloatExtensions.kt index 62504046c78..2ca5ece9da3 100644 --- a/utility/src/main/java/org/oppia/android/util/math/FloatExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/FloatExtensions.kt @@ -14,3 +14,5 @@ fun Float.approximatelyEquals(other: Float): Boolean { fun Double.approximatelyEquals(other: Double): Boolean { return abs(this - other) < FLOAT_EQUALITY_INTERVAL } + +fun Double.toPlainString(): String = toBigDecimal().toPlainString() diff --git a/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt index d4a57faf9be..1da9fef1857 100644 --- a/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt @@ -2,6 +2,19 @@ package org.oppia.android.util.math import org.oppia.android.app.model.Fraction +/** Returns whether this fraction has a fractional component. */ +fun Fraction.hasFractionalPart(): Boolean { + return numerator != 0 +} + +/** + * Returns whether this fraction only represents a whole number. Note that for the fraction '0' this + * will return true. + */ +fun Fraction.isOnlyWholeNumber(): Boolean { + return !hasFractionalPart() +} + /** * Returns a [Double] version of this fraction. * @@ -13,6 +26,35 @@ fun Fraction.toDouble(): Double { return if (isNegative) -doubleVal else doubleVal } +/** + * Returns a submittable answer string representation of this fraction (note that this may not be + * the verbatim string originally submitted by the user, if any. + */ +fun Fraction.toAnswerString(): String { + return when { + isOnlyWholeNumber() -> { + // Fraction is only a whole number. + if (isNegative) "-$wholeNumber" else "$wholeNumber" + } + wholeNumber == 0 -> { + // Fraction contains just a fraction (no whole number). + when (denominator) { + 1 -> if (isNegative) "-$numerator" else "$numerator" + else -> if (isNegative) "-$numerator/$denominator" else "$numerator/$denominator" + } + } + else -> { + // Otherwise it's a mixed number. Note that the denominator is always shown here to account + // for strange cases that would require evaluation to resolve, such as: "2 2/1". + if (isNegative) { + "-$wholeNumber $numerator/$denominator" + } else { + "$wholeNumber $numerator/$denominator" + } + } + } +} + /** * Returns this fraction in its most simplified form. * @@ -26,6 +68,23 @@ fun Fraction.toSimplestForm(): Fraction { }.build() } +/** + * Returns this fraction in an improper form (that is, with a 0 whole number and only fractional + * parts). + */ +fun Fraction.toImproperForm(): Fraction { + val newNumerator = numerator + (denominator * wholeNumber) + return toBuilder().apply { + numerator = newNumerator + wholeNumber = 0 + }.build() +} + +/** Returns the negated form of this fraction. */ +operator fun Fraction.unaryMinus(): Fraction { + return toBuilder().apply { isNegative = !this@unaryMinus.isNegative }.build() +} + /** Returns the greatest common divisor between two integers. */ fun gcd(x: Int, y: Int): Int { return if (y == 0) x else gcd(y, x % y) diff --git a/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt new file mode 100644 index 00000000000..e1b98934566 --- /dev/null +++ b/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt @@ -0,0 +1,56 @@ +package org.oppia.android.util.math + +import org.oppia.android.app.model.Polynomial +import org.oppia.android.app.model.Polynomial.Term +import org.oppia.android.app.model.Polynomial.Term.Variable +import org.oppia.android.app.model.Real +import org.oppia.android.app.model.Real.RealTypeCase.INTEGER +import org.oppia.android.app.model.Real.RealTypeCase.IRRATIONAL +import org.oppia.android.app.model.Real.RealTypeCase.RATIONAL +import org.oppia.android.app.model.Real.RealTypeCase.REALTYPE_NOT_SET + +/** Returns whether this polynomial is a constant-only polynomial (contains no variables). */ +fun Polynomial.isConstant(): Boolean = termCount == 1 && getTerm(0).variableCount == 0 + +/** + * Returns the first term coefficient from this polynomial. This corresponds to the whole value of + * the polynomial iff isConstant() returns true, otherwise this value isn't useful. + * + * Note that this function can throw if the polynomial is empty (so isConstant() should always be + * checked first). + */ +fun Polynomial.getConstant(): Real = getTerm(0).coefficient + +fun Polynomial.toPlainText(): String { + return termList.map { it.toPlainText() }.reduce { acc, termAnswerStr -> + if (termAnswerStr.startsWith("-")) { + "$acc - ${termAnswerStr.drop(1)}" + } else "$acc + $termAnswerStr" + } +} + +private fun Term.toPlainText(): String { + val productValues = mutableListOf() + + // Include the coefficient if there is one (coefficients of 1 are ignored only if there are + // variables present). + productValues += when { + variableList.isEmpty() || !abs(coefficient).isApproximatelyEqualTo(1.0) -> when { + coefficient.isRational() && variableList.isNotEmpty() -> "(${coefficient.toPlainText()})" + else -> coefficient.toPlainText() + } + coefficient.isNegative() -> "-" + else -> "" + } + + // Include any present variables. + productValues += variableList.map(Variable::toPlainText) + + // Take the product of all relevant values of the term. + return productValues.joinToString(separator = "") +} + +private fun Variable.toPlainText(): String { + return if (power > 1) "$name^$power" else name +} + diff --git a/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt new file mode 100644 index 00000000000..6df36abd3b6 --- /dev/null +++ b/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt @@ -0,0 +1,53 @@ +package org.oppia.android.util.math + +import org.oppia.android.app.model.Real +import org.oppia.android.app.model.Real.RealTypeCase.INTEGER +import org.oppia.android.app.model.Real.RealTypeCase.IRRATIONAL +import org.oppia.android.app.model.Real.RealTypeCase.RATIONAL +import org.oppia.android.app.model.Real.RealTypeCase.REALTYPE_NOT_SET + +fun Real.isRational(): Boolean = realTypeCase == RATIONAL + +fun Real.isNegative(): Boolean = when (realTypeCase) { + RATIONAL -> rational.isNegative + IRRATIONAL -> irrational < 0 + INTEGER -> integer < 0 + REALTYPE_NOT_SET, null -> throw Exception("Invalid real: $this.") +} + +fun Real.toDouble(): Double { + return when (realTypeCase) { + RATIONAL -> rational.toDouble() + INTEGER -> integer.toDouble() + IRRATIONAL -> irrational + REALTYPE_NOT_SET, null -> throw Exception("Invalid real: $this.") + } +} + +fun Real.toPlainText(): String = when (realTypeCase) { + // Note that the rational part is first converted to an improper fraction since mixed fractions + // can't be expressed as a single coefficient in typical polynomial syntax). + RATIONAL -> rational.toImproperForm().toAnswerString() + IRRATIONAL -> irrational.toPlainString() + INTEGER -> integer.toString() + REALTYPE_NOT_SET, null -> "" +} + +fun Real.isApproximatelyEqualTo(value: Double): Boolean { + return toDouble().approximatelyEquals(value) +} + +operator fun Real.unaryMinus(): Real { + return when (realTypeCase) { + RATIONAL -> recompute { it.setRational(-rational) } + IRRATIONAL -> recompute { it.setIrrational(-irrational) } + INTEGER -> recompute { it.setInteger(-integer) } + REALTYPE_NOT_SET, null -> throw Exception("Invalid real: $this.") + } +} + +fun abs(real: Real): Real = if (real.isNegative()) -real else real + +private fun Real.recompute(transform: (Real.Builder) -> Real.Builder): Real { + return transform(newBuilderForType()).build() +} From 7c36fdfbe93e4f13319344a4dc118825c244304a Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Tue, 14 Dec 2021 23:15:30 -0800 Subject: [PATCH 009/162] Lint fix. --- ...utIsEquivalentToAndInSimplestFormRuleClassifierProvider.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProvider.kt index af698227520..f9498f7d965 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProvider.kt @@ -36,7 +36,7 @@ class FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProvider input: Fraction, writtenTranslationContext: WrittenTranslationContext ): Boolean { - return answer.toDouble().approximatelyEquals(input.toDouble()) - && answer == input.toSimplestForm() + return answer.toDouble().approximatelyEquals(input.toDouble()) && + answer == input.toSimplestForm() } } From d430f8c826054b6d9c4a7d1fc5b7da9f4297cc1f Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Tue, 14 Dec 2021 23:17:24 -0800 Subject: [PATCH 010/162] Lint fixes. --- .../oppia/android/domain/util/InteractionObjectExtensions.kt | 1 - .../java/org/oppia/android/testing/math/PolynomialSubject.kt | 2 +- .../java/org/oppia/android/util/math/PolynomialExtensions.kt | 5 ----- 3 files changed, 1 insertion(+), 7 deletions(-) diff --git a/domain/src/main/java/org/oppia/android/domain/util/InteractionObjectExtensions.kt b/domain/src/main/java/org/oppia/android/domain/util/InteractionObjectExtensions.kt index 11d3af018a9..32f9123e852 100644 --- a/domain/src/main/java/org/oppia/android/domain/util/InteractionObjectExtensions.kt +++ b/domain/src/main/java/org/oppia/android/domain/util/InteractionObjectExtensions.kt @@ -1,7 +1,6 @@ package org.oppia.android.domain.util import org.oppia.android.app.model.ClickOnImage -import org.oppia.android.app.model.Fraction import org.oppia.android.app.model.ImageWithRegions import org.oppia.android.app.model.InteractionObject import org.oppia.android.app.model.InteractionObject.ObjectTypeCase.BOOL_VALUE diff --git a/testing/src/main/java/org/oppia/android/testing/math/PolynomialSubject.kt b/testing/src/main/java/org/oppia/android/testing/math/PolynomialSubject.kt index 6b05db139a5..bb4a3d970d5 100644 --- a/testing/src/main/java/org/oppia/android/testing/math/PolynomialSubject.kt +++ b/testing/src/main/java/org/oppia/android/testing/math/PolynomialSubject.kt @@ -44,7 +44,7 @@ class PolynomialSubject( fun term(index: Int): PolynomialTermSubject = assertThat(nonNullActual.termList[index]) fun evaluatesToPlainTextThat(): StringSubject = assertThat(nonNullActual.toPlainText()) - + companion object { fun assertThat(actual: Polynomial?): PolynomialSubject = assertAbout(::PolynomialSubject).that(actual) diff --git a/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt index e1b98934566..a4ba72213be 100644 --- a/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt @@ -4,10 +4,6 @@ import org.oppia.android.app.model.Polynomial import org.oppia.android.app.model.Polynomial.Term import org.oppia.android.app.model.Polynomial.Term.Variable import org.oppia.android.app.model.Real -import org.oppia.android.app.model.Real.RealTypeCase.INTEGER -import org.oppia.android.app.model.Real.RealTypeCase.IRRATIONAL -import org.oppia.android.app.model.Real.RealTypeCase.RATIONAL -import org.oppia.android.app.model.Real.RealTypeCase.REALTYPE_NOT_SET /** Returns whether this polynomial is a constant-only polynomial (contains no variables). */ fun Polynomial.isConstant(): Boolean = termCount == 1 && getTerm(0).variableCount == 0 @@ -53,4 +49,3 @@ private fun Term.toPlainText(): String { private fun Variable.toPlainText(): String { return if (power > 1) "$name^$power" else name } - From 0099d67a87ea22f7ad26fb3536453646127dc077 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Tue, 14 Dec 2021 23:34:12 -0800 Subject: [PATCH 011/162] Add math tokenizer + utility & tests. This is mostly copied from #2173. --- .../org/oppia/android/util/math/BUILD.bazel | 21 + .../oppia/android/util/math/MathTokenizer.kt | 381 ++++++++++++++++++ .../android/util/math/PeekableIterator.kt | 38 ++ .../org/oppia/android/util/math/BUILD.bazel | 18 + .../android/util/math/MathTokenizerTest.kt | 195 +++++++++ 5 files changed, 653 insertions(+) create mode 100644 utility/src/main/java/org/oppia/android/util/math/MathTokenizer.kt create mode 100644 utility/src/main/java/org/oppia/android/util/math/PeekableIterator.kt create mode 100644 utility/src/test/java/org/oppia/android/util/math/MathTokenizerTest.kt diff --git a/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel b/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel index 054b7df78bb..1c099b59d0f 100644 --- a/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel @@ -20,3 +20,24 @@ kt_android_library( "//model/src/main/proto:math_java_proto_lite", ], ) + +kt_android_library( + name = "tokenizer", + srcs = [ + "MathTokenizer.kt", + ], + visibility = [ + "//:oppia_testing_visibility", + ], + deps = [ + ":peekable_iterator", + "//model/src/main/proto:math_java_proto_lite", + ], +) + +kt_android_library( + name = "peekable_iterator", + srcs = [ + "PeekableIterator.kt", + ], +) diff --git a/utility/src/main/java/org/oppia/android/util/math/MathTokenizer.kt b/utility/src/main/java/org/oppia/android/util/math/MathTokenizer.kt new file mode 100644 index 00000000000..37ca0410cd0 --- /dev/null +++ b/utility/src/main/java/org/oppia/android/util/math/MathTokenizer.kt @@ -0,0 +1,381 @@ +package org.oppia.android.util.math + +import org.oppia.android.app.model.MathBinaryOperation +import org.oppia.android.app.model.MathBinaryOperation.Operator.ADD +import org.oppia.android.app.model.MathBinaryOperation.Operator.DIVIDE +import org.oppia.android.app.model.MathBinaryOperation.Operator.EXPONENTIATE +import org.oppia.android.app.model.MathBinaryOperation.Operator.MULTIPLY +import org.oppia.android.app.model.MathBinaryOperation.Operator.SUBTRACT +import org.oppia.android.app.model.MathUnaryOperation +import org.oppia.android.app.model.MathUnaryOperation.Operator.NEGATE +import org.oppia.android.app.model.MathUnaryOperation.Operator.POSITIVE +import java.lang.StringBuilder + +// TODO: rename to MathTokenizer & add documentation. +// TODO: consider a more efficient implementation that uses 1 underlying buffer (which could still +// be sequence-backed) with a forced lookahead-of-1 API, to also avoid rebuffering when parsing +// sequences of characters like for integers. + +// TODO: add customization to omit certain symbols (such as variables for numeric expressions?) +class MathTokenizer private constructor() { + companion object { + fun tokenize(input: String): Sequence = tokenize(input.toCharArray().asSequence()) + + fun tokenize(input: Sequence): Sequence { + val chars = PeekableIterator.fromSequence(input) + return generateSequence { + // Consume any whitespace that might precede a valid token. + chars.consumeWhitespace() + + // Parse the next token from the underlying sequence. + when (chars.peek()) { + in '0'..'9' -> tokenizeIntegerOrRealNumber(chars) + in 'a'..'z', in 'A'..'Z' -> tokenizeVariableOrFunctionName(chars) + '√' -> tokenizeSymbol(chars) { startIndex, endIndex -> + Token.SquareRootSymbol(startIndex, endIndex) + } + '+' -> tokenizeSymbol(chars) { startIndex, endIndex -> + Token.PlusSymbol(startIndex, endIndex) + } + // TODO: add tests for different subtraction/minus symbols. + '-', '−' -> tokenizeSymbol(chars) { startIndex, endIndex -> + Token.MinusSymbol(startIndex, endIndex) + } + '*', '×' -> tokenizeSymbol(chars) { startIndex, endIndex -> + Token.MultiplySymbol(startIndex, endIndex) + } + '/', '÷' -> tokenizeSymbol(chars) { startIndex, endIndex -> + Token.DivideSymbol(startIndex, endIndex) + } + '^' -> tokenizeSymbol(chars) { startIndex, endIndex -> + Token.ExponentiationSymbol(startIndex, endIndex) + } + '=' -> tokenizeSymbol(chars) { startIndex, endIndex -> + Token.EqualsSymbol(startIndex, endIndex) + } + '(' -> tokenizeSymbol(chars) { startIndex, endIndex -> + Token.LeftParenthesisSymbol(startIndex, endIndex) + } + ')' -> tokenizeSymbol(chars) { startIndex, endIndex -> + Token.RightParenthesisSymbol(startIndex, endIndex) + } + null -> null // End of stream. + // Invalid character. + else -> tokenizeSymbol(chars) { startIndex, endIndex -> + Token.InvalidToken(startIndex, endIndex) + } + } + } + } + + private fun tokenizeIntegerOrRealNumber(chars: PeekableIterator): Token { + val startIndex = chars.getRetrievalCount() + val integerPart1 = + parseInteger(chars) + ?: return Token.InvalidToken(startIndex, endIndex = chars.getRetrievalCount()) + chars.consumeWhitespace() // Whitespace is allowed between digits and the '.'. + return if (chars.peek() == '.') { + chars.next() // Parse the "." since it will be re-added later. + chars.consumeWhitespace() // Whitespace is allowed between the '.' and following digits. + + // Another integer must follow the ".". + val integerPart2 = parseInteger(chars) + ?: return Token.InvalidToken(startIndex, endIndex = chars.getRetrievalCount()) + + // TODO: validate that the result isn't NaN or INF. + val doubleValue = "$integerPart1.$integerPart2".toDoubleOrNull() + ?: return Token.InvalidToken(startIndex, endIndex = chars.getRetrievalCount()) + Token.PositiveRealNumber(doubleValue, startIndex, endIndex = chars.getRetrievalCount()) + } else { + Token.PositiveInteger( + integerPart1.toIntOrNull() + ?: return Token.InvalidToken(startIndex, endIndex = chars.getRetrievalCount()), + startIndex, + endIndex = chars.getRetrievalCount() + ) + } + } + + private fun tokenizeVariableOrFunctionName(chars: PeekableIterator): Token { + val startIndex = chars.getRetrievalCount() + val firstChar = chars.next() + + // latin_letter = lowercase_latin_letter | uppercase_latin_letter ; + // variable = latin_letter ; + return tokenizeFunctionName(firstChar, startIndex, chars) + ?: Token.VariableName( + firstChar.toString(), startIndex, endIndex = chars.getRetrievalCount() + ) + } + + private fun tokenizeFunctionName( + currChar: Char, + startIndex: Int, + chars: PeekableIterator + ): Token? { + // allowed_function_name = "sqrt" ; + // disallowed_function_name = + // "exp" | "log" | "log10" | "ln" | "sin" | "cos" | "tan" | "cot" | "csc" + // | "sec" | "atan" | "asin" | "acos" | "abs" ; + // function_name = allowed_function_name | disallowed_function_name ; + val nextChar = chars.peek() + return when (currChar) { + 'a' -> { + // abs, acos, asin, atan, or variable. + when (nextChar) { + 'b' -> + tokenizeExpectedFunction(name = "abs", isAllowedFunction = false, startIndex, chars) + 'c' -> + tokenizeExpectedFunction(name = "acos", isAllowedFunction = false, startIndex, chars) + 's' -> + tokenizeExpectedFunction(name = "asin", isAllowedFunction = false, startIndex, chars) + 't' -> + tokenizeExpectedFunction(name = "atan", isAllowedFunction = false, startIndex, chars) + else -> null // Must be a variable. + } + } + 'c' -> { + // cos, cot, csc, or variable. + when (nextChar) { + 'o' -> { + chars.next() // Skip the 'o' to go to the last character. + val name = if (chars.peek() == 's') { + chars.expectNextMatches { it == 's' } + ?: return Token.IncompleteFunctionName( + startIndex, endIndex = chars.getRetrievalCount() + ) + "cos" + } else { + // Otherwise, it must be 'c' for 'cot' since the parser can't backtrack. + chars.expectNextMatches { it == 't' } + ?: return Token.IncompleteFunctionName( + startIndex, endIndex = chars.getRetrievalCount() + ) + "cot" + } + Token.FunctionName( + name, isAllowedFunction = false, startIndex, endIndex = chars.getRetrievalCount() + ) + } + 's' -> + tokenizeExpectedFunction(name = "csc", isAllowedFunction = false, startIndex, chars) + else -> null // Must be a variable. + } + } + 'e' -> { + // exp or variable. + if (nextChar == 'x') { + tokenizeExpectedFunction(name = "exp", isAllowedFunction = false, startIndex, chars) + } else null // Must be a variable. + } + 'l' -> { + // ln, log, log10, or variable. + when (nextChar) { + 'n' -> + tokenizeExpectedFunction(name = "ln", isAllowedFunction = false, startIndex, chars) + 'o' -> { + // Skip the 'o'. Following the 'o' must be a 'g' since the parser can't backtrack. + chars.next() + chars.expectNextMatches { it == 'g' } + ?: return Token.IncompleteFunctionName( + startIndex, endIndex = chars.getRetrievalCount() + ) + val name = if (chars.peek() == '1') { + // '10' must be next for 'log10'. + chars.expectNextMatches { it == '1' } + ?: return Token.IncompleteFunctionName( + startIndex, endIndex = chars.getRetrievalCount() + ) + chars.expectNextMatches { it == '0' } + ?: return Token.IncompleteFunctionName( + startIndex, endIndex = chars.getRetrievalCount() + ) + "log10" + } else "log" + Token.FunctionName( + name, isAllowedFunction = false, startIndex, endIndex = chars.getRetrievalCount() + ) + } + else -> null // Must be a variable. + } + } + 's' -> { + // sec, sin, sqrt, or variable. + when (nextChar) { + 'e' -> + tokenizeExpectedFunction(name = "sec", isAllowedFunction = false, startIndex, chars) + 'i' -> + tokenizeExpectedFunction(name = "sin", isAllowedFunction = false, startIndex, chars) + 'q' -> + tokenizeExpectedFunction(name = "sqrt", isAllowedFunction = true, startIndex, chars) + else -> null // Must be a variable. + } + } + 't' -> { + // tan or variable. + if (nextChar == 'a') { + tokenizeExpectedFunction(name = "tan", isAllowedFunction = false, startIndex, chars) + } else null // Must be a variable. + } + else -> null // Must be a variable since no known functions match the first character. + } + } + + private fun tokenizeExpectedFunction( + name: String, + isAllowedFunction: Boolean, + startIndex: Int, + chars: PeekableIterator + ): Token { + return chars.expectNextCharsForFunctionName(name.substring(1), startIndex) + ?: Token.FunctionName( + name, isAllowedFunction, startIndex, endIndex = chars.getRetrievalCount() + ) + } + + private fun tokenizeSymbol(chars: PeekableIterator, factory: (Int, Int) -> Token): Token { + val startIndex = chars.getRetrievalCount() + chars.next() // Parse the symbol. + val endIndex = chars.getRetrievalCount() + return factory(startIndex, endIndex) + } + + private fun parseInteger(chars: PeekableIterator): String? { + val integerBuilder = StringBuilder() + while (chars.peek() in '0'..'9') { + integerBuilder.append(chars.next()) + } + return if (integerBuilder.isNotEmpty()) { + integerBuilder.toString() + } else null // Failed to parse; no digits. + } + + interface UnaryOperatorToken { + fun getUnaryOperator(): MathUnaryOperation.Operator + } + + interface BinaryOperatorToken { + fun getBinaryOperator(): MathBinaryOperation.Operator + } + + sealed class Token { + /** The index in the input stream at which point this token begins. */ + abstract val startIndex: Int + + /** The (exclusive) index in the input stream at which point this token ends. */ + abstract val endIndex: Int + + class PositiveInteger( + val parsedValue: Int, + override val startIndex: Int, + override val endIndex: Int + ) : Token() + + class PositiveRealNumber( + val parsedValue: Double, + override val startIndex: Int, + override val endIndex: Int + ) : Token() + + class VariableName( + val parsedName: String, + override val startIndex: Int, + override val endIndex: Int + ) : Token() + + class FunctionName( + val parsedName: String, + val isAllowedFunction: Boolean, + override val startIndex: Int, + override val endIndex: Int + ) : Token() + + class MinusSymbol( + override val startIndex: Int, + override val endIndex: Int + ) : Token(), UnaryOperatorToken, BinaryOperatorToken { + override fun getUnaryOperator(): MathUnaryOperation.Operator = NEGATE + + override fun getBinaryOperator(): MathBinaryOperation.Operator = SUBTRACT + } + + class SquareRootSymbol(override val startIndex: Int, override val endIndex: Int) : Token() + + class PlusSymbol( + override val startIndex: Int, + override val endIndex: Int + ) : Token(), UnaryOperatorToken, BinaryOperatorToken { + override fun getUnaryOperator(): MathUnaryOperation.Operator = POSITIVE + + override fun getBinaryOperator(): MathBinaryOperation.Operator = ADD + } + + class MultiplySymbol( + override val startIndex: Int, + override val endIndex: Int + ) : Token(), BinaryOperatorToken { + override fun getBinaryOperator(): MathBinaryOperation.Operator = MULTIPLY + } + + class DivideSymbol( + override val startIndex: Int, + override val endIndex: Int + ) : Token(), BinaryOperatorToken { + override fun getBinaryOperator(): MathBinaryOperation.Operator = DIVIDE + } + + class ExponentiationSymbol( + override val startIndex: Int, + override val endIndex: Int + ) : Token(), BinaryOperatorToken { + override fun getBinaryOperator(): MathBinaryOperation.Operator = EXPONENTIATE + } + + class EqualsSymbol(override val startIndex: Int, override val endIndex: Int) : Token() + + class LeftParenthesisSymbol( + override val startIndex: Int, + override val endIndex: Int + ) : Token() + + class RightParenthesisSymbol( + override val startIndex: Int, + override val endIndex: Int + ) : Token() + + class IncompleteFunctionName( + override val startIndex: Int, + override val endIndex: Int + ) : Token() + + class InvalidToken(override val startIndex: Int, override val endIndex: Int) : Token() + } + + // TODO: consider whether to use the more correct & expensive Java Char.isWhitespace(). + private fun Char.isWhitespace(): Boolean = when (this) { + ' ', '\t', '\n', '\r' -> true + else -> false + } + + private fun PeekableIterator.consumeWhitespace() { + while (peek()?.isWhitespace() == true) next() + } + + /** + * Expects each of the characters to be next in the token stream, in the order of the string. + * All characters must be present in [this] iterator. Returns non-null if a failure occurs, + * otherwise null if all characters were confirmed to be present. If null is returned, [this] + * iterator will be at the token that comes after the last confirmed character in the string. + */ + private fun PeekableIterator.expectNextCharsForFunctionName( + chars: String, + startIndex: Int + ): Token? { + for (c in chars) { + expectNextValue { c } + ?: return Token.IncompleteFunctionName(startIndex, endIndex = getRetrievalCount()) + } + return null + } + } +} diff --git a/utility/src/main/java/org/oppia/android/util/math/PeekableIterator.kt b/utility/src/main/java/org/oppia/android/util/math/PeekableIterator.kt new file mode 100644 index 00000000000..1a7abacc061 --- /dev/null +++ b/utility/src/main/java/org/oppia/android/util/math/PeekableIterator.kt @@ -0,0 +1,38 @@ +package org.oppia.android.util.math + +class PeekableIterator(private val backingIterator: Iterator) : Iterator { + private var next: T? = null + private var count: Int = 0 + + override fun hasNext(): Boolean = next != null || backingIterator.hasNext() + + override fun next(): T = next?.also { + next = null + count++ + } ?: retrieveNext() + + fun peek(): T? { + return when { + next != null -> next + hasNext() -> retrieveNext().also { next = it } + else -> null + } + } + + fun expectNextValue(expected: () -> T): T? = expectNextMatches { it == expected() } + + fun expectNextMatches(predicate: (T) -> Boolean): T? { + // Only call the predicate if not at the end of the stream, and only call next() if the next + // value matches. + return peek()?.takeIf(predicate)?.also { next() } + } + + fun getRetrievalCount(): Int = count + + private fun retrieveNext(): T = backingIterator.next() + + companion object { + fun fromSequence(sequence: Sequence): PeekableIterator = + PeekableIterator(sequence.iterator()) + } +} diff --git a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel index 493d89d66a0..d918580ffb9 100644 --- a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel @@ -4,6 +4,24 @@ TODO: document load("//:oppia_android_test.bzl", "oppia_android_test") +oppia_android_test( + name = "MathTokenizerTest", + srcs = ["MathTokenizerTest.kt"], + custom_package = "org.oppia.android.util.math", + test_class = "org.oppia.android.util.math.MathTokenizerTest", + test_manifest = "//utility:test_manifest", + deps = [ + "//model/src/main/proto:math_java_proto_lite", + "//testing:assertion_helpers", + "//third_party:androidx_test_ext_junit", + "//third_party:com_google_truth_truth", + "//third_party:junit_junit", + "//third_party:org_robolectric_robolectric", + "//third_party:robolectric_android-all", + "//utility/src/main/java/org/oppia/android/util/math:tokenizer", + ], +) + oppia_android_test( name = "RatioExtensionsTest", srcs = ["RatioExtensionsTest.kt"], diff --git a/utility/src/test/java/org/oppia/android/util/math/MathTokenizerTest.kt b/utility/src/test/java/org/oppia/android/util/math/MathTokenizerTest.kt new file mode 100644 index 00000000000..ac7e6556b9c --- /dev/null +++ b/utility/src/test/java/org/oppia/android/util/math/MathTokenizerTest.kt @@ -0,0 +1,195 @@ +package org.oppia.android.util.math + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.DoubleSubject +import com.google.common.truth.FailureMetadata +import com.google.common.truth.IntegerSubject +import com.google.common.truth.StringSubject +import com.google.common.truth.Subject +import com.google.common.truth.Truth.assertAbout +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.oppia.android.util.math.MathTokenizer.Companion.Token +import org.robolectric.annotation.LooperMode + +/** Tests for [MathTokenizer]. */ +@RunWith(AndroidJUnit4::class) +@LooperMode(LooperMode.Mode.PAUSED) +class MathTokenizerTest { + @Test + fun testLotsOfCases() { + // TODO: split this up + // testTokenize_emptyString_producesNoTokens + val tokens1 = MathTokenizer.tokenize(" ").toList() + assertThat(tokens1).isEmpty() + + val tokens2 = MathTokenizer.tokenize(" 2 ").toList() + assertThat(tokens2).hasSize(1) + assertThat(tokens2.first()).isPositiveIntegerWhoseValue().isEqualTo(2) + + val tokens3 = MathTokenizer.tokenize(" 2.5 ").toList() + assertThat(tokens3).hasSize(1) + assertThat(tokens3.first()).isPositiveRealNumberWhoseValue().isWithin(1e-5).of(2.5) + + val tokens4 = MathTokenizer.tokenize(" x ").toList() + assertThat(tokens4).hasSize(1) + assertThat(tokens4.first()).isVariableWhoseName().isEqualTo("x") + + val tokens5 = MathTokenizer.tokenize(" z x ").toList() + assertThat(tokens5).hasSize(2) + assertThat(tokens5[0]).isVariableWhoseName().isEqualTo("z") + assertThat(tokens5[1]).isVariableWhoseName().isEqualTo("x") + + val tokens6 = MathTokenizer.tokenize("2^3^2").toList() + assertThat(tokens6).hasSize(5) + assertThat(tokens6[0]).isPositiveIntegerWhoseValue().isEqualTo(2) + assertThat(tokens6[1]).isExponentiationSymbol() + assertThat(tokens6[2]).isPositiveIntegerWhoseValue().isEqualTo(3) + assertThat(tokens6[3]).isExponentiationSymbol() + assertThat(tokens6[4]).isPositiveIntegerWhoseValue().isEqualTo(2) + + val tokens7 = MathTokenizer.tokenize("sqrt(2)").toList() + assertThat(tokens7).hasSize(4) + assertThat(tokens7[0]).isFunctionWhoseName().isEqualTo("sqrt") + assertThat(tokens7[1]).isLeftParenthesisSymbol() + assertThat(tokens7[2]).isPositiveIntegerWhoseValue().isEqualTo(2) + assertThat(tokens7[3]).isRightParenthesisSymbol() + + val tokens8 = MathTokenizer.tokenize("sqr(2)").toList() + assertThat(tokens8).hasSize(4) + assertThat(tokens8[0]).isIncompleteFunctionName() + assertThat(tokens8[1]).isLeftParenthesisSymbol() + assertThat(tokens8[2]).isPositiveIntegerWhoseValue().isEqualTo(2) + assertThat(tokens8[3]).isRightParenthesisSymbol() + + val tokens9 = MathTokenizer.tokenize("xyz(2)").toList() + assertThat(tokens9).hasSize(6) + assertThat(tokens9[0]).isVariableWhoseName().isEqualTo("x") + assertThat(tokens9[1]).isVariableWhoseName().isEqualTo("y") + assertThat(tokens9[2]).isVariableWhoseName().isEqualTo("z") + assertThat(tokens9[3]).isLeftParenthesisSymbol() + assertThat(tokens9[4]).isPositiveIntegerWhoseValue().isEqualTo(2) + assertThat(tokens9[5]).isRightParenthesisSymbol() + + val tokens10 = MathTokenizer.tokenize("732").toList() + assertThat(tokens10).hasSize(1) + assertThat(tokens10.first()).isPositiveIntegerWhoseValue().isEqualTo(732) + + val tokens11 = MathTokenizer.tokenize("73 2").toList() + assertThat(tokens11).hasSize(2) + assertThat(tokens11[0]).isPositiveIntegerWhoseValue().isEqualTo(73) + assertThat(tokens11[1]).isPositiveIntegerWhoseValue().isEqualTo(2) + + val tokens12 = MathTokenizer.tokenize("1*2-3+4^7-8/3*2+7").toList() + assertThat(tokens12).hasSize(17) + assertThat(tokens12[0]).isPositiveIntegerWhoseValue().isEqualTo(1) + assertThat(tokens12[1]).isMultiplySymbol() + assertThat(tokens12[2]).isPositiveIntegerWhoseValue().isEqualTo(2) + assertThat(tokens12[3]).isMinusSymbol() + assertThat(tokens12[4]).isPositiveIntegerWhoseValue().isEqualTo(3) + assertThat(tokens12[5]).isPlusSymbol() + assertThat(tokens12[6]).isPositiveIntegerWhoseValue().isEqualTo(4) + assertThat(tokens12[7]).isExponentiationSymbol() + assertThat(tokens12[8]).isPositiveIntegerWhoseValue().isEqualTo(7) + assertThat(tokens12[9]).isMinusSymbol() + assertThat(tokens12[10]).isPositiveIntegerWhoseValue().isEqualTo(8) + assertThat(tokens12[11]).isDivideSymbol() + assertThat(tokens12[12]).isPositiveIntegerWhoseValue().isEqualTo(3) + assertThat(tokens12[13]).isMultiplySymbol() + assertThat(tokens12[14]).isPositiveIntegerWhoseValue().isEqualTo(2) + assertThat(tokens12[15]).isPlusSymbol() + assertThat(tokens12[16]).isPositiveIntegerWhoseValue().isEqualTo(7) + + val tokens13 = MathTokenizer.tokenize("x = √2 × 7 ÷ 4").toList() + assertThat(tokens13).hasSize(8) + assertThat(tokens13[0]).isVariableWhoseName().isEqualTo("x") + assertThat(tokens13[1]).isEqualsSymbol() + assertThat(tokens13[2]).isSquareRootSymbol() + assertThat(tokens13[3]).isPositiveIntegerWhoseValue().isEqualTo(2) + assertThat(tokens13[4]).isMultiplySymbol() + assertThat(tokens13[5]).isPositiveIntegerWhoseValue().isEqualTo(7) + assertThat(tokens13[6]).isDivideSymbol() + assertThat(tokens13[7]).isPositiveIntegerWhoseValue().isEqualTo(4) + } + + private class TokenSubject( + metadata: FailureMetadata, + private val actual: T + ) : Subject(metadata, actual) { + fun isPositiveIntegerWhoseValue(): IntegerSubject { + return assertThat(actual.asVerifiedType().parsedValue) + } + + fun isPositiveRealNumberWhoseValue(): DoubleSubject { + return assertThat(actual.asVerifiedType().parsedValue) + } + + fun isVariableWhoseName(): StringSubject { + return assertThat(actual.asVerifiedType().parsedName) + } + + fun isFunctionWhoseName(): StringSubject { + return assertThat(actual.asVerifiedType().parsedName) + } + + fun isMinusSymbol() { + actual.asVerifiedType() + } + + fun isSquareRootSymbol() { + actual.asVerifiedType() + } + + fun isPlusSymbol() { + actual.asVerifiedType() + } + + fun isMultiplySymbol() { + actual.asVerifiedType() + } + + fun isDivideSymbol() { + actual.asVerifiedType() + } + + fun isExponentiationSymbol() { + actual.asVerifiedType() + } + + fun isEqualsSymbol() { + actual.asVerifiedType() + } + + fun isLeftParenthesisSymbol() { + actual.asVerifiedType() + } + + fun isRightParenthesisSymbol() { + actual.asVerifiedType() + } + + fun isInvalidToken() { + actual.asVerifiedType() + } + + fun isIncompleteFunctionName() { + actual.asVerifiedType() + } + + private companion object { + private inline fun Token.asVerifiedType(): T { + assertThat(this).isInstanceOf(T::class.java) + return this as T + } + } + } + + private companion object { + private fun assertThat(actual: T): TokenSubject = + assertAbout(createTokenSubjectFactory()).that(actual) + + private fun createTokenSubjectFactory() = + Subject.Factory, T>(::TokenSubject) + } +} From 1d721d563071b57a4cf801dfec97f77da7663220 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 15 Dec 2021 00:25:11 -0800 Subject: [PATCH 012/162] Add math expression/equation parsing support. This includes full error detection, and specific test suites for each parsing case. Much revisement is needed in the tests, and some additional issues may yet need to be fixed in the parser and/or error-detection logic. This is copied from #2173 with revisement & reduction since it's part of a multi-PR split. --- .../org/oppia/android/util/math/BUILD.bazel | 30 + .../android/util/math/MathExpressionParser.kt | 1044 +++++++++ .../android/util/math/MathParsingError.kt | 69 + .../util/math/AlgebraicEquationParserTest.kt | 227 ++ .../math/AlgebraicExpressionParserTest.kt | 1939 +++++++++++++++++ .../org/oppia/android/util/math/BUILD.bazel | 82 + .../util/math/MathExpressionParserTest.kt | 344 +++ .../util/math/NumericExpressionParserTest.kt | 1777 +++++++++++++++ 8 files changed, 5512 insertions(+) create mode 100644 utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt create mode 100644 utility/src/main/java/org/oppia/android/util/math/MathParsingError.kt create mode 100644 utility/src/test/java/org/oppia/android/util/math/AlgebraicEquationParserTest.kt create mode 100644 utility/src/test/java/org/oppia/android/util/math/AlgebraicExpressionParserTest.kt create mode 100644 utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt create mode 100644 utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt diff --git a/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel b/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel index 1c099b59d0f..1e0da7381b5 100644 --- a/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel @@ -21,6 +21,36 @@ kt_android_library( ], ) +kt_android_library( + name = "parsing_error", + srcs = [ + "MathParsingError.kt", + ], + visibility = [ + "//:oppia_api_visibility", + ], + deps = [ + "//model/src/main/proto:math_java_proto_lite", + ], +) + +kt_android_library( + name = "parser", + srcs = [ + "MathExpressionParser.kt", + ], + visibility = [ + "//:oppia_testing_visibility", + ], + deps = [ + ":extensions", + ":parsing_error", + ":peekable_iterator", + ":tokenizer", + "//model/src/main/proto:math_java_proto_lite", + ], +) + kt_android_library( name = "tokenizer", srcs = [ diff --git a/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt b/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt new file mode 100644 index 00000000000..80b0acd49b3 --- /dev/null +++ b/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt @@ -0,0 +1,1044 @@ +package org.oppia.android.util.math + +import org.oppia.android.app.model.MathBinaryOperation +import org.oppia.android.app.model.MathBinaryOperation.Operator.ADD +import org.oppia.android.app.model.MathBinaryOperation.Operator.DIVIDE +import org.oppia.android.app.model.MathBinaryOperation.Operator.EXPONENTIATE +import org.oppia.android.app.model.MathBinaryOperation.Operator.MULTIPLY +import org.oppia.android.app.model.MathBinaryOperation.Operator.SUBTRACT +import org.oppia.android.app.model.MathEquation +import org.oppia.android.app.model.MathExpression +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.BINARY_OPERATION +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.CONSTANT +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.EXPRESSIONTYPE_NOT_SET +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.FUNCTION_CALL +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.GROUP +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.UNARY_OPERATION +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.VARIABLE +import org.oppia.android.app.model.MathFunctionCall +import org.oppia.android.app.model.MathUnaryOperation +import org.oppia.android.app.model.MathUnaryOperation.Operator.NEGATE +import org.oppia.android.app.model.MathUnaryOperation.Operator.POSITIVE +import org.oppia.android.app.model.Real +import org.oppia.android.util.math.MathExpressionParser.ParseContext.AlgebraicExpressionContext +import org.oppia.android.util.math.MathExpressionParser.ParseContext.NumericExpressionContext +import org.oppia.android.util.math.MathParsingError.DisabledVariablesInUseError +import org.oppia.android.util.math.MathParsingError.EquationHasWrongNumberOfEqualsError +import org.oppia.android.util.math.MathParsingError.EquationMissingLhsOrRhsError +import org.oppia.android.util.math.MathParsingError.ExponentIsVariableExpressionError +import org.oppia.android.util.math.MathParsingError.ExponentTooLargeError +import org.oppia.android.util.math.MathParsingError.FunctionNameIncompleteError +import org.oppia.android.util.math.MathParsingError.GenericError +import org.oppia.android.util.math.MathParsingError.HangingSquareRootError +import org.oppia.android.util.math.MathParsingError.InvalidFunctionInUseError +import org.oppia.android.util.math.MathParsingError.MultipleRedundantParenthesesError +import org.oppia.android.util.math.MathParsingError.NestedExponentsError +import org.oppia.android.util.math.MathParsingError.NoVariableOrNumberAfterBinaryOperatorError +import org.oppia.android.util.math.MathParsingError.NoVariableOrNumberBeforeBinaryOperatorError +import org.oppia.android.util.math.MathParsingError.NumberAfterVariableError +import org.oppia.android.util.math.MathParsingError.RedundantParenthesesForIndividualTermsError +import org.oppia.android.util.math.MathParsingError.SingleRedundantParenthesesError +import org.oppia.android.util.math.MathParsingError.SpacesBetweenNumbersError +import org.oppia.android.util.math.MathParsingError.SubsequentBinaryOperatorsError +import org.oppia.android.util.math.MathParsingError.SubsequentUnaryOperatorsError +import org.oppia.android.util.math.MathParsingError.TermDividedByZeroError +import org.oppia.android.util.math.MathParsingError.UnbalancedParenthesesError +import org.oppia.android.util.math.MathParsingError.UnnecessarySymbolsError +import org.oppia.android.util.math.MathParsingError.VariableInNumericExpressionError +import org.oppia.android.util.math.MathTokenizer.Companion.BinaryOperatorToken +import org.oppia.android.util.math.MathTokenizer.Companion.Token +import org.oppia.android.util.math.MathTokenizer.Companion.Token.DivideSymbol +import org.oppia.android.util.math.MathTokenizer.Companion.Token.EqualsSymbol +import org.oppia.android.util.math.MathTokenizer.Companion.Token.ExponentiationSymbol +import org.oppia.android.util.math.MathTokenizer.Companion.Token.FunctionName +import org.oppia.android.util.math.MathTokenizer.Companion.Token.IncompleteFunctionName +import org.oppia.android.util.math.MathTokenizer.Companion.Token.InvalidToken +import org.oppia.android.util.math.MathTokenizer.Companion.Token.LeftParenthesisSymbol +import org.oppia.android.util.math.MathTokenizer.Companion.Token.MinusSymbol +import org.oppia.android.util.math.MathTokenizer.Companion.Token.MultiplySymbol +import org.oppia.android.util.math.MathTokenizer.Companion.Token.PlusSymbol +import org.oppia.android.util.math.MathTokenizer.Companion.Token.PositiveInteger +import org.oppia.android.util.math.MathTokenizer.Companion.Token.PositiveRealNumber +import org.oppia.android.util.math.MathTokenizer.Companion.Token.RightParenthesisSymbol +import org.oppia.android.util.math.MathTokenizer.Companion.Token.SquareRootSymbol +import org.oppia.android.util.math.MathTokenizer.Companion.Token.VariableName +import kotlin.math.absoluteValue + +class MathExpressionParser private constructor(private val parseContext: ParseContext) { + // TODO: + // - Add helpers to reduce overall parser length. + // - Integrate with new errors & update the routines to not rely on exceptions except in actual exceptional cases. Make sure optional errors can be disabled (for testing purposes). + // - Rename this to be a generic parser, update the public API, add documentation, remove the old classes, and split up the big test routines into actual separate tests. + + // TODO: implement specific errors. + // TODO: verify remaining GenericErrors are correct. + + // TODO: document that 'generic' means either 'numeric' or 'algebraic' (ie that the expression is syntactically the same between both grammars). + // TODO: document that one design goal is keeping the grammar for this parser as LL(1) & why. + + private fun parseGenericEquationGrammar(): MathParsingResult { + // generic_equation_grammar = generic_equation ; + return parseGenericEquation().maybeFail { equation -> + checkForLearnerErrors(equation.leftSide) ?: checkForLearnerErrors(equation.rightSide) + } + } + + private fun parseGenericExpressionGrammar(): MathParsingResult { + // generic_expression_grammar = generic_expression ; + return parseGenericExpression().maybeFail { expression -> checkForLearnerErrors(expression) } + } + + private fun parseGenericEquation(): MathParsingResult { + // algebraic_equation = generic_expression , equals_operator , generic_expression ; + + if (parseContext.hasNextTokenOfType()) { + // If equals starts the string, then there's no LHS. + return EquationMissingLhsOrRhsError.toFailure() + } + + val lhsResult = parseGenericExpression().also { + parseContext.consumeTokenOfType() + }.maybeFail { + if (!parseContext.hasMoreTokens()) { + // If there are no tokens following the equals symbol, then there's no RHS. + EquationMissingLhsOrRhsError + } else null + } + + val rhsResult = lhsResult.flatMap { parseGenericExpression() } + return lhsResult.combineWith(rhsResult) { lhs, rhs -> + MathEquation.newBuilder().apply { + leftSide = lhs + rightSide = rhs + }.build() + } + } + + private fun parseGenericExpression(): MathParsingResult { + // generic_expression = generic_add_sub_expression ; + return parseGenericAddSubExpression() + } + + private fun parseGenericAddSubExpression(): MathParsingResult { + // generic_add_sub_expression = + // generic_mult_div_expression , { generic_add_sub_expression_rhs } ; + return parseGenericBinaryExpression( + parseLhs = this::parseGenericMultDivExpression + ) { nextToken -> + // generic_add_sub_expression_rhs = + // generic_add_expression_rhs | generic_sub_expression_rhs ; + when (nextToken) { + is PlusSymbol -> BinaryOperationRhs( + operator = ADD, + rhsResult = parseGenericAddExpressionRhs() + ) + is MinusSymbol -> BinaryOperationRhs( + operator = SUBTRACT, + rhsResult = parseGenericSubExpressionRhs() + ) + is PositiveInteger, is PositiveRealNumber, is DivideSymbol, is EqualsSymbol, + is ExponentiationSymbol, is FunctionName, is InvalidToken, is LeftParenthesisSymbol, + is MultiplySymbol, is RightParenthesisSymbol, is SquareRootSymbol, is VariableName, + is IncompleteFunctionName, null -> null + } + } + } + + private fun parseGenericAddExpressionRhs(): MathParsingResult { + // generic_add_expression_rhs = plus_operator , generic_mult_div_expression ; + return parseContext.consumeTokenOfType().maybeFail { + if (!parseContext.hasMoreTokens()) { + NoVariableOrNumberAfterBinaryOperatorError(ADD) + } else null + }.flatMap { + parseGenericMultDivExpression() + } + } + + private fun parseGenericSubExpressionRhs(): MathParsingResult { + // generic_sub_expression_rhs = minus_operator , generic_mult_div_expression ; + return parseContext.consumeTokenOfType().maybeFail { + if (!parseContext.hasMoreTokens()) { + NoVariableOrNumberAfterBinaryOperatorError(SUBTRACT) + } else null + }.flatMap { + parseGenericMultDivExpression() + } + } + + private fun parseGenericMultDivExpression(): MathParsingResult { + // generic_mult_div_expression = + // generic_exp_expression , { generic_mult_div_expression_rhs } ; + return parseGenericBinaryExpression( + parseLhs = this::parseGenericExpExpression + ) { nextToken -> + // generic_mult_div_expression_rhs = + // generic_mult_expression_rhs + // | generic_div_expression_rhs + // | generic_implicit_mult_expression_rhs ; + when (nextToken) { + is MultiplySymbol -> BinaryOperationRhs( + operator = MULTIPLY, + rhsResult = parseGenericMultExpressionRhs() + ) + is DivideSymbol -> BinaryOperationRhs( + operator = DIVIDE, + rhsResult = parseGenericDivExpressionRhs() + ) + is FunctionName, is LeftParenthesisSymbol, is SquareRootSymbol -> BinaryOperationRhs( + operator = MULTIPLY, + rhsResult = parseGenericImplicitMultExpressionRhs(), + isImplicit = true + ) + is VariableName -> { + if (parseContext is AlgebraicExpressionContext) { + BinaryOperationRhs( + operator = MULTIPLY, + rhsResult = parseGenericImplicitMultExpressionRhs(), + isImplicit = true + ) + } else null + } + // Not a match to the expression. + is PositiveInteger, is PositiveRealNumber, is EqualsSymbol, is ExponentiationSymbol, + is InvalidToken, is MinusSymbol, is PlusSymbol, is RightParenthesisSymbol, + is IncompleteFunctionName, null -> null + } + } + } + + private fun parseGenericMultExpressionRhs(): MathParsingResult { + // generic_mult_expression_rhs = multiplication_operator , generic_exp_expression ; + return parseContext.consumeTokenOfType().maybeFail { + if (!parseContext.hasMoreTokens()) { + NoVariableOrNumberAfterBinaryOperatorError(MULTIPLY) + } else null + }.flatMap { + parseGenericExpExpression() + } + } + + private fun parseGenericDivExpressionRhs(): MathParsingResult { + // generic_div_expression_rhs = division_operator , generic_exp_expression ; + return parseContext.consumeTokenOfType().maybeFail { + if (!parseContext.hasMoreTokens()) { + NoVariableOrNumberAfterBinaryOperatorError(DIVIDE) + } else null + }.flatMap { + parseGenericExpExpression() + } + } + + private fun parseGenericImplicitMultExpressionRhs(): MathParsingResult { + // generic_implicit_mult_expression_rhs is either numeric_implicit_mult_expression_rhs or + // algebraic_implicit_mult_or_exp_expression_rhs depending on the current parser context. + return when (parseContext) { + is NumericExpressionContext -> parseNumericImplicitMultExpressionRhs() + is AlgebraicExpressionContext -> parseAlgebraicImplicitMultOrExpExpressionRhs() + } + } + + private fun parseNumericImplicitMultExpressionRhs(): MathParsingResult { + // numeric_implicit_mult_expression_rhs = generic_term_without_unary_without_number ; + return parseGenericTermWithoutUnaryWithoutNumber() + } + + private fun parseAlgebraicImplicitMultOrExpExpressionRhs(): MathParsingResult { + // algebraic_implicit_mult_or_exp_expression_rhs = + // generic_term_without_unary_without_number , [ generic_exp_expression_tail ] ; + val possibleLhs = parseGenericTermWithoutUnaryWithoutNumber() + return if (parseContext.hasNextTokenOfType()) { + parseGenericExpExpressionTail(possibleLhs) + } else possibleLhs + } + + private fun parseGenericExpExpression(): MathParsingResult { + // generic_exp_expression = generic_term_with_unary , [ generic_exp_expression_tail ] ; + val possibleLhs = parseGenericTermWithUnary() + return if (parseContext.hasNextTokenOfType()) { + parseGenericExpExpressionTail(possibleLhs) + } else possibleLhs + } + + // Use tail recursion so that the last exponentiation is evaluated first, and right-to-left + // associativity can be kept via backtracking. + private fun parseGenericExpExpressionTail( + lhsResult: MathParsingResult + ): MathParsingResult { + // generic_exp_expression_tail = exponentiation_operator , generic_exp_expression ; + return BinaryOperationRhs( + operator = EXPONENTIATE, + rhsResult = lhsResult.flatMap { + parseContext.consumeTokenOfType() + }.maybeFail { + if (!parseContext.hasMoreTokens()) { + NoVariableOrNumberAfterBinaryOperatorError(EXPONENTIATE) + } else null + }.flatMap { + parseGenericExpExpression() + } + ).computeBinaryOperationExpression(lhsResult) + } + + private fun parseGenericTermWithUnary(): MathParsingResult { + // generic_term_with_unary = + // number | generic_term_without_unary_without_number | generic_plus_minus_unary_term ; + return when (val nextToken = parseContext.peekToken()) { + is MinusSymbol, is PlusSymbol -> parseGenericPlusMinusUnaryTerm() + is PositiveInteger, is PositiveRealNumber -> parseNumber().takeUnless { + parseContext.hasNextTokenOfType() || + parseContext.hasNextTokenOfType() + } ?: SpacesBetweenNumbersError.toFailure() + is FunctionName, is LeftParenthesisSymbol, is SquareRootSymbol -> + parseGenericTermWithoutUnaryWithoutNumber() + is VariableName -> { + if (parseContext is AlgebraicExpressionContext) { + parseGenericTermWithoutUnaryWithoutNumber() + } else VariableInNumericExpressionError.toFailure() + } + is DivideSymbol, is ExponentiationSymbol, is MultiplySymbol -> { + val previousToken = parseContext.getPreviousToken() + when { + previousToken is BinaryOperatorToken -> { + SubsequentBinaryOperatorsError( + operator1 = parseContext.extractSubexpression(previousToken), + operator2 = parseContext.extractSubexpression(nextToken) + ).toFailure() + } + nextToken is BinaryOperatorToken -> { + NoVariableOrNumberBeforeBinaryOperatorError( + operator = nextToken.getBinaryOperator() + ).toFailure() + } + else -> GenericError.toFailure() + } + } + is EqualsSymbol -> { + if (parseContext is AlgebraicExpressionContext && parseContext.isPartOfEquation) { + EquationHasWrongNumberOfEqualsError.toFailure() + } else GenericError.toFailure() + } + is IncompleteFunctionName -> nextToken.toFailure() + is InvalidToken -> nextToken.toFailure() + is RightParenthesisSymbol, null -> GenericError.toFailure() + } + } + + private fun parseGenericTermWithoutUnaryWithoutNumber(): MathParsingResult { + // generic_term_without_unary_without_number is either numeric_term_without_unary_without_number + // or algebraic_term_without_unary_without_number based the current parser context. + return when (parseContext) { + is NumericExpressionContext -> parseNumericTermWithoutUnaryWithoutNumber() + is AlgebraicExpressionContext -> parseAlgebraicTermWithoutUnaryWithoutNumber() + } + } + + private fun parseNumericTermWithoutUnaryWithoutNumber(): MathParsingResult { + // numeric_term_without_unary_without_number = + // generic_function_expression | generic_group_expression | generic_rooted_term ; + return when (val nextToken = parseContext.peekToken()) { + is FunctionName -> parseGenericFunctionExpression() + is LeftParenthesisSymbol -> parseGenericGroupExpression() + is SquareRootSymbol -> parseGenericRootedTerm() + is VariableName -> VariableInNumericExpressionError.toFailure() + is PositiveInteger, is PositiveRealNumber, is DivideSymbol, is EqualsSymbol, + is ExponentiationSymbol, is MinusSymbol, is MultiplySymbol, is PlusSymbol, + is RightParenthesisSymbol, null -> GenericError.toFailure() + is IncompleteFunctionName -> nextToken.toFailure() + is InvalidToken -> nextToken.toFailure() + } + } + + private fun parseAlgebraicTermWithoutUnaryWithoutNumber(): MathParsingResult { + // algebraic_term_without_unary_without_number = + // generic_function_expression | generic_group_expression | generic_rooted_term | variable ; + return when (val nextToken = parseContext.peekToken()) { + is FunctionName -> parseGenericFunctionExpression() + is LeftParenthesisSymbol -> parseGenericGroupExpression() + is SquareRootSymbol -> parseGenericRootedTerm() + is VariableName -> parseVariable() + is PositiveInteger, is PositiveRealNumber, is DivideSymbol, is EqualsSymbol, + is ExponentiationSymbol, is MinusSymbol, is MultiplySymbol, is PlusSymbol, + is RightParenthesisSymbol, null -> GenericError.toFailure() + is IncompleteFunctionName -> nextToken.toFailure() + is InvalidToken -> nextToken.toFailure() + } + } + + private fun parseGenericFunctionExpression(): MathParsingResult { + // generic_function_expression = function_name , left_paren , generic_expression , right_paren ; + val funcNameResult = + parseContext.consumeTokenOfType().maybeFail { functionName -> + when { + !functionName.isAllowedFunction -> InvalidFunctionInUseError(functionName.parsedName) + functionName.parsedName == "sqrt" -> null + else -> GenericError + } + }.also { + parseContext.consumeTokenOfType() + } + val argResult = funcNameResult.flatMap { parseGenericExpression() } + val rightParenResult = + argResult.flatMap { + parseContext.consumeTokenOfType { UnbalancedParenthesesError } + } + return funcNameResult.combineWith(argResult, rightParenResult) { funcName, arg, rightParen -> + MathExpression.newBuilder().apply { + parseStartIndex = funcName.startIndex + parseEndIndex = rightParen.endIndex + functionCall = MathFunctionCall.newBuilder().apply { + functionType = MathFunctionCall.FunctionType.SQUARE_ROOT + argument = arg + }.build() + }.build() + } + } + + private fun parseGenericGroupExpression(): MathParsingResult { + // generic_group_expression = left_paren , generic_expression , right_paren ; + val leftParenResult = parseContext.consumeTokenOfType() + val expResult = + leftParenResult.flatMap { + if (parseContext.hasMoreTokens()) { + parseGenericExpression() + } else UnbalancedParenthesesError.toFailure() + } + val rightParenResult = + expResult.flatMap { + parseContext.consumeTokenOfType { UnbalancedParenthesesError } + } + return leftParenResult.combineWith(expResult, rightParenResult) { leftParen, exp, rightParen -> + MathExpression.newBuilder().apply { + parseStartIndex = leftParen.startIndex + parseEndIndex = rightParen.endIndex + group = exp + }.build() + } + } + + private fun parseGenericPlusMinusUnaryTerm(): MathParsingResult { + // generic_plus_minus_unary_term = generic_negated_term | generic_positive_term ; + return when (val nextToken = parseContext.peekToken()) { + is MinusSymbol -> parseGenericNegatedTerm() + is PlusSymbol -> parseGenericPositiveTerm() + is PositiveInteger, is PositiveRealNumber, is DivideSymbol, is EqualsSymbol, + is ExponentiationSymbol, is FunctionName, is LeftParenthesisSymbol, is MultiplySymbol, + is RightParenthesisSymbol, is SquareRootSymbol, is VariableName, null -> + GenericError.toFailure() + is IncompleteFunctionName -> nextToken.toFailure() + is InvalidToken -> nextToken.toFailure() + } + } + + private fun parseGenericNegatedTerm(): MathParsingResult { + // generic_negated_term = minus_operator , generic_mult_div_expression ; + val minusResult = parseContext.consumeTokenOfType() + val expResult = minusResult.flatMap { parseGenericMultDivExpression() } + return minusResult.combineWith(expResult) { minus, op -> + MathExpression.newBuilder().apply { + parseStartIndex = minus.startIndex + parseEndIndex = op.parseEndIndex + unaryOperation = MathUnaryOperation.newBuilder().apply { + operator = NEGATE + operand = op + }.build() + }.build() + } + } + + private fun parseGenericPositiveTerm(): MathParsingResult { + // generic_positive_term = plus_operator , generic_mult_div_expression ; + val plusResult = parseContext.consumeTokenOfType() + val expResult = plusResult.flatMap { parseGenericMultDivExpression() } + return plusResult.combineWith(expResult) { plus, op -> + MathExpression.newBuilder().apply { + parseStartIndex = plus.startIndex + parseEndIndex = op.parseEndIndex + unaryOperation = MathUnaryOperation.newBuilder().apply { + operator = POSITIVE + operand = op + }.build() + }.build() + } + } + + private fun parseGenericRootedTerm(): MathParsingResult { + // generic_rooted_term = square_root_operator , generic_term_with_unary ; + val sqrtResult = + parseContext.consumeTokenOfType().maybeFail { + if (!parseContext.hasMoreTokens()) HangingSquareRootError else null + } + val expResult = sqrtResult.flatMap { parseGenericTermWithUnary() } + return sqrtResult.combineWith(expResult) { sqrtSymbol, op -> + MathExpression.newBuilder().apply { + parseStartIndex = sqrtSymbol.startIndex + parseEndIndex = op.parseEndIndex + functionCall = MathFunctionCall.newBuilder().apply { + functionType = MathFunctionCall.FunctionType.SQUARE_ROOT + argument = op + }.build() + }.build() + } + } + + private fun parseNumber(): MathParsingResult { + // number = positive_real_number | positive_integer ; + return when (val nextToken = parseContext.peekToken()) { + is PositiveInteger -> { + parseContext.consumeTokenOfType().map { positiveInteger -> + MathExpression.newBuilder().apply { + parseStartIndex = positiveInteger.startIndex + parseEndIndex = positiveInteger.endIndex + constant = positiveInteger.toReal() + }.build() + } + } + is PositiveRealNumber -> { + parseContext.consumeTokenOfType().map { positiveRealNumber -> + MathExpression.newBuilder().apply { + parseStartIndex = positiveRealNumber.startIndex + parseEndIndex = positiveRealNumber.endIndex + constant = positiveRealNumber.toReal() + }.build() + } + } + is DivideSymbol, is EqualsSymbol, is ExponentiationSymbol, is FunctionName, + is LeftParenthesisSymbol, is MinusSymbol, is MultiplySymbol, is PlusSymbol, + is RightParenthesisSymbol, is SquareRootSymbol, is VariableName, null -> + GenericError.toFailure() + is IncompleteFunctionName -> nextToken.toFailure() + is InvalidToken -> nextToken.toFailure() + } + } + + private fun parseVariable(): MathParsingResult { + val variableNameResult = + parseContext.consumeTokenOfType().maybeFail { + if (!parseContext.allowsVariables()) GenericError else null + }.maybeFail { variableName -> + return@maybeFail if (parseContext.hasMoreTokens()) { + when (val nextToken = parseContext.peekToken()) { + is PositiveInteger -> + NumberAfterVariableError(nextToken.toReal(), variableName.parsedName) + is PositiveRealNumber -> + NumberAfterVariableError(nextToken.toReal(), variableName.parsedName) + else -> null + } + } else null + } + return variableNameResult.map { variableName -> + MathExpression.newBuilder().apply { + parseStartIndex = variableName.startIndex + parseEndIndex = variableName.endIndex + variable = variableName.parsedName + }.build() + } + } + + private fun parseGenericBinaryExpression( + parseLhs: () -> MathParsingResult, + parseRhs: (Token?) -> BinaryOperationRhs? + ): MathParsingResult { + var lastLhsResult = parseLhs() + while (!lastLhsResult.isFailure()) { + // Compute the next LHS if there are further RHS expressions. + lastLhsResult = + parseRhs(parseContext.peekToken()) + ?.computeBinaryOperationExpression(lastLhsResult) + ?: break // Not a match to the expression. + } + return lastLhsResult + } + + private fun checkForLearnerErrors(expression: MathExpression): MathParsingError? { + val firstMultiRedundantGroup = expression.findFirstMultiRedundantGroup() + val nextRedundantGroup = expression.findNextRedundantGroup() + val nextUnaryOperation = expression.findNextRedundantUnaryOperation() + val nextExpWithVariableExp = expression.findNextExponentiationWithVariablePower() + val nextExpWithTooLargePower = expression.findNextExponentiationWithTooLargePower() + val nextExpWithNestedExp = expression.findNextNestedExponentiation() + val nextDivByZero = expression.findNextDivisionByZero() + val disallowedVariables = expression.findAllDisallowedVariables(parseContext) + // Note that the order of checks here is important since errors have precedence, and some are + // redundant and, in the wrong order, may cause the wrong error to be returned. + val includeOptionalErrors = parseContext.errorCheckingMode.includesOptionalErrors() + return when { + includeOptionalErrors && firstMultiRedundantGroup != null -> { + val subExpression = parseContext.extractSubexpression(firstMultiRedundantGroup) + MultipleRedundantParenthesesError(subExpression, firstMultiRedundantGroup) + } + includeOptionalErrors && expression.expressionTypeCase == GROUP -> + SingleRedundantParenthesesError(parseContext.rawExpression, expression) + includeOptionalErrors && nextRedundantGroup != null -> { + val subExpression = parseContext.extractSubexpression(nextRedundantGroup) + RedundantParenthesesForIndividualTermsError(subExpression, nextRedundantGroup) + } + includeOptionalErrors && nextUnaryOperation != null -> SubsequentUnaryOperatorsError + includeOptionalErrors && nextExpWithVariableExp != null -> ExponentIsVariableExpressionError + includeOptionalErrors && nextExpWithTooLargePower != null -> ExponentTooLargeError + includeOptionalErrors && nextExpWithNestedExp != null -> NestedExponentsError + includeOptionalErrors && nextDivByZero != null -> TermDividedByZeroError + includeOptionalErrors && disallowedVariables.isNotEmpty() -> + DisabledVariablesInUseError(disallowedVariables.toList()) + else -> ensureNoRemainingTokens() + } + } + + private fun ensureNoRemainingTokens(): MathParsingError? { + // Make sure all tokens were consumed (otherwise there are trailing tokens which invalidate the + // whole grammar). + return if (parseContext.hasMoreTokens()) { + when (val nextToken = parseContext.peekToken()) { + is LeftParenthesisSymbol, is RightParenthesisSymbol -> UnbalancedParenthesesError + is EqualsSymbol -> { + if (parseContext is AlgebraicExpressionContext && parseContext.isPartOfEquation) { + EquationHasWrongNumberOfEqualsError + } else GenericError + } + is IncompleteFunctionName -> nextToken.toError() + is InvalidToken -> nextToken.toError() + is PositiveInteger, is PositiveRealNumber, is DivideSymbol, is ExponentiationSymbol, + is FunctionName, is MinusSymbol, is MultiplySymbol, is PlusSymbol, is SquareRootSymbol, + is VariableName, null -> GenericError + } + } else null + } + + private fun PositiveInteger.toReal(): Real = Real.newBuilder().apply { + integer = parsedValue + }.build() + + private fun PositiveRealNumber.toReal(): Real = Real.newBuilder().apply { + irrational = parsedValue + }.build() + + @Suppress("unused") // The receiver is behaving as a namespace. + private fun IncompleteFunctionName.toError(): MathParsingError = FunctionNameIncompleteError + + private fun InvalidToken.toError(): MathParsingError = + UnnecessarySymbolsError(parseContext.extractSubexpression(this)) + + private fun IncompleteFunctionName.toFailure(): MathParsingResult = toError().toFailure() + + private fun InvalidToken.toFailure(): MathParsingResult = toError().toFailure() + + private sealed class ParseContext(val rawExpression: String) { + val tokens: PeekableIterator by lazy { + PeekableIterator.fromSequence(MathTokenizer.tokenize(rawExpression)) + } + private var previousToken: Token? = null + + abstract val errorCheckingMode: ErrorCheckingMode + + abstract fun allowsVariables(): Boolean + + fun hasMoreTokens(): Boolean = tokens.hasNext() + + fun peekToken(): Token? = tokens.peek() + + /** + * Returns the last token consumed by [consumeTokenOfType], or null if none. Note: this should + * only be used for error reporting purposes, not for parsing. Using this for parsing would, in + * certain cases, allow for a non-LL(1) grammar which is against one design goal for this + * parser. + */ + fun getPreviousToken(): Token? = previousToken + + inline fun hasNextTokenOfType(): Boolean = peekToken() is T + + inline fun consumeTokenOfType( + missingError: () -> MathParsingError = { GenericError } + ): MathParsingResult { + val maybeToken = tokens.expectNextMatches { it is T } as? T + return maybeToken?.let { token -> + previousToken = token + MathParsingResult.Success(token) + } ?: missingError().toFailure() + } + + fun extractSubexpression(token: Token): String { + return rawExpression.substring(token.startIndex, token.endIndex) + } + + fun extractSubexpression(expression: MathExpression): String { + return rawExpression.substring(expression.parseStartIndex, expression.parseEndIndex) + } + + class NumericExpressionContext( + rawExpression: String, + override val errorCheckingMode: ErrorCheckingMode + ) : ParseContext(rawExpression) { + // Numeric expressions never allow variables. + override fun allowsVariables(): Boolean = false + } + + class AlgebraicExpressionContext( + rawExpression: String, + val isPartOfEquation: Boolean, + private val allowedVariables: List, + override val errorCheckingMode: ErrorCheckingMode + ) : ParseContext(rawExpression) { + fun allowsVariable(variableName: String): Boolean = variableName in allowedVariables + + override fun allowsVariables(): Boolean = true + } + } + + companion object { + enum class ErrorCheckingMode { + REQUIRED_ONLY, + ALL_ERRORS + } + + sealed class MathParsingResult { + data class Success(val result: T) : MathParsingResult() + + data class Failure(val error: MathParsingError) : MathParsingResult() + } + + fun parseNumericExpression( + rawExpression: String, + errorCheckingMode: ErrorCheckingMode = ErrorCheckingMode.ALL_ERRORS + ): MathParsingResult = + createNumericParser(rawExpression, errorCheckingMode).parseGenericExpressionGrammar() + + fun parseAlgebraicExpression( + rawExpression: String, + allowedVariables: List, + errorCheckingMode: ErrorCheckingMode = ErrorCheckingMode.ALL_ERRORS + ): MathParsingResult { + return createAlgebraicParser( + rawExpression, isPartOfEquation = false, allowedVariables, errorCheckingMode + ).parseGenericExpressionGrammar() + } + + fun parseAlgebraicEquation( + rawExpression: String, + allowedVariables: List, + errorCheckingMode: ErrorCheckingMode = ErrorCheckingMode.ALL_ERRORS + ): MathParsingResult { + return createAlgebraicParser( + rawExpression, isPartOfEquation = true, allowedVariables, errorCheckingMode + ).parseGenericEquationGrammar() + } + + private fun createNumericParser( + rawExpression: String, + errorCheckingMode: ErrorCheckingMode + ): MathExpressionParser = + MathExpressionParser(NumericExpressionContext(rawExpression, errorCheckingMode)) + + private fun createAlgebraicParser( + rawExpression: String, + isPartOfEquation: Boolean, + allowedVariables: List, + errorCheckingMode: ErrorCheckingMode + ): MathExpressionParser { + return MathExpressionParser( + AlgebraicExpressionContext( + rawExpression, isPartOfEquation, allowedVariables, errorCheckingMode + ) + ) + } + + private fun ErrorCheckingMode.includesOptionalErrors() = this == ErrorCheckingMode.ALL_ERRORS + + private fun MathParsingError.toFailure(): MathParsingResult = + MathParsingResult.Failure(this) + + private fun MathParsingResult.isFailure() = this is MathParsingResult.Failure + + /** + * Maps [this] result to a new value. Note that this lazily uses the provided function (i.e. + * it's only used if [this] result is passing, otherwise the method will short-circuit a failure + * state so that [this] result's failure is preserved). + * + * @param operation computes a new success result given the current successful result value + * @return a new [MathParsingResult] with a successful result provided by the operation, or the + * preserved failure of [this] result + */ + private fun MathParsingResult.map( + operation: (T1) -> T2 + ): MathParsingResult = flatMap { result -> MathParsingResult.Success(operation(result)) } + + /** + * Maps [this] result to a new value. Note that this lazily uses the provided function (i.e. + * it's only used if [this] result is passing, otherwise the method will short-circuit a failure + * state so that [this] result's failure is preserved). + * + * @param operation computes a new result (either a success or failure) given the current + * successful result value + * @return a new [MathParsingResult] with either a result provided by the operation, or the + * preserved failure of [this] result + */ + private fun MathParsingResult.flatMap( + operation: (T1) -> MathParsingResult + ): MathParsingResult { + return when (this) { + is MathParsingResult.Success -> operation(result) + is MathParsingResult.Failure -> error.toFailure() + } + } + + /** + * Potentially changes [this] result into a failure based on the provided [operation]. Note that + * this function lazily uses the operation (i.e. it's only called if [this] result is in a + * passing state), and the returned result will only be in a failing state if [operation] + * returns a non-null error. + * + * @param operation computes a failure error, or null if no error was determined, given the + * current successful result value + * @return either [this] or a failing result if [operation] was called & returned a non-null + * error + */ + private fun MathParsingResult.maybeFail( + operation: (T) -> MathParsingError? + ): MathParsingResult = flatMap { result -> operation(result)?.toFailure() ?: this } + + /** + * Calls an operation if [this] operation isn't already failing, and returns a failure only if + * that operation's result is a failure (otherwise returns [this] result). This function can be + * useful to ensure that subsequent operations are successful even when those operations' + * results are never directly used. + * + * @param operation computes a new result that, when failing, will result in a failing result + * returned from this function. This is only called if [this] result is currently + * successful. + * @return either [this] (iff either this result is failing, or the result of [operation] is a + * success), or the failure returned by [operation] + */ + private fun MathParsingResult.also( + operation: () -> MathParsingResult + ): MathParsingResult = flatMap { + when (val other = operation()) { + is MathParsingResult.Success -> this + is MathParsingResult.Failure -> other.error.toFailure() + } + } + + /** + * Combines [this] result with another result, given a specific combination function. + * + * @param other the result to combine with [this] result + * @param combine computes a new value given the result from [this] and [other]. Note that this + * is only called if both results are successful, and the corresponding successful values + * are provided in-order ([this] result's value is the first parameter, and [other]'s is the + * second). + * @return either [this] result's or [other]'s failure, if either are failing, or a successful + * result containing the value computed by [combine] + */ + private fun MathParsingResult.combineWith( + other: MathParsingResult, + combine: (I1, I2) -> O, + ): MathParsingResult { + return flatMap { result -> + other.map { otherResult -> + combine(result, otherResult) + } + } + } + + /** + * Performs the same operation as the other [combineWith] function, except with three + * [MathParsingResult]s, instead. + */ + private fun MathParsingResult.combineWith( + other1: MathParsingResult, + other2: MathParsingResult, + combine: (I1, I2, I3) -> O, + ): MathParsingResult { + return flatMap { result -> + other1.flatMap { otherResult1 -> + other2.map { otherResult2 -> + combine(result, otherResult1, otherResult2) + } + } + } + } + + private data class BinaryOperationRhs( + val operator: MathBinaryOperation.Operator, + val rhsResult: MathParsingResult, + val isImplicit: Boolean = false + ) { + fun computeBinaryOperationExpression( + lhsResult: MathParsingResult + ): MathParsingResult { + return lhsResult.combineWith(rhsResult) { lhs, rhs -> + MathExpression.newBuilder().apply { + parseStartIndex = lhs.parseStartIndex + parseEndIndex = rhs.parseEndIndex + binaryOperation = MathBinaryOperation.newBuilder().apply { + operator = this@BinaryOperationRhs.operator + leftOperand = lhs + rightOperand = rhs + isImplicit = this@BinaryOperationRhs.isImplicit + }.build() + }.build() + } + } + } + + private fun MathExpression.findFirstMultiRedundantGroup(): MathExpression? { + return when (expressionTypeCase) { + BINARY_OPERATION -> { + binaryOperation.leftOperand.findFirstMultiRedundantGroup() + ?: binaryOperation.rightOperand.findFirstMultiRedundantGroup() + } + UNARY_OPERATION -> unaryOperation.operand.findFirstMultiRedundantGroup() + FUNCTION_CALL -> functionCall.argument.findFirstMultiRedundantGroup() + GROUP -> + group.takeIf { it.expressionTypeCase == GROUP } + ?: group.findFirstMultiRedundantGroup() + CONSTANT, VARIABLE, EXPRESSIONTYPE_NOT_SET, null -> null + } + } + + private fun MathExpression.findNextRedundantGroup(): MathExpression? { + return when (expressionTypeCase) { + BINARY_OPERATION -> { + binaryOperation.leftOperand.findNextRedundantGroup() + ?: binaryOperation.rightOperand.findNextRedundantGroup() + } + UNARY_OPERATION -> unaryOperation.operand.findNextRedundantGroup() + FUNCTION_CALL -> functionCall.argument.findNextRedundantGroup() + GROUP -> group.takeIf { + it.expressionTypeCase in listOf(CONSTANT, VARIABLE) + } ?: group.findNextRedundantGroup() + CONSTANT, VARIABLE, EXPRESSIONTYPE_NOT_SET, null -> null + } + } + + private fun MathExpression.findNextRedundantUnaryOperation(): MathExpression? { + return when (expressionTypeCase) { + BINARY_OPERATION -> { + binaryOperation.leftOperand.findNextRedundantUnaryOperation() + ?: binaryOperation.rightOperand.findNextRedundantUnaryOperation() + } + UNARY_OPERATION -> unaryOperation.operand.takeIf { + it.expressionTypeCase == UNARY_OPERATION + } ?: unaryOperation.operand.findNextRedundantUnaryOperation() + FUNCTION_CALL -> functionCall.argument.findNextRedundantUnaryOperation() + GROUP -> group.findNextRedundantUnaryOperation() + CONSTANT, VARIABLE, EXPRESSIONTYPE_NOT_SET, null -> null + } + } + + private fun MathExpression.findNextExponentiationWithVariablePower(): MathExpression? { + return when (expressionTypeCase) { + BINARY_OPERATION -> { + takeIf { + binaryOperation.operator == EXPONENTIATE && + binaryOperation.rightOperand.isVariableExpression() + } ?: binaryOperation.leftOperand.findNextExponentiationWithVariablePower() + ?: binaryOperation.rightOperand.findNextExponentiationWithVariablePower() + } + UNARY_OPERATION -> unaryOperation.operand.findNextExponentiationWithVariablePower() + FUNCTION_CALL -> functionCall.argument.findNextExponentiationWithVariablePower() + GROUP -> group.findNextExponentiationWithVariablePower() + CONSTANT, VARIABLE, EXPRESSIONTYPE_NOT_SET, null -> null + } + } + + private fun MathExpression.findNextExponentiationWithTooLargePower(): MathExpression? { + return when (expressionTypeCase) { + BINARY_OPERATION -> { + takeIf { + binaryOperation.operator == EXPONENTIATE && + binaryOperation.rightOperand.expressionTypeCase == CONSTANT && + binaryOperation.rightOperand.constant.toDouble() > 5.0 + } ?: binaryOperation.leftOperand.findNextExponentiationWithTooLargePower() + ?: binaryOperation.rightOperand.findNextExponentiationWithTooLargePower() + } + UNARY_OPERATION -> unaryOperation.operand.findNextExponentiationWithTooLargePower() + FUNCTION_CALL -> functionCall.argument.findNextExponentiationWithTooLargePower() + GROUP -> group.findNextExponentiationWithTooLargePower() + CONSTANT, VARIABLE, EXPRESSIONTYPE_NOT_SET, null -> null + } + } + + private fun MathExpression.findNextNestedExponentiation(): MathExpression? { + return when (expressionTypeCase) { + BINARY_OPERATION -> { + takeIf { + binaryOperation.operator == EXPONENTIATE && + binaryOperation.rightOperand.containsExponentiation() + } ?: binaryOperation.leftOperand.findNextNestedExponentiation() + ?: binaryOperation.rightOperand.findNextNestedExponentiation() + } + UNARY_OPERATION -> unaryOperation.operand.findNextNestedExponentiation() + FUNCTION_CALL -> functionCall.argument.findNextNestedExponentiation() + GROUP -> group.findNextNestedExponentiation() + CONSTANT, VARIABLE, EXPRESSIONTYPE_NOT_SET, null -> null + } + } + + private fun MathExpression.findNextDivisionByZero(): MathExpression? { + return when (expressionTypeCase) { + BINARY_OPERATION -> { + takeIf { + binaryOperation.operator == DIVIDE && + binaryOperation.rightOperand.expressionTypeCase == CONSTANT && + binaryOperation.rightOperand.constant + .toDouble().absoluteValue.approximatelyEquals(0.0) + } ?: binaryOperation.leftOperand.findNextDivisionByZero() + ?: binaryOperation.rightOperand.findNextDivisionByZero() + } + UNARY_OPERATION -> unaryOperation.operand.findNextDivisionByZero() + FUNCTION_CALL -> functionCall.argument.findNextDivisionByZero() + GROUP -> group.findNextDivisionByZero() + CONSTANT, VARIABLE, EXPRESSIONTYPE_NOT_SET, null -> null + } + } + + private fun MathExpression.findAllDisallowedVariables(context: ParseContext): Set { + return if (context is AlgebraicExpressionContext) { + findAllDisallowedVariablesAux(context) + } else setOf() + } + + private fun MathExpression.findAllDisallowedVariablesAux( + context: AlgebraicExpressionContext + ): Set { + return when (expressionTypeCase) { + VARIABLE -> if (context.allowsVariable(variable)) setOf() else setOf(variable) + BINARY_OPERATION -> { + binaryOperation.leftOperand.findAllDisallowedVariablesAux(context) + + binaryOperation.rightOperand.findAllDisallowedVariablesAux(context) + } + UNARY_OPERATION -> unaryOperation.operand.findAllDisallowedVariablesAux(context) + FUNCTION_CALL -> functionCall.argument.findAllDisallowedVariablesAux(context) + GROUP -> group.findAllDisallowedVariablesAux(context) + CONSTANT, EXPRESSIONTYPE_NOT_SET, null -> setOf() + } + } + + private fun MathExpression.isVariableExpression(): Boolean { + return when (expressionTypeCase) { + VARIABLE -> true + BINARY_OPERATION -> { + binaryOperation.leftOperand.isVariableExpression() || + binaryOperation.rightOperand.isVariableExpression() + } + UNARY_OPERATION -> unaryOperation.operand.isVariableExpression() + FUNCTION_CALL -> functionCall.argument.isVariableExpression() + GROUP -> group.isVariableExpression() + CONSTANT, EXPRESSIONTYPE_NOT_SET, null -> false + } + } + + private fun MathExpression.containsExponentiation(): Boolean { + return when (expressionTypeCase) { + BINARY_OPERATION -> { + binaryOperation.operator == EXPONENTIATE || + binaryOperation.leftOperand.containsExponentiation() || + binaryOperation.rightOperand.containsExponentiation() + } + UNARY_OPERATION -> unaryOperation.operand.containsExponentiation() + FUNCTION_CALL -> functionCall.argument.containsExponentiation() + GROUP -> group.containsExponentiation() + CONSTANT, VARIABLE, EXPRESSIONTYPE_NOT_SET, null -> false + } + } + } +} diff --git a/utility/src/main/java/org/oppia/android/util/math/MathParsingError.kt b/utility/src/main/java/org/oppia/android/util/math/MathParsingError.kt new file mode 100644 index 00000000000..44fd1debb4a --- /dev/null +++ b/utility/src/main/java/org/oppia/android/util/math/MathParsingError.kt @@ -0,0 +1,69 @@ +package org.oppia.android.util.math + +import org.oppia.android.app.model.MathBinaryOperation +import org.oppia.android.app.model.MathExpression +import org.oppia.android.app.model.Real + +sealed class MathParsingError { + object SpacesBetweenNumbersError : MathParsingError() + + object UnbalancedParenthesesError : MathParsingError() + + data class SingleRedundantParenthesesError( + val rawExpression: String, + val expression: MathExpression + ) : MathParsingError() + + data class MultipleRedundantParenthesesError( + val rawExpression: String, + val expression: MathExpression + ) : MathParsingError() + + data class RedundantParenthesesForIndividualTermsError( + val rawExpression: String, + val expression: MathExpression + ) : MathParsingError() + + data class UnnecessarySymbolsError(val invalidSymbol: String) : MathParsingError() + + data class NumberAfterVariableError(val number: Real, val variable: String) : MathParsingError() + + data class SubsequentBinaryOperatorsError( + val operator1: String, + val operator2: String + ) : MathParsingError() + + object SubsequentUnaryOperatorsError : MathParsingError() + + data class NoVariableOrNumberBeforeBinaryOperatorError( + val operator: MathBinaryOperation.Operator + ) : MathParsingError() + + data class NoVariableOrNumberAfterBinaryOperatorError( + val operator: MathBinaryOperation.Operator + ) : MathParsingError() + + object ExponentIsVariableExpressionError : MathParsingError() + + object ExponentTooLargeError : MathParsingError() + + object NestedExponentsError : MathParsingError() + + object HangingSquareRootError : MathParsingError() + + object TermDividedByZeroError : MathParsingError() + + object VariableInNumericExpressionError : MathParsingError() + + data class DisabledVariablesInUseError(val variables: List) : MathParsingError() + + object EquationHasWrongNumberOfEqualsError : MathParsingError() + + object EquationMissingLhsOrRhsError : MathParsingError() + + data class InvalidFunctionInUseError(val functionName: String) : MathParsingError() + + object FunctionNameIncompleteError : MathParsingError() + + object GenericError : MathParsingError() +} diff --git a/utility/src/test/java/org/oppia/android/util/math/AlgebraicEquationParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/AlgebraicEquationParserTest.kt new file mode 100644 index 00000000000..94fc6e50ab4 --- /dev/null +++ b/utility/src/test/java/org/oppia/android/util/math/AlgebraicEquationParserTest.kt @@ -0,0 +1,227 @@ +package org.oppia.android.util.math + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.oppia.android.app.model.MathEquation +import org.oppia.android.testing.math.MathEquationSubject.Companion.assertThat +import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode +import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult +import org.robolectric.annotation.LooperMode + +/** Tests for [MathExpressionParser]. */ +@RunWith(AndroidJUnit4::class) +@LooperMode(LooperMode.Mode.PAUSED) +class AlgebraicEquationParserTest { + @Test + fun testLotsOfCasesForAlgebraicEquation() { + expectFailureWhenParsingAlgebraicEquation(" x =") + expectFailureWhenParsingAlgebraicEquation(" = y") + + val equation1 = parseAlgebraicEquationSuccessfully("x = 1") + assertThat(equation1).hasLeftHandSideThat().hasStructureThatMatches { + variable { + withNameThat().isEqualTo("x") + } + } + + val equation2 = + parseAlgebraicEquationSuccessfully( + "y = mx + b", allowedVariables = listOf("x", "y", "b", "m") + ) + assertThat(equation2).hasLeftHandSideThat().hasStructureThatMatches { + variable { + withNameThat().isEqualTo("y") + } + } + assertThat(equation2).hasRightHandSideThat().hasStructureThatMatches { + addition { + leftOperand { + multiplication { + leftOperand { + variable { + withNameThat().isEqualTo("m") + } + } + rightOperand { + variable { + withNameThat().isEqualTo("x") + } + } + } + } + rightOperand { + variable { + withNameThat().isEqualTo("b") + } + } + } + } + + val equation3 = parseAlgebraicEquationSuccessfully("y = (x+1)^2") + assertThat(equation3).hasLeftHandSideThat().hasStructureThatMatches { + variable { + withNameThat().isEqualTo("y") + } + } + assertThat(equation3).hasRightHandSideThat().hasStructureThatMatches { + exponentiation { + leftOperand { + group { + addition { + leftOperand { + variable { + withNameThat().isEqualTo("x") + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + } + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + + val equation4 = parseAlgebraicEquationSuccessfully("y = (x+1)(x-1)") + assertThat(equation4).hasLeftHandSideThat().hasStructureThatMatches { + variable { + withNameThat().isEqualTo("y") + } + } + assertThat(equation4).hasRightHandSideThat().hasStructureThatMatches { + multiplication { + leftOperand { + group { + addition { + leftOperand { + variable { + withNameThat().isEqualTo("x") + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + } + } + } + rightOperand { + group { + subtraction { + leftOperand { + variable { + withNameThat().isEqualTo("x") + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + } + } + } + } + } + + expectFailureWhenParsingAlgebraicEquation("y = (x+1)(x-1) 2") + expectFailureWhenParsingAlgebraicEquation("y 2 = (x+1)(x-1)") + + val equation5 = + parseAlgebraicEquationSuccessfully( + "a*x^2 + b*x + c = 0", allowedVariables = listOf("x", "a", "b", "c") + ) + assertThat(equation5).hasLeftHandSideThat().hasStructureThatMatches { + addition { + leftOperand { + addition { + leftOperand { + multiplication { + leftOperand { + variable { + withNameThat().isEqualTo("a") + } + } + rightOperand { + exponentiation { + leftOperand { + variable { + withNameThat().isEqualTo("x") + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + rightOperand { + multiplication { + leftOperand { + variable { + withNameThat().isEqualTo("b") + } + } + rightOperand { + variable { + withNameThat().isEqualTo("x") + } + } + } + } + } + } + rightOperand { + variable { + withNameThat().isEqualTo("c") + } + } + } + } + assertThat(equation5).hasRightHandSideThat().hasStructureThatMatches { + constant { + withValueThat().isIntegerThat().isEqualTo(0) + } + } + } + + private companion object { + // TODO: fix helper API. + + private fun expectFailureWhenParsingAlgebraicEquation(expression: String): MathParsingError { + val result = parseAlgebraicEquationWithAllErrors(expression) + assertThat(result).isInstanceOf(MathParsingResult.Failure::class.java) + return (result as MathParsingResult.Failure).error + } + + private fun parseAlgebraicEquationSuccessfully( + expression: String, + allowedVariables: List = listOf("x", "y", "z") + ): MathEquation { + val result = parseAlgebraicEquationWithAllErrors(expression, allowedVariables) + return (result as MathParsingResult.Success).result + } + + private fun parseAlgebraicEquationWithAllErrors( + expression: String, + allowedVariables: List = listOf("x", "y", "z") + ): MathParsingResult { + return MathExpressionParser.parseAlgebraicEquation( + expression, allowedVariables, ErrorCheckingMode.ALL_ERRORS + ) + } + } +} diff --git a/utility/src/test/java/org/oppia/android/util/math/AlgebraicExpressionParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/AlgebraicExpressionParserTest.kt new file mode 100644 index 00000000000..9fea4084970 --- /dev/null +++ b/utility/src/test/java/org/oppia/android/util/math/AlgebraicExpressionParserTest.kt @@ -0,0 +1,1939 @@ +package org.oppia.android.util.math + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.oppia.android.app.model.MathExpression +import org.oppia.android.app.model.MathFunctionCall.FunctionType.SQUARE_ROOT +import org.oppia.android.testing.math.MathExpressionSubject.Companion.assertThat +import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode +import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult +import org.robolectric.annotation.LooperMode + +/** Tests for [MathExpressionParser]. */ +@RunWith(AndroidJUnit4::class) +@LooperMode(LooperMode.Mode.PAUSED) +class AlgebraicExpressionParserTest { + @Test + fun testLotsOfCasesForAlgebraicExpression() { + // TODO: split this up + // TODO: add log string generation for expressions. + expectFailureWhenParsingAlgebraicExpression("") + + val expression1 = parseAlgebraicExpressionWithAllErrors("1") + assertThat(expression1).hasStructureThatMatches { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + + val expression61 = parseAlgebraicExpressionWithAllErrors("x") + assertThat(expression61).hasStructureThatMatches { + variable { + withNameThat().isEqualTo("x") + } + } + + val expression2 = parseAlgebraicExpressionWithAllErrors(" 2 ") + assertThat(expression2).hasStructureThatMatches { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + + val expression3 = parseAlgebraicExpressionWithAllErrors(" 2.5 ") + assertThat(expression3).hasStructureThatMatches { + constant { + withValueThat().isIrrationalThat().isWithin(1e-5).of(2.5) + } + } + + val expression62 = parseAlgebraicExpressionWithAllErrors(" y ") + assertThat(expression62).hasStructureThatMatches { + variable { + withNameThat().isEqualTo("y") + } + } + + val expression63 = parseAlgebraicExpressionWithAllErrors(" z x ") + assertThat(expression63).hasStructureThatMatches { + multiplication { + leftOperand { + variable { + withNameThat().isEqualTo("z") + } + } + rightOperand { + variable { + withNameThat().isEqualTo("x") + } + } + } + } + + val expression4 = parseAlgebraicExpressionWithoutOptionalErrors("2^3^2") + assertThat(expression4).hasStructureThatMatches { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + + val expression23 = parseAlgebraicExpressionWithAllErrors("(2^3)^2") + assertThat(expression23).hasStructureThatMatches { + exponentiation { + leftOperand { + group { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + + val expression24 = parseAlgebraicExpressionWithAllErrors("512/32/4") + assertThat(expression24).hasStructureThatMatches { + division { + leftOperand { + division { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(512) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(32) + } + } + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + + val expression25 = parseAlgebraicExpressionWithAllErrors("512/(32/4)") + assertThat(expression25).hasStructureThatMatches { + division { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(512) + } + } + rightOperand { + group { + division { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(32) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + } + + val expression5 = parseAlgebraicExpressionWithAllErrors("sqrt(2)") + assertThat(expression5).hasStructureThatMatches { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + + expectFailureWhenParsingAlgebraicExpression("sqr(2)") + + val expression64 = parseAlgebraicExpressionWithoutOptionalErrors("xyz(2)") + assertThat(expression64).hasStructureThatMatches { + multiplication { + leftOperand { + multiplication { + leftOperand { + multiplication { + leftOperand { + variable { + withNameThat().isEqualTo("x") + } + } + rightOperand { + variable { + withNameThat().isEqualTo("y") + } + } + } + } + rightOperand { + variable { + withNameThat().isEqualTo("z") + } + } + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + + val expression6 = parseAlgebraicExpressionWithAllErrors("732") + assertThat(expression6).hasStructureThatMatches { + constant { + withValueThat().isIntegerThat().isEqualTo(732) + } + } + + expectFailureWhenParsingAlgebraicExpression("73 2") + + // Verify order of operations between higher & lower precedent operators. + val expression32 = parseAlgebraicExpressionWithAllErrors("3+4^5") + assertThat(expression32).hasStructureThatMatches { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + } + } + } + } + + val expression7 = parseAlgebraicExpressionWithoutOptionalErrors("3*2-3+4^7*8/3*2+7") + assertThat(expression7).hasStructureThatMatches { + // To better visualize the precedence & order of operations, see this grouped version: + // (((3*2)-3)+((((4^7)*8)/3)*2))+7. + addition { + leftOperand { + // ((3*2)-3)+((((4^7)*8)/3)*2) + addition { + leftOperand { + // (1*2)-3 + subtraction { + leftOperand { + // 3*2 + multiplication { + leftOperand { + // 3 + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + // 2 + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + // 3 + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + rightOperand { + // (((4^7)*8)/3)*2 + multiplication { + leftOperand { + // ((4^7)*8)/3 + division { + leftOperand { + // (4^7)*8 + multiplication { + leftOperand { + // 4^7 + exponentiation { + leftOperand { + // 4 + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + rightOperand { + // 7 + constant { + withValueThat().isIntegerThat().isEqualTo(7) + } + } + } + } + rightOperand { + // 8 + constant { + withValueThat().isIntegerThat().isEqualTo(8) + } + } + } + } + rightOperand { + // 3 + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + rightOperand { + // 2 + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + rightOperand { + // 7 + constant { + withValueThat().isIntegerThat().isEqualTo(7) + } + } + } + } + + expectFailureWhenParsingAlgebraicExpression("x = √2 × 7 ÷ 4") + + val expression8 = parseAlgebraicExpressionWithAllErrors("(1+2)(3+4)") + assertThat(expression8).hasStructureThatMatches { + multiplication { + leftOperand { + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + rightOperand { + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + } + + // Right implicit multiplication of numbers isn't allowed. + expectFailureWhenParsingAlgebraicExpression("(1+2)2") + + val expression10 = parseAlgebraicExpressionWithAllErrors("2(1+2)") + assertThat(expression10).hasStructureThatMatches { + multiplication { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + + // Right implicit multiplication of numbers isn't allowed. + expectFailureWhenParsingAlgebraicExpression("sqrt(2)3") + + val expression12 = parseAlgebraicExpressionWithAllErrors("3sqrt(2)") + assertThat(expression12).hasStructureThatMatches { + multiplication { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + + val expression65 = parseAlgebraicExpressionWithAllErrors("xsqrt(2)") + assertThat(expression65).hasStructureThatMatches { + multiplication { + leftOperand { + variable { + withNameThat().isEqualTo("x") + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + + val expression13 = parseAlgebraicExpressionWithAllErrors("sqrt(2)*(1+2)*(3-2^5)") + assertThat(expression13).hasStructureThatMatches { + multiplication { + leftOperand { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + rightOperand { + group { + subtraction { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + } + } + } + } + } + } + } + + val expression58 = parseAlgebraicExpressionWithAllErrors("sqrt(2)(1+2)(3-2^5)") + assertThat(expression58).hasStructureThatMatches { + multiplication { + leftOperand { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + rightOperand { + group { + subtraction { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + } + } + } + } + } + } + } + + val expression14 = parseAlgebraicExpressionWithoutOptionalErrors("((3))") + assertThat(expression14).hasStructureThatMatches { + group { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + + val expression15 = parseAlgebraicExpressionWithoutOptionalErrors("++3") + assertThat(expression15).hasStructureThatMatches { + positive { + operand { + positive { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + } + + val expression16 = parseAlgebraicExpressionWithoutOptionalErrors("--4") + assertThat(expression16).hasStructureThatMatches { + negation { + operand { + negation { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + + val expression17 = parseAlgebraicExpressionWithAllErrors("1+-4") + assertThat(expression17).hasStructureThatMatches { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + negation { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + + val expression18 = parseAlgebraicExpressionWithAllErrors("1++4") + assertThat(expression18).hasStructureThatMatches { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + positive { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + + val expression19 = parseAlgebraicExpressionWithAllErrors("1--4") + assertThat(expression19).hasStructureThatMatches { + subtraction { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + negation { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + + expectFailureWhenParsingAlgebraicExpression("1-^-4") + + val expression20 = parseAlgebraicExpressionWithAllErrors("√2 × 7 ÷ 4") + assertThat(expression20).hasStructureThatMatches { + division { + leftOperand { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(7) + } + } + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + + expectFailureWhenParsingAlgebraicExpression("1+2 &asdf") + + val expression21 = parseAlgebraicExpressionWithAllErrors("sqrt(2)sqrt(3)sqrt(4)") + // Note that this tree demonstrates left associativity. + assertThat(expression21).hasStructureThatMatches { + multiplication { + leftOperand { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + + val expression22 = parseAlgebraicExpressionWithAllErrors("(1+2)(3-7^2)(5+-17)") + assertThat(expression22).hasStructureThatMatches { + multiplication { + leftOperand { + multiplication { + leftOperand { + // 1+2 + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + rightOperand { + // 3-7^2 + group { + subtraction { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(7) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + } + } + rightOperand { + // 5+-17 + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + rightOperand { + negation { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(17) + } + } + } + } + } + } + } + } + } + + val expression26 = parseAlgebraicExpressionWithAllErrors("3^-2") + assertThat(expression26).hasStructureThatMatches { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + negation { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + + val expression27 = parseAlgebraicExpressionWithoutOptionalErrors("(3^-2)^(3^-2)") + assertThat(expression27).hasStructureThatMatches { + exponentiation { + leftOperand { + group { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + negation { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + rightOperand { + group { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + negation { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + } + } + + val expression28 = parseAlgebraicExpressionWithAllErrors("1-3^sqrt(4)") + assertThat(expression28).hasStructureThatMatches { + subtraction { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + } + } + + // "Hard" order of operation problems loosely based on & other problems that can often stump + // people: https://www.basic-mathematics.com/hard-order-of-operations-problems.html. + val expression29 = parseAlgebraicExpressionWithAllErrors("3÷2*(3+4)") + assertThat(expression29).hasStructureThatMatches { + multiplication { + leftOperand { + division { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + } + + val expression59 = parseAlgebraicExpressionWithAllErrors("3÷2(3+4)") + assertThat(expression59).hasStructureThatMatches { + multiplication { + leftOperand { + division { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + } + + // Numbers cannot have implicit multiplication unless they are in groups. + expectFailureWhenParsingAlgebraicExpression("2 2") + + expectFailureWhenParsingAlgebraicExpression("2 2^2") + + expectFailureWhenParsingAlgebraicExpression("2^2 2") + + val expression31 = parseAlgebraicExpressionWithoutOptionalErrors("(3)(4)(5)") + assertThat(expression31).hasStructureThatMatches { + multiplication { + leftOperand { + multiplication { + leftOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + } + } + } + + val expression33 = parseAlgebraicExpressionWithoutOptionalErrors("2^(3)") + assertThat(expression33).hasStructureThatMatches { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + + // Verify that implicit multiple has lower precedence than exponentiation. + val expression34 = parseAlgebraicExpressionWithoutOptionalErrors("2^(3)(4)") + assertThat(expression34).hasStructureThatMatches { + multiplication { + leftOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + + // An exponentiation can never be an implicit right operand. + expectFailureWhenParsingAlgebraicExpression("2^(3)2^2") + + val expression35 = parseAlgebraicExpressionWithoutOptionalErrors("2^(3)*2^2") + assertThat(expression35).hasStructureThatMatches { + multiplication { + leftOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + rightOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + + // An exponentiation can be a right operand of an implicit mult if it's grouped. + val expression36 = parseAlgebraicExpressionWithoutOptionalErrors("2^(3)(2^2)") + assertThat(expression36).hasStructureThatMatches { + multiplication { + leftOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + rightOperand { + group { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + + // An exponentiation can never be an implicit right operand. + expectFailureWhenParsingAlgebraicExpression("2^3(4)2^3") + + val expression38 = parseAlgebraicExpressionWithoutOptionalErrors("2^3(4)*2^3") + assertThat(expression38).hasStructureThatMatches { + // 2^3(4)*2^3 + multiplication { + leftOperand { + // 2^3(4) + multiplication { + leftOperand { + // 2^3 + exponentiation { + leftOperand { + // 2 + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + // 3 + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + rightOperand { + // 4 + group { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + rightOperand { + // 2^3 + exponentiation { + leftOperand { + // 2 + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + // 3 + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + } + + expectFailureWhenParsingAlgebraicExpression("2^2 2^2") + expectFailureWhenParsingAlgebraicExpression("(3) 2^2") + expectFailureWhenParsingAlgebraicExpression("sqrt(3) 2^2") + expectFailureWhenParsingAlgebraicExpression("√2 2^2") + expectFailureWhenParsingAlgebraicExpression("2^2 3") + + expectFailureWhenParsingAlgebraicExpression("-2 3") + + val expression39 = parseAlgebraicExpressionWithAllErrors("-(1+2)") + assertThat(expression39).hasStructureThatMatches { + negation { + operand { + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + + // Should pass for algebra. + val expression66 = parseAlgebraicExpressionWithAllErrors("-2 x") + assertThat(expression66).hasStructureThatMatches { + negation { + operand { + multiplication { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + variable { + withNameThat().isEqualTo("x") + } + } + } + } + } + } + + val expression40 = parseAlgebraicExpressionWithAllErrors("-2 (1+2)") + assertThat(expression40).hasStructureThatMatches { + // The negation happens last for parity with other common calculators. + negation { + operand { + multiplication { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + } + } + + val expression41 = parseAlgebraicExpressionWithoutOptionalErrors("-2^3(4)") + assertThat(expression41).hasStructureThatMatches { + negation { + operand { + multiplication { + leftOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + } + + val expression43 = parseAlgebraicExpressionWithoutOptionalErrors("√2^2(3)") + assertThat(expression43).hasStructureThatMatches { + multiplication { + leftOperand { + exponentiation { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + + val expression60 = parseAlgebraicExpressionWithoutOptionalErrors("√(2^2(3))") + assertThat(expression60).hasStructureThatMatches { + functionCallTo(SQUARE_ROOT) { + argument { + group { + multiplication { + leftOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + } + } + } + + val expression42 = parseAlgebraicExpressionWithAllErrors("-2*-2") + // Note that the following structure is not the same as (-2)*(-2) since unary negation has + // higher precedence than multiplication, so it's first & recurses to include the entire + // multiplication expression. + assertThat(expression42).hasStructureThatMatches { + negation { + operand { + multiplication { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + negation { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + } + + val expression44 = parseAlgebraicExpressionWithoutOptionalErrors("2(2)") + assertThat(expression44).hasStructureThatMatches { + multiplication { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + + val expression45 = parseAlgebraicExpressionWithAllErrors("2sqrt(2)") + assertThat(expression45).hasStructureThatMatches { + multiplication { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + + val expression46 = parseAlgebraicExpressionWithAllErrors("2√2") + assertThat(expression46).hasStructureThatMatches { + multiplication { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + + val expression47 = parseAlgebraicExpressionWithoutOptionalErrors("(2)(2)") + assertThat(expression47).hasStructureThatMatches { + multiplication { + leftOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + + val expression48 = parseAlgebraicExpressionWithoutOptionalErrors("sqrt(2)(2)") + assertThat(expression48).hasStructureThatMatches { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + + val expression49 = parseAlgebraicExpressionWithAllErrors("sqrt(2)sqrt(2)") + assertThat(expression49).hasStructureThatMatches { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + + val expression50 = parseAlgebraicExpressionWithAllErrors("√2√2") + assertThat(expression50).hasStructureThatMatches { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + + val expression51 = parseAlgebraicExpressionWithoutOptionalErrors("(2)(2)(2)") + assertThat(expression51).hasStructureThatMatches { + multiplication { + leftOperand { + multiplication { + leftOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + + val expression52 = parseAlgebraicExpressionWithAllErrors("sqrt(2)sqrt(2)sqrt(2)") + assertThat(expression52).hasStructureThatMatches { + multiplication { + leftOperand { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + + val expression53 = parseAlgebraicExpressionWithAllErrors("√2√2√2") + assertThat(expression53).hasStructureThatMatches { + multiplication { + leftOperand { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + + // Should fail for algebra. + expectFailureWhenParsingAlgebraicExpression("x7") + + // Should pass for algebra. + val expression67 = parseAlgebraicExpressionWithAllErrors("2x^2y^-3") + assertThat(expression67).hasStructureThatMatches { + // 2x^2y^-3 -> (2*(x^2))*(y^(-3)) + multiplication { + // 2x^2 + leftOperand { + multiplication { + // 2 + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + // x^2 + rightOperand { + exponentiation { + // x + leftOperand { + variable { + withNameThat().isEqualTo("x") + } + } + // 2 + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + // y^-3 + rightOperand { + exponentiation { + // y + leftOperand { + variable { + withNameThat().isEqualTo("y") + } + } + // -3 + rightOperand { + negation { + // 3 + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + } + } + } + + val expression54 = parseAlgebraicExpressionWithAllErrors("2*2/-4+7*2") + assertThat(expression54).hasStructureThatMatches { + // 2*2/-4+7*2 -> ((2*2)/(-4))+(7*2) + addition { + leftOperand { + // 2*2/-4 + division { + leftOperand { + // 2*2 + multiplication { + leftOperand { + // 2 + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + // 2 + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + // -4 + negation { + // 4 + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + rightOperand { + // 7*2 + multiplication { + leftOperand { + // 7 + constant { + withValueThat().isIntegerThat().isEqualTo(7) + } + } + rightOperand { + // 2 + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + + val expression55 = parseAlgebraicExpressionWithAllErrors("3/(1-2)") + assertThat(expression55).hasStructureThatMatches { + division { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + group { + subtraction { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + + val expression56 = parseAlgebraicExpressionWithoutOptionalErrors("(3)/(1-2)") + assertThat(expression56).hasStructureThatMatches { + division { + leftOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + rightOperand { + group { + subtraction { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + + val expression57 = parseAlgebraicExpressionWithoutOptionalErrors("3/((1-2))") + assertThat(expression57).hasStructureThatMatches { + division { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + group { + group { + subtraction { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + } + + // TODO: add others, including tests for malformed expressions throughout the parser & + // tokenizer. + } + + private companion object { + // TODO: fix helper API. + + private fun expectFailureWhenParsingAlgebraicExpression( + expression: String, + allowedVariables: List = listOf("x", "y", "z") + ): MathParsingError { + val result = + parseAlgebraicExpressionInternal(expression, ErrorCheckingMode.ALL_ERRORS, allowedVariables) + assertThat(result).isInstanceOf(MathParsingResult.Failure::class.java) + return (result as MathParsingResult.Failure).error + } + + private fun parseAlgebraicExpressionWithoutOptionalErrors( + expression: String, + allowedVariables: List = listOf("x", "y", "z") + ): MathExpression { + val result = + parseAlgebraicExpressionInternal( + expression, ErrorCheckingMode.REQUIRED_ONLY, allowedVariables + ) + return (result as MathParsingResult.Success).result + } + + private fun parseAlgebraicExpressionWithAllErrors( + expression: String, + allowedVariables: List = listOf("x", "y", "z") + ): MathExpression { + val result = + parseAlgebraicExpressionInternal( + expression, ErrorCheckingMode.ALL_ERRORS, allowedVariables + ) + return (result as MathParsingResult.Success).result + } + + private fun parseAlgebraicExpressionInternal( + expression: String, + errorCheckingMode: ErrorCheckingMode, + allowedVariables: List = listOf("x", "y", "z") + ): MathParsingResult { + return MathExpressionParser.parseAlgebraicExpression( + expression, allowedVariables, errorCheckingMode + ) + } + } +} diff --git a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel index d918580ffb9..2a8b0c295c4 100644 --- a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel @@ -4,6 +4,69 @@ TODO: document load("//:oppia_android_test.bzl", "oppia_android_test") +# "//testing/src/main/java/org/oppia/android/testing/math:comparable_operation_list_subject", +# "//testing/src/main/java/org/oppia/android/testing/math:fraction_subject", +# "//testing/src/main/java/org/oppia/android/testing/math:math_equation_subject", +# "//testing/src/main/java/org/oppia/android/testing/math:math_expression_subject", +# "//testing/src/main/java/org/oppia/android/testing/math:polynomial_subject", +# "//testing/src/main/java/org/oppia/android/testing/math:real_subject", + +oppia_android_test( + name = "AlgebraicEquationParserTest", + srcs = ["AlgebraicEquationParserTest.kt"], + custom_package = "org.oppia.android.util.math", + test_class = "org.oppia.android.util.math.AlgebraicEquationParserTest", + test_manifest = "//utility:test_manifest", + deps = [ + "//model/src/main/proto:math_java_proto_lite", + "//testing/src/main/java/org/oppia/android/testing/math:math_equation_subject", + "//third_party:androidx_test_ext_junit", + "//third_party:com_google_truth_extensions_truth-liteproto-extension", + "//third_party:com_google_truth_truth", + "//third_party:junit_junit", + "//third_party:org_robolectric_robolectric", + "//third_party:robolectric_android-all", + "//utility/src/main/java/org/oppia/android/util/math:parser", + ], +) + +oppia_android_test( + name = "AlgebraicExpressionParserTest", + srcs = ["AlgebraicExpressionParserTest.kt"], + custom_package = "org.oppia.android.util.math", + test_class = "org.oppia.android.util.math.AlgebraicExpressionParserTest", + test_manifest = "//utility:test_manifest", + deps = [ + "//model/src/main/proto:math_java_proto_lite", + "//testing/src/main/java/org/oppia/android/testing/math:math_expression_subject", + "//third_party:androidx_test_ext_junit", + "//third_party:com_google_truth_extensions_truth-liteproto-extension", + "//third_party:com_google_truth_truth", + "//third_party:junit_junit", + "//third_party:org_robolectric_robolectric", + "//third_party:robolectric_android-all", + "//utility/src/main/java/org/oppia/android/util/math:parser", + ], +) + +oppia_android_test( + name = "MathExpressionParserTest", + srcs = ["MathExpressionParserTest.kt"], + custom_package = "org.oppia.android.util.math", + test_class = "org.oppia.android.util.math.MathExpressionParserTest", + test_manifest = "//utility:test_manifest", + deps = [ + "//model/src/main/proto:math_java_proto_lite", + "//third_party:androidx_test_ext_junit", + "//third_party:com_google_truth_extensions_truth-liteproto-extension", + "//third_party:com_google_truth_truth", + "//third_party:junit_junit", + "//third_party:org_robolectric_robolectric", + "//third_party:robolectric_android-all", + "//utility/src/main/java/org/oppia/android/util/math:parser", + ], +) + oppia_android_test( name = "MathTokenizerTest", srcs = ["MathTokenizerTest.kt"], @@ -22,6 +85,25 @@ oppia_android_test( ], ) +oppia_android_test( + name = "NumericExpressionParserTest", + srcs = ["NumericExpressionParserTest.kt"], + custom_package = "org.oppia.android.util.math", + test_class = "org.oppia.android.util.math.NumericExpressionParserTest", + test_manifest = "//utility:test_manifest", + deps = [ + "//model/src/main/proto:math_java_proto_lite", + "//testing/src/main/java/org/oppia/android/testing/math:math_expression_subject", + "//third_party:androidx_test_ext_junit", + "//third_party:com_google_truth_extensions_truth-liteproto-extension", + "//third_party:com_google_truth_truth", + "//third_party:junit_junit", + "//third_party:org_robolectric_robolectric", + "//third_party:robolectric_android-all", + "//utility/src/main/java/org/oppia/android/util/math:parser", + ], +) + oppia_android_test( name = "RatioExtensionsTest", srcs = ["RatioExtensionsTest.kt"], diff --git a/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt new file mode 100644 index 00000000000..3c50966b01e --- /dev/null +++ b/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt @@ -0,0 +1,344 @@ +package org.oppia.android.util.math + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.oppia.android.app.model.MathBinaryOperation +import org.oppia.android.app.model.MathEquation +import org.oppia.android.app.model.MathExpression +import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode +import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult +import org.oppia.android.util.math.MathParsingError.DisabledVariablesInUseError +import org.oppia.android.util.math.MathParsingError.EquationHasWrongNumberOfEqualsError +import org.oppia.android.util.math.MathParsingError.EquationMissingLhsOrRhsError +import org.oppia.android.util.math.MathParsingError.ExponentIsVariableExpressionError +import org.oppia.android.util.math.MathParsingError.ExponentTooLargeError +import org.oppia.android.util.math.MathParsingError.FunctionNameIncompleteError +import org.oppia.android.util.math.MathParsingError.HangingSquareRootError +import org.oppia.android.util.math.MathParsingError.InvalidFunctionInUseError +import org.oppia.android.util.math.MathParsingError.MultipleRedundantParenthesesError +import org.oppia.android.util.math.MathParsingError.NestedExponentsError +import org.oppia.android.util.math.MathParsingError.NoVariableOrNumberAfterBinaryOperatorError +import org.oppia.android.util.math.MathParsingError.NoVariableOrNumberBeforeBinaryOperatorError +import org.oppia.android.util.math.MathParsingError.NumberAfterVariableError +import org.oppia.android.util.math.MathParsingError.RedundantParenthesesForIndividualTermsError +import org.oppia.android.util.math.MathParsingError.SingleRedundantParenthesesError +import org.oppia.android.util.math.MathParsingError.SpacesBetweenNumbersError +import org.oppia.android.util.math.MathParsingError.SubsequentBinaryOperatorsError +import org.oppia.android.util.math.MathParsingError.SubsequentUnaryOperatorsError +import org.oppia.android.util.math.MathParsingError.TermDividedByZeroError +import org.oppia.android.util.math.MathParsingError.UnbalancedParenthesesError +import org.oppia.android.util.math.MathParsingError.UnnecessarySymbolsError +import org.oppia.android.util.math.MathParsingError.VariableInNumericExpressionError +import org.robolectric.annotation.LooperMode + +/** Tests for [MathExpressionParser]. */ +@RunWith(AndroidJUnit4::class) +@LooperMode(LooperMode.Mode.PAUSED) +class MathExpressionParserTest { + // TODO: add high-level checks for the three types, but don't test in detail since there are + // separate suites. Also, document the separate suites' existence in this suites's KDoc. + + @Test + fun testErrorCases() { + // TODO: split up. + val failure1 = expectFailureWhenParsingNumericExpression("73 2") + assertThat(failure1).isEqualTo(SpacesBetweenNumbersError) + + val failure2 = expectFailureWhenParsingNumericExpression("(73") + assertThat(failure2).isEqualTo(UnbalancedParenthesesError) + + val failure3 = expectFailureWhenParsingNumericExpression("73)") + assertThat(failure3).isEqualTo(UnbalancedParenthesesError) + + val failure4 = expectFailureWhenParsingNumericExpression("((73)") + assertThat(failure4).isEqualTo(UnbalancedParenthesesError) + + val failure5 = expectFailureWhenParsingNumericExpression("73 (") + assertThat(failure5).isEqualTo(UnbalancedParenthesesError) + + val failure6 = expectFailureWhenParsingNumericExpression("73 )") + assertThat(failure6).isEqualTo(UnbalancedParenthesesError) + + val failure7 = expectFailureWhenParsingNumericExpression("sqrt(73") + assertThat(failure7).isEqualTo(UnbalancedParenthesesError) + + // TODO: test properties on errors (& add better testing library for errors, or at least helpers). + val failure8 = expectFailureWhenParsingNumericExpression("(7 * 2 + 4)") + assertThat(failure8).isInstanceOf(SingleRedundantParenthesesError::class.java) + + val failure9 = expectFailureWhenParsingNumericExpression("((5 + 4))") + assertThat(failure9).isInstanceOf(MultipleRedundantParenthesesError::class.java) + + val failure13 = expectFailureWhenParsingNumericExpression("(((5 + 4)))") + assertThat(failure13).isInstanceOf(MultipleRedundantParenthesesError::class.java) + + val failure14 = expectFailureWhenParsingNumericExpression("1+((5 + 4))") + assertThat(failure14).isInstanceOf(MultipleRedundantParenthesesError::class.java) + + val failure15 = expectFailureWhenParsingNumericExpression("1+(7*((( 9 + 3) )))") + assertThat(failure15).isInstanceOf(MultipleRedundantParenthesesError::class.java) + assertThat((failure15 as MultipleRedundantParenthesesError).rawExpression) + .isEqualTo("(( 9 + 3) )") + + parseNumericExpressionSuccessfully("1+(5+4)") + parseNumericExpressionSuccessfully("(5+4)+1") + + val failure10 = expectFailureWhenParsingNumericExpression("(5) + 4") + assertThat(failure10).isInstanceOf(RedundantParenthesesForIndividualTermsError::class.java) + + val failure11 = expectFailureWhenParsingNumericExpression("5^(2)") + assertThat(failure11).isInstanceOf(RedundantParenthesesForIndividualTermsError::class.java) + assertThat((failure11 as RedundantParenthesesForIndividualTermsError).rawExpression) + .isEqualTo("2") + + val failure12 = expectFailureWhenParsingNumericExpression("sqrt((2))") + assertThat(failure12).isInstanceOf(RedundantParenthesesForIndividualTermsError::class.java) + + val failure16 = expectFailureWhenParsingNumericExpression("$2") + assertThat(failure16).isInstanceOf(UnnecessarySymbolsError::class.java) + assertThat((failure16 as UnnecessarySymbolsError).invalidSymbol).isEqualTo("$") + + val failure17 = expectFailureWhenParsingNumericExpression("5%") + assertThat(failure17).isInstanceOf(UnnecessarySymbolsError::class.java) + assertThat((failure17 as UnnecessarySymbolsError).invalidSymbol).isEqualTo("%") + + val failure18 = expectFailureWhenParsingAlgebraicExpression("x5") + assertThat(failure18).isInstanceOf(NumberAfterVariableError::class.java) + assertThat((failure18 as NumberAfterVariableError).number.integer).isEqualTo(5) + assertThat(failure18.variable).isEqualTo("x") + + val failure19 = expectFailureWhenParsingAlgebraicExpression("2+y 3.14*7") + assertThat(failure19).isInstanceOf(NumberAfterVariableError::class.java) + assertThat((failure19 as NumberAfterVariableError).number.irrational).isWithin(1e-5).of(3.14) + assertThat(failure19.variable).isEqualTo("y") + + // TODO: expand to multiple tests or use parametrized tests. + // RHS operators don't result in unary operations (which are valid in the grammar). + val rhsOperators = listOf("*", "×", "/", "÷", "^") + val lhsOperators = rhsOperators + listOf("+", "-", "−") + val operatorCombinations = lhsOperators.flatMap { op1 -> rhsOperators.map { op1 to it } } + for ((op1, op2) in operatorCombinations) { + val failure22 = expectFailureWhenParsingNumericExpression(expression = "1 $op1$op2 2") + assertThat(failure22).isInstanceOf(SubsequentBinaryOperatorsError::class.java) + assertThat((failure22 as SubsequentBinaryOperatorsError).operator1).isEqualTo(op1) + assertThat(failure22.operator2).isEqualTo(op2) + } + + val failure37 = expectFailureWhenParsingNumericExpression("++2") + assertThat(failure37).isInstanceOf(SubsequentUnaryOperatorsError::class.java) + + val failure38 = expectFailureWhenParsingAlgebraicExpression("--x") + assertThat(failure38).isInstanceOf(SubsequentUnaryOperatorsError::class.java) + + val failure39 = expectFailureWhenParsingAlgebraicExpression("-+x") + assertThat(failure39).isInstanceOf(SubsequentUnaryOperatorsError::class.java) + + val failure40 = expectFailureWhenParsingNumericExpression("+-2") + assertThat(failure40).isInstanceOf(SubsequentUnaryOperatorsError::class.java) + + parseNumericExpressionSuccessfully("2++3") // Will succeed since it's 2 + (+2). + val failure41 = expectFailureWhenParsingNumericExpression("2+++3") + assertThat(failure41).isInstanceOf(SubsequentUnaryOperatorsError::class.java) + + val failure23 = expectFailureWhenParsingNumericExpression("/2") + assertThat(failure23).isInstanceOf(NoVariableOrNumberBeforeBinaryOperatorError::class.java) + assertThat((failure23 as NoVariableOrNumberBeforeBinaryOperatorError).operator) + .isEqualTo(MathBinaryOperation.Operator.DIVIDE) + + val failure24 = expectFailureWhenParsingAlgebraicExpression("*x") + assertThat(failure24).isInstanceOf(NoVariableOrNumberBeforeBinaryOperatorError::class.java) + assertThat((failure24 as NoVariableOrNumberBeforeBinaryOperatorError).operator) + .isEqualTo(MathBinaryOperation.Operator.MULTIPLY) + + val failure27 = expectFailureWhenParsingNumericExpression("2^") + assertThat(failure27).isInstanceOf(NoVariableOrNumberAfterBinaryOperatorError::class.java) + assertThat((failure27 as NoVariableOrNumberAfterBinaryOperatorError).operator) + .isEqualTo(MathBinaryOperation.Operator.EXPONENTIATE) + + val failure25 = expectFailureWhenParsingNumericExpression("2/") + assertThat(failure25).isInstanceOf(NoVariableOrNumberAfterBinaryOperatorError::class.java) + assertThat((failure25 as NoVariableOrNumberAfterBinaryOperatorError).operator) + .isEqualTo(MathBinaryOperation.Operator.DIVIDE) + + val failure26 = expectFailureWhenParsingAlgebraicExpression("x*") + assertThat(failure26).isInstanceOf(NoVariableOrNumberAfterBinaryOperatorError::class.java) + assertThat((failure26 as NoVariableOrNumberAfterBinaryOperatorError).operator) + .isEqualTo(MathBinaryOperation.Operator.MULTIPLY) + + val failure28 = expectFailureWhenParsingAlgebraicExpression("x+") + assertThat(failure28).isInstanceOf(NoVariableOrNumberAfterBinaryOperatorError::class.java) + assertThat((failure28 as NoVariableOrNumberAfterBinaryOperatorError).operator) + .isEqualTo(MathBinaryOperation.Operator.ADD) + + val failure29 = expectFailureWhenParsingAlgebraicExpression("x-") + assertThat(failure29).isInstanceOf(NoVariableOrNumberAfterBinaryOperatorError::class.java) + assertThat((failure29 as NoVariableOrNumberAfterBinaryOperatorError).operator) + .isEqualTo(MathBinaryOperation.Operator.SUBTRACT) + + val failure42 = expectFailureWhenParsingAlgebraicExpression("2^x") + assertThat(failure42).isInstanceOf(ExponentIsVariableExpressionError::class.java) + + val failure43 = expectFailureWhenParsingAlgebraicExpression("2^(1+x)") + assertThat(failure43).isInstanceOf(ExponentIsVariableExpressionError::class.java) + + val failure44 = expectFailureWhenParsingAlgebraicExpression("2^3^x") + assertThat(failure44).isInstanceOf(ExponentIsVariableExpressionError::class.java) + + val failure45 = expectFailureWhenParsingAlgebraicExpression("2^sqrt(x)") + assertThat(failure45).isInstanceOf(ExponentIsVariableExpressionError::class.java) + + val failure46 = expectFailureWhenParsingNumericExpression("2^7") + assertThat(failure46).isInstanceOf(ExponentTooLargeError::class.java) + + val failure47 = expectFailureWhenParsingNumericExpression("2^30.12") + assertThat(failure47).isInstanceOf(ExponentTooLargeError::class.java) + + parseNumericExpressionSuccessfully("2^3") + + val failure48 = expectFailureWhenParsingNumericExpression("2^3^2") + assertThat(failure48).isInstanceOf(NestedExponentsError::class.java) + + val failure49 = expectFailureWhenParsingAlgebraicExpression("x^2^5") + assertThat(failure49).isInstanceOf(NestedExponentsError::class.java) + + val failure20 = expectFailureWhenParsingNumericExpression("2√") + assertThat(failure20).isInstanceOf(HangingSquareRootError::class.java) + + val failure50 = expectFailureWhenParsingNumericExpression("2/0") + assertThat(failure50).isInstanceOf(TermDividedByZeroError::class.java) + + val failure51 = expectFailureWhenParsingAlgebraicExpression("x/0") + assertThat(failure51).isInstanceOf(TermDividedByZeroError::class.java) + + val failure52 = expectFailureWhenParsingNumericExpression("sqrt(2+7/0.0)") + assertThat(failure52).isInstanceOf(TermDividedByZeroError::class.java) + + val failure21 = expectFailureWhenParsingNumericExpression("x+y") + assertThat(failure21).isInstanceOf(VariableInNumericExpressionError::class.java) + + val failure53 = expectFailureWhenParsingAlgebraicExpression("x+y+a") + assertThat(failure53).isInstanceOf(DisabledVariablesInUseError::class.java) + assertThat((failure53 as DisabledVariablesInUseError).variables).containsExactly("a") + + val failure54 = expectFailureWhenParsingAlgebraicExpression("apple") + assertThat(failure54).isInstanceOf(DisabledVariablesInUseError::class.java) + assertThat((failure54 as DisabledVariablesInUseError).variables) + .containsExactly("a", "p", "l", "e") + + val failure55 = + expectFailureWhenParsingAlgebraicExpression("apple", allowedVariables = listOf("a", "p", "l")) + assertThat(failure55).isInstanceOf(DisabledVariablesInUseError::class.java) + assertThat((failure55 as DisabledVariablesInUseError).variables).containsExactly("e") + + parseAlgebraicExpressionSuccessfully("x+y+z") + + val failure56 = + expectFailureWhenParsingAlgebraicExpression("x+y+z", allowedVariables = listOf()) + assertThat(failure56).isInstanceOf(DisabledVariablesInUseError::class.java) + assertThat((failure56 as DisabledVariablesInUseError).variables).containsExactly("x", "y", "z") + + val failure30 = expectFailureWhenParsingAlgebraicEquation("x==2") + assertThat(failure30).isInstanceOf(EquationHasWrongNumberOfEqualsError::class.java) + + val failure31 = expectFailureWhenParsingAlgebraicEquation("x=2=y") + assertThat(failure31).isInstanceOf(EquationHasWrongNumberOfEqualsError::class.java) + + val failure32 = expectFailureWhenParsingAlgebraicEquation("x=2=") + assertThat(failure32).isInstanceOf(EquationHasWrongNumberOfEqualsError::class.java) + + val failure33 = expectFailureWhenParsingAlgebraicEquation("x=") + assertThat(failure33).isInstanceOf(EquationMissingLhsOrRhsError::class.java) + + val failure34 = expectFailureWhenParsingAlgebraicEquation("=x") + assertThat(failure34).isInstanceOf(EquationMissingLhsOrRhsError::class.java) + + val failure35 = expectFailureWhenParsingAlgebraicEquation("=x") + assertThat(failure35).isInstanceOf(EquationMissingLhsOrRhsError::class.java) + + // TODO: expand to multiple tests or use parametrized tests. + val prohibitedFunctionNames = + listOf( + "exp", "log", "log10", "ln", "sin", "cos", "tan", "cot", "csc", "sec", "atan", "asin", + "acos", "abs" + ) + for (functionName in prohibitedFunctionNames) { + val failure36 = expectFailureWhenParsingAlgebraicEquation("$functionName(0.5)") + assertThat(failure36).isInstanceOf(InvalidFunctionInUseError::class.java) + assertThat((failure36 as InvalidFunctionInUseError).functionName).isEqualTo(functionName) + } + + val failure57 = expectFailureWhenParsingAlgebraicExpression("sq") + assertThat(failure57).isInstanceOf(FunctionNameIncompleteError::class.java) + + val failure58 = expectFailureWhenParsingAlgebraicExpression("sqr") + assertThat(failure58).isInstanceOf(FunctionNameIncompleteError::class.java) + + // TODO: Other cases: sqrt(, sqrt(), sqrt 2, +2 + } + + private companion object { + // TODO: fix helper API. + + private fun expectFailureWhenParsingNumericExpression(expression: String): MathParsingError { + val result = parseNumericExpressionWithAllErrors(expression) + assertThat(result).isInstanceOf(MathParsingResult.Failure::class.java) + return (result as MathParsingResult.Failure).error + } + + private fun parseNumericExpressionSuccessfully(expression: String): MathExpression { + val result = parseNumericExpressionWithAllErrors(expression) + return (result as MathParsingResult.Success).result + } + + private fun parseNumericExpressionWithAllErrors( + expression: String + ): MathParsingResult { + return MathExpressionParser.parseNumericExpression(expression, ErrorCheckingMode.ALL_ERRORS) + } + + private fun expectFailureWhenParsingAlgebraicExpression( + expression: String, + allowedVariables: List = listOf("x", "y", "z") + ): MathParsingError { + val result = + parseAlgebraicExpressionWithAllErrors(expression, allowedVariables) + assertThat(result).isInstanceOf(MathParsingResult.Failure::class.java) + return (result as MathParsingResult.Failure).error + } + + private fun parseAlgebraicExpressionSuccessfully( + expression: String, + allowedVariables: List = listOf("x", "y", "z") + ): MathExpression { + val result = parseAlgebraicExpressionWithAllErrors(expression, allowedVariables) + return (result as MathParsingResult.Success).result + } + + private fun parseAlgebraicExpressionWithAllErrors( + expression: String, + allowedVariables: List = listOf("x", "y", "z") + ): MathParsingResult { + return MathExpressionParser.parseAlgebraicExpression( + expression, allowedVariables, ErrorCheckingMode.ALL_ERRORS + ) + } + + private fun expectFailureWhenParsingAlgebraicEquation(expression: String): MathParsingError { + val result = parseAlgebraicEquationInternal(expression, ErrorCheckingMode.ALL_ERRORS) + assertThat(result).isInstanceOf(MathParsingResult.Failure::class.java) + return (result as MathParsingResult.Failure).error + } + + private fun parseAlgebraicEquationInternal( + expression: String, + errorCheckingMode: ErrorCheckingMode, + allowedVariables: List = listOf("x", "y", "z") + ): MathParsingResult { + return MathExpressionParser.parseAlgebraicEquation( + expression, allowedVariables, errorCheckingMode + ) + } + } +} diff --git a/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt new file mode 100644 index 00000000000..d1f9e17b47d --- /dev/null +++ b/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt @@ -0,0 +1,1777 @@ +package org.oppia.android.util.math + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.oppia.android.app.model.MathExpression +import org.oppia.android.app.model.MathFunctionCall.FunctionType.SQUARE_ROOT +import org.oppia.android.testing.math.MathExpressionSubject.Companion.assertThat +import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode +import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult +import org.robolectric.annotation.LooperMode + +/** Tests for [MathExpressionParser]. */ +@RunWith(AndroidJUnit4::class) +@LooperMode(LooperMode.Mode.PAUSED) +class NumericExpressionParserTest { + @Test + fun testLotsOfCasesForNumericExpression() { + // TODO: split this up + // TODO: add log string generation for expressions. + expectFailureWhenParsingNumericExpression("") + + val expression1 = parseNumericExpressionWithAllErrors("1") + assertThat(expression1).hasStructureThatMatches { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + + expectFailureWhenParsingNumericExpression("x") + + val expression2 = parseNumericExpressionWithAllErrors(" 2 ") + assertThat(expression2).hasStructureThatMatches { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + + val expression3 = parseNumericExpressionWithAllErrors(" 2.5 ") + assertThat(expression3).hasStructureThatMatches { + constant { + withValueThat().isIrrationalThat().isWithin(1e-5).of(2.5) + } + } + + expectFailureWhenParsingNumericExpression(" x ") + + expectFailureWhenParsingNumericExpression(" z x ") + + val expression4 = parseNumericExpressionWithoutOptionalErrors("2^3^2") + assertThat(expression4).hasStructureThatMatches { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + + val expression23 = parseNumericExpressionWithAllErrors("(2^3)^2") + assertThat(expression23).hasStructureThatMatches { + exponentiation { + leftOperand { + group { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + + val expression24 = parseNumericExpressionWithAllErrors("512/32/4") + assertThat(expression24).hasStructureThatMatches { + division { + leftOperand { + division { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(512) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(32) + } + } + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + + val expression25 = parseNumericExpressionWithAllErrors("512/(32/4)") + assertThat(expression25).hasStructureThatMatches { + division { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(512) + } + } + rightOperand { + group { + division { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(32) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + } + + val expression5 = parseNumericExpressionWithAllErrors("sqrt(2)") + assertThat(expression5).hasStructureThatMatches { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + + expectFailureWhenParsingNumericExpression("sqr(2)") + + expectFailureWhenParsingNumericExpression("xyz(2)") + + val expression6 = parseNumericExpressionWithAllErrors("732") + assertThat(expression6).hasStructureThatMatches { + constant { + withValueThat().isIntegerThat().isEqualTo(732) + } + } + + // Verify order of operations between higher & lower precedent operators. + val expression32 = parseNumericExpressionWithAllErrors("3+4^5") + assertThat(expression32).hasStructureThatMatches { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + } + } + } + } + + val expression7 = parseNumericExpressionWithoutOptionalErrors("3*2-3+4^7*8/3*2+7") + assertThat(expression7).hasStructureThatMatches { + // To better visualize the precedence & order of operations, see this grouped version: + // (((3*2)-3)+((((4^7)*8)/3)*2))+7. + addition { + leftOperand { + // ((3*2)-3)+((((4^7)*8)/3)*2) + addition { + leftOperand { + // (1*2)-3 + subtraction { + leftOperand { + // 3*2 + multiplication { + leftOperand { + // 3 + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + // 2 + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + // 3 + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + rightOperand { + // (((4^7)*8)/3)*2 + multiplication { + leftOperand { + // ((4^7)*8)/3 + division { + leftOperand { + // (4^7)*8 + multiplication { + leftOperand { + // 4^7 + exponentiation { + leftOperand { + // 4 + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + rightOperand { + // 7 + constant { + withValueThat().isIntegerThat().isEqualTo(7) + } + } + } + } + rightOperand { + // 8 + constant { + withValueThat().isIntegerThat().isEqualTo(8) + } + } + } + } + rightOperand { + // 3 + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + rightOperand { + // 2 + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + rightOperand { + // 7 + constant { + withValueThat().isIntegerThat().isEqualTo(7) + } + } + } + } + + expectFailureWhenParsingNumericExpression("x = √2 × 7 ÷ 4") + + val expression8 = parseNumericExpressionWithAllErrors("(1+2)(3+4)") + assertThat(expression8).hasStructureThatMatches { + multiplication { + leftOperand { + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + rightOperand { + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + } + + // Right implicit multiplication of numbers isn't allowed. + expectFailureWhenParsingNumericExpression("(1+2)2") + + val expression10 = parseNumericExpressionWithAllErrors("2(1+2)") + assertThat(expression10).hasStructureThatMatches { + multiplication { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + + // Right implicit multiplication of numbers isn't allowed. + expectFailureWhenParsingNumericExpression("sqrt(2)3") + + val expression12 = parseNumericExpressionWithAllErrors("3sqrt(2)") + assertThat(expression12).hasStructureThatMatches { + multiplication { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + + expectFailureWhenParsingNumericExpression("xsqrt(2)") + + val expression13 = parseNumericExpressionWithAllErrors("sqrt(2)*(1+2)*(3-2^5)") + assertThat(expression13).hasStructureThatMatches { + multiplication { + leftOperand { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + rightOperand { + group { + subtraction { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + } + } + } + } + } + } + } + + val expression58 = parseNumericExpressionWithAllErrors("sqrt(2)(1+2)(3-2^5)") + assertThat(expression58).hasStructureThatMatches { + multiplication { + leftOperand { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + rightOperand { + group { + subtraction { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + } + } + } + } + } + } + } + + val expression14 = parseNumericExpressionWithoutOptionalErrors("((3))") + assertThat(expression14).hasStructureThatMatches { + group { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + + val expression15 = parseNumericExpressionWithoutOptionalErrors("++3") + assertThat(expression15).hasStructureThatMatches { + positive { + operand { + positive { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + } + + val expression16 = parseNumericExpressionWithoutOptionalErrors("--4") + assertThat(expression16).hasStructureThatMatches { + negation { + operand { + negation { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + + val expression17 = parseNumericExpressionWithAllErrors("1+-4") + assertThat(expression17).hasStructureThatMatches { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + negation { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + + val expression18 = parseNumericExpressionWithAllErrors("1++4") + assertThat(expression18).hasStructureThatMatches { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + positive { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + + val expression19 = parseNumericExpressionWithAllErrors("1--4") + assertThat(expression19).hasStructureThatMatches { + subtraction { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + negation { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + + expectFailureWhenParsingNumericExpression("1-^-4") + + val expression20 = parseNumericExpressionWithAllErrors("√2 × 7 ÷ 4") + assertThat(expression20).hasStructureThatMatches { + division { + leftOperand { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(7) + } + } + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + + expectFailureWhenParsingNumericExpression("1+2 &asdf") + + val expression21 = parseNumericExpressionWithAllErrors("sqrt(2)sqrt(3)sqrt(4)") + // Note that this tree demonstrates left associativity. + assertThat(expression21).hasStructureThatMatches { + multiplication { + leftOperand { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + + val expression22 = parseNumericExpressionWithAllErrors("(1+2)(3-7^2)(5+-17)") + assertThat(expression22).hasStructureThatMatches { + multiplication { + leftOperand { + multiplication { + leftOperand { + // 1+2 + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + rightOperand { + // 3-7^2 + group { + subtraction { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(7) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + } + } + rightOperand { + // 5+-17 + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + rightOperand { + negation { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(17) + } + } + } + } + } + } + } + } + } + + val expression26 = parseNumericExpressionWithAllErrors("3^-2") + assertThat(expression26).hasStructureThatMatches { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + negation { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + + val expression27 = parseNumericExpressionWithoutOptionalErrors("(3^-2)^(3^-2)") + assertThat(expression27).hasStructureThatMatches { + exponentiation { + leftOperand { + group { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + negation { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + rightOperand { + group { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + negation { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + } + } + + val expression28 = parseNumericExpressionWithAllErrors("1-3^sqrt(4)") + assertThat(expression28).hasStructureThatMatches { + subtraction { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + } + } + + // "Hard" order of operation problems loosely based on & other problems that can often stump + // people: https://www.basic-mathematics.com/hard-order-of-operations-problems.html. + val expression29 = parseNumericExpressionWithAllErrors("3÷2*(3+4)") + assertThat(expression29).hasStructureThatMatches { + multiplication { + leftOperand { + division { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + } + + val expression59 = parseNumericExpressionWithAllErrors("3÷2(3+4)") + assertThat(expression59).hasStructureThatMatches { + multiplication { + leftOperand { + division { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + } + + // Numbers cannot have implicit multiplication unless they are in groups. + expectFailureWhenParsingNumericExpression("2 2") + + expectFailureWhenParsingNumericExpression("2 2^2") + + expectFailureWhenParsingNumericExpression("2^2 2") + + val expression31 = parseNumericExpressionWithoutOptionalErrors("(3)(4)(5)") + assertThat(expression31).hasStructureThatMatches { + multiplication { + leftOperand { + multiplication { + leftOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + } + } + } + + val expression33 = parseNumericExpressionWithoutOptionalErrors("2^(3)") + assertThat(expression33).hasStructureThatMatches { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + + // Verify that implicit multiple has lower precedence than exponentiation. + val expression34 = parseNumericExpressionWithoutOptionalErrors("2^(3)(4)") + assertThat(expression34).hasStructureThatMatches { + multiplication { + leftOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + + // An exponentiation can never be an implicit right operand. + expectFailureWhenParsingNumericExpression("2^(3)2^2") + + val expression35 = parseNumericExpressionWithoutOptionalErrors("2^(3)*2^2") + assertThat(expression35).hasStructureThatMatches { + multiplication { + leftOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + rightOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + + // An exponentiation can be a right operand of an implicit mult if it's grouped. + val expression36 = parseNumericExpressionWithoutOptionalErrors("2^(3)(2^2)") + assertThat(expression36).hasStructureThatMatches { + multiplication { + leftOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + rightOperand { + group { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + + // An exponentiation can never be an implicit right operand. + expectFailureWhenParsingNumericExpression("2^3(4)2^3") + + val expression38 = parseNumericExpressionWithoutOptionalErrors("2^3(4)*2^3") + assertThat(expression38).hasStructureThatMatches { + // 2^3(4)*2^3 + multiplication { + leftOperand { + // 2^3(4) + multiplication { + leftOperand { + // 2^3 + exponentiation { + leftOperand { + // 2 + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + // 3 + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + rightOperand { + // 4 + group { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + rightOperand { + // 2^3 + exponentiation { + leftOperand { + // 2 + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + // 3 + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + } + + expectFailureWhenParsingNumericExpression("2^2 2^2") + expectFailureWhenParsingNumericExpression("(3) 2^2") + expectFailureWhenParsingNumericExpression("sqrt(3) 2^2") + expectFailureWhenParsingNumericExpression("√2 2^2") + expectFailureWhenParsingNumericExpression("2^2 3") + + expectFailureWhenParsingNumericExpression("-2 3") + + val expression39 = parseNumericExpressionWithAllErrors("-(1+2)") + assertThat(expression39).hasStructureThatMatches { + negation { + operand { + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + + // Should pass for algebra. + expectFailureWhenParsingNumericExpression("-2 x") + + val expression40 = parseNumericExpressionWithAllErrors("-2 (1+2)") + assertThat(expression40).hasStructureThatMatches { + // The negation happens last for parity with other common calculators. + negation { + operand { + multiplication { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + } + } + + val expression41 = parseNumericExpressionWithoutOptionalErrors("-2^3(4)") + assertThat(expression41).hasStructureThatMatches { + negation { + operand { + multiplication { + leftOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + } + + val expression43 = parseNumericExpressionWithoutOptionalErrors("√2^2(3)") + assertThat(expression43).hasStructureThatMatches { + multiplication { + leftOperand { + exponentiation { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + + val expression60 = parseNumericExpressionWithoutOptionalErrors("√(2^2(3))") + assertThat(expression60).hasStructureThatMatches { + functionCallTo(SQUARE_ROOT) { + argument { + group { + multiplication { + leftOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + } + } + } + + val expression42 = parseNumericExpressionWithAllErrors("-2*-2") + // Note that the following structure is not the same as (-2)*(-2) since unary negation has + // higher precedence than multiplication, so it's first & recurses to include the entire + // multiplication expression. + assertThat(expression42).hasStructureThatMatches { + negation { + operand { + multiplication { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + negation { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + } + + // TODO: Here & elsewhere, fix the fact that this is actually a valid use of single-term + // parentheses (there's a bug in the current error detection logic). + val expression44 = parseNumericExpressionWithoutOptionalErrors("2(2)") + assertThat(expression44).hasStructureThatMatches { + multiplication { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + + val expression45 = parseNumericExpressionWithAllErrors("2sqrt(2)") + assertThat(expression45).hasStructureThatMatches { + multiplication { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + + val expression46 = parseNumericExpressionWithAllErrors("2√2") + assertThat(expression46).hasStructureThatMatches { + multiplication { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + + val expression47 = parseNumericExpressionWithoutOptionalErrors("(2)(2)") + assertThat(expression47).hasStructureThatMatches { + multiplication { + leftOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + + val expression48 = parseNumericExpressionWithoutOptionalErrors("sqrt(2)(2)") + assertThat(expression48).hasStructureThatMatches { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + + val expression49 = parseNumericExpressionWithAllErrors("sqrt(2)sqrt(2)") + assertThat(expression49).hasStructureThatMatches { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + + val expression50 = parseNumericExpressionWithAllErrors("√2√2") + assertThat(expression50).hasStructureThatMatches { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + + val expression51 = parseNumericExpressionWithoutOptionalErrors("(2)(2)(2)") + assertThat(expression51).hasStructureThatMatches { + multiplication { + leftOperand { + multiplication { + leftOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + + val expression52 = parseNumericExpressionWithAllErrors("sqrt(2)sqrt(2)sqrt(2)") + assertThat(expression52).hasStructureThatMatches { + multiplication { + leftOperand { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + + val expression53 = parseNumericExpressionWithAllErrors("√2√2√2") + assertThat(expression53).hasStructureThatMatches { + multiplication { + leftOperand { + multiplication { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + + // Should fail for algebra. + expectFailureWhenParsingNumericExpression("x7") + + // Should pass for algebra. + expectFailureWhenParsingNumericExpression("2x^2") + + val expression54 = parseNumericExpressionWithAllErrors("2*2/-4+7*2") + assertThat(expression54).hasStructureThatMatches { + // 2*2/-4+7*2 -> ((2*2)/(-4))+(7*2) + addition { + leftOperand { + // 2*2/-4 + division { + leftOperand { + // 2*2 + multiplication { + leftOperand { + // 2 + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + // 2 + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + // -4 + negation { + // 4 + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + rightOperand { + // 7*2 + multiplication { + leftOperand { + // 7 + constant { + withValueThat().isIntegerThat().isEqualTo(7) + } + } + rightOperand { + // 2 + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + + val expression55 = parseNumericExpressionWithAllErrors("3/(1-2)") + assertThat(expression55).hasStructureThatMatches { + division { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + group { + subtraction { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + + val expression56 = parseNumericExpressionWithoutOptionalErrors("(3)/(1-2)") + assertThat(expression56).hasStructureThatMatches { + division { + leftOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + rightOperand { + group { + subtraction { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + + val expression57 = parseNumericExpressionWithoutOptionalErrors("3/((1-2))") + assertThat(expression57).hasStructureThatMatches { + division { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + group { + group { + subtraction { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + } + + // TODO: add others, including tests for malformed expressions throughout the parser & + // tokenizer. + } + + private companion object { + // TODO: fix helper API. + + private fun expectFailureWhenParsingNumericExpression(expression: String): MathParsingError { + val result = parseNumericExpressionInternal(expression, ErrorCheckingMode.ALL_ERRORS) + assertThat(result).isInstanceOf(MathParsingResult.Failure::class.java) + return (result as MathParsingResult.Failure).error + } + + private fun parseNumericExpressionWithoutOptionalErrors(expression: String): MathExpression { + val result = + parseNumericExpressionInternal( + expression, ErrorCheckingMode.REQUIRED_ONLY + ) + return (result as MathParsingResult.Success).result + } + + private fun parseNumericExpressionWithAllErrors(expression: String): MathExpression { + val result = + parseNumericExpressionInternal( + expression, ErrorCheckingMode.ALL_ERRORS + ) + return (result as MathParsingResult.Success).result + } + + private fun parseNumericExpressionInternal( + expression: String, + errorCheckingMode: ErrorCheckingMode + ): MathParsingResult { + return MathExpressionParser.parseNumericExpression(expression, errorCheckingMode) + } + } +} From 22ae591c2db2d44a5d06b9a3e894df56d0db903e Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 15 Dec 2021 11:27:15 -0800 Subject: [PATCH 013/162] Add exp evaluation & LaTeX conversion support. This is mainly copied from #2173. --- .../testing/math/MathEquationSubject.kt | 11 + .../testing/math/MathExpressionSubject.kt | 30 ++ .../org/oppia/android/util/math/BUILD.bazel | 108 +++++++- .../util/math/ExpressionToLatexConverter.kt | 74 +++++ .../android/util/math/FractionExtensions.kt | 137 +++++++++ .../util/math/MathExpressionExtensions.kt | 13 + .../util/math/NumericExpressionEvaluator.kt | 70 +++++ .../oppia/android/util/math/RealExtensions.kt | 260 ++++++++++++++++++ .../util/math/AlgebraicEquationParserTest.kt | 1 + .../math/AlgebraicExpressionParserTest.kt | 70 +++++ .../org/oppia/android/util/math/BUILD.bazel | 20 ++ .../util/math/ExpressionToLatexTest.kt | 123 +++++++++ .../util/math/NumericExpressionParserTest.kt | 71 +++++ 13 files changed, 977 insertions(+), 11 deletions(-) create mode 100644 utility/src/main/java/org/oppia/android/util/math/ExpressionToLatexConverter.kt create mode 100644 utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt create mode 100644 utility/src/main/java/org/oppia/android/util/math/NumericExpressionEvaluator.kt create mode 100644 utility/src/test/java/org/oppia/android/util/math/ExpressionToLatexTest.kt diff --git a/testing/src/main/java/org/oppia/android/testing/math/MathEquationSubject.kt b/testing/src/main/java/org/oppia/android/testing/math/MathEquationSubject.kt index ce24e1e08cc..373b1434b0e 100644 --- a/testing/src/main/java/org/oppia/android/testing/math/MathEquationSubject.kt +++ b/testing/src/main/java/org/oppia/android/testing/math/MathEquationSubject.kt @@ -1,10 +1,13 @@ package org.oppia.android.testing.math import com.google.common.truth.FailureMetadata +import com.google.common.truth.StringSubject import com.google.common.truth.Truth.assertAbout +import com.google.common.truth.Truth.assertThat import com.google.common.truth.extensions.proto.LiteProtoSubject import org.oppia.android.app.model.MathEquation import org.oppia.android.testing.math.MathExpressionSubject.Companion.assertThat +import org.oppia.android.util.math.toRawLatex class MathEquationSubject( metadata: FailureMetadata, @@ -14,6 +17,14 @@ class MathEquationSubject( fun hasRightHandSideThat(): MathExpressionSubject = assertThat(actual.rightSide) + fun convertsToLatexStringThat(): StringSubject = + assertThat(convertToLatex(divAsFraction = false)) + + fun convertsWithFractionsToLatexStringThat(): StringSubject = + assertThat(convertToLatex(divAsFraction = true)) + + private fun convertToLatex(divAsFraction: Boolean): String = actual.toRawLatex(divAsFraction) + companion object { fun assertThat(actual: MathEquation): MathEquationSubject = assertAbout(::MathEquationSubject).that(actual) diff --git a/testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt b/testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt index c9be134e209..eea079a7e4b 100644 --- a/testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt +++ b/testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt @@ -1,6 +1,8 @@ package org.oppia.android.testing.math +import com.google.common.truth.DoubleSubject import com.google.common.truth.FailureMetadata +import com.google.common.truth.IntegerSubject import com.google.common.truth.StringSubject import com.google.common.truth.Truth.assertAbout import com.google.common.truth.Truth.assertThat @@ -17,6 +19,8 @@ import org.oppia.android.app.model.MathFunctionCall import org.oppia.android.app.model.MathUnaryOperation import org.oppia.android.app.model.Real import org.oppia.android.testing.math.RealSubject.Companion.assertThat +import org.oppia.android.util.math.evaluateAsNumericExpression +import org.oppia.android.util.math.toRawLatex // See: https://kotlinlang.org/docs/type-safe-builders.html. class MathExpressionSubject( @@ -28,6 +32,32 @@ class MathExpressionSubject( ExpressionComparator.createFromExpression(actual).also(init) } + fun evaluatesToRationalThat(): FractionSubject = + FractionSubject.assertThat(evaluateAsReal(expectedType = Real.RealTypeCase.RATIONAL).rational) + + fun evaluatesToIrrationalThat(): DoubleSubject = + assertThat(evaluateAsReal(expectedType = Real.RealTypeCase.IRRATIONAL).irrational) + + fun evaluatesToIntegerThat(): IntegerSubject = + assertThat(evaluateAsReal(expectedType = Real.RealTypeCase.INTEGER).integer) + + fun convertsToLatexStringThat(): StringSubject = + assertThat(convertToLatex(divAsFraction = false)) + + fun convertsWithFractionsToLatexStringThat(): StringSubject = + assertThat(convertToLatex(divAsFraction = true)) + + private fun evaluateAsReal(expectedType: Real.RealTypeCase): Real { + val real = actual.evaluateAsNumericExpression() + assertWithMessage("Failed to evaluate numeric expression").that(real).isNotNull() + assertWithMessage("Expected constant to evaluate to $expectedType") + .that(real?.realTypeCase) + .isEqualTo(expectedType) + return checkNotNull(real) // Just to remove the nullable operator; the actual check is above. + } + + private fun convertToLatex(divAsFraction: Boolean): String = actual.toRawLatex(divAsFraction) + // TODO: update DSL to not have return values (since it's unnecessary). @ExpressionComparatorMarker class ExpressionComparator private constructor(private val expression: MathExpression) { diff --git a/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel b/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel index 1e0da7381b5..351b04e5f33 100644 --- a/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel @@ -4,20 +4,18 @@ TODO: document load("@io_bazel_rules_kotlin//kotlin:kotlin.bzl", "kt_android_library") -kt_android_library( +android_library( name = "extensions", - srcs = [ - "FloatExtensions.kt", - "FractionExtensions.kt", - "PolynomialExtensions.kt", - "RatioExtensions.kt", - "RealExtensions.kt", - ], visibility = [ "//:oppia_api_visibility", ], - deps = [ - "//model/src/main/proto:math_java_proto_lite", + exports = [ + ":float_extensions", + ":fraction_extensions", + ":math_expression_extensions", + ":polynomial_extensions", + ":ratio_extensions", + ":real_extensions", ], ) @@ -43,9 +41,9 @@ kt_android_library( "//:oppia_testing_visibility", ], deps = [ - ":extensions", ":parsing_error", ":peekable_iterator", + ":real_extensions", ":tokenizer", "//model/src/main/proto:math_java_proto_lite", ], @@ -71,3 +69,91 @@ kt_android_library( "PeekableIterator.kt", ], ) + +kt_android_library( + name = "float_extensions", + srcs = [ + "FloatExtensions.kt", + ], + deps = [ + "//model/src/main/proto:math_java_proto_lite", + ], +) + +kt_android_library( + name = "fraction_extensions", + srcs = [ + "FractionExtensions.kt", + ], + deps = [ + "//model/src/main/proto:math_java_proto_lite", + ], +) + +kt_android_library( + name = "math_expression_extensions", + srcs = [ + "MathExpressionExtensions.kt", + ], + deps = [ + ":expression_to_latex_converter", + ":numeric_expression_evaluator", + "//model/src/main/proto:math_java_proto_lite", + ], +) + +kt_android_library( + name = "polynomial_extensions", + srcs = [ + "PolynomialExtensions.kt", + ], + deps = [ + ":real_extensions", + "//model/src/main/proto:math_java_proto_lite", + ], +) + +kt_android_library( + name = "ratio_extensions", + srcs = [ + "RatioExtensions.kt", + ], + deps = [ + ":fraction_extensions", + "//model/src/main/proto:math_java_proto_lite", + ], +) + +kt_android_library( + name = "real_extensions", + srcs = [ + "RealExtensions.kt", + ], + deps = [ + ":float_extensions", + ":fraction_extensions", + "//model/src/main/proto:math_java_proto_lite", + ], +) + +kt_android_library( + name = "expression_to_latex_converter", + srcs = [ + "ExpressionToLatexConverter.kt", + ], + deps = [ + ":real_extensions", + "//model/src/main/proto:math_java_proto_lite", + ], +) + +kt_android_library( + name = "numeric_expression_evaluator", + srcs = [ + "NumericExpressionEvaluator.kt", + ], + deps = [ + ":real_extensions", + "//model/src/main/proto:math_java_proto_lite", + ], +) diff --git a/utility/src/main/java/org/oppia/android/util/math/ExpressionToLatexConverter.kt b/utility/src/main/java/org/oppia/android/util/math/ExpressionToLatexConverter.kt new file mode 100644 index 00000000000..2c108f02fa7 --- /dev/null +++ b/utility/src/main/java/org/oppia/android/util/math/ExpressionToLatexConverter.kt @@ -0,0 +1,74 @@ +package org.oppia.android.util.math + +import org.oppia.android.app.model.MathBinaryOperation.Operator.ADD +import org.oppia.android.app.model.MathBinaryOperation.Operator.DIVIDE +import org.oppia.android.app.model.MathBinaryOperation.Operator.EXPONENTIATE +import org.oppia.android.app.model.MathBinaryOperation.Operator.MULTIPLY +import org.oppia.android.app.model.MathBinaryOperation.Operator.SUBTRACT +import org.oppia.android.app.model.MathEquation +import org.oppia.android.app.model.MathExpression +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.BINARY_OPERATION +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.CONSTANT +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.EXPRESSIONTYPE_NOT_SET +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.FUNCTION_CALL +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.GROUP +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.UNARY_OPERATION +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.VARIABLE +import org.oppia.android.app.model.MathFunctionCall.FunctionType +import org.oppia.android.app.model.MathFunctionCall.FunctionType.FUNCTION_UNSPECIFIED +import org.oppia.android.app.model.MathFunctionCall.FunctionType.SQUARE_ROOT +import org.oppia.android.app.model.MathUnaryOperation.Operator.NEGATE +import org.oppia.android.app.model.MathUnaryOperation.Operator.POSITIVE +import org.oppia.android.app.model.MathBinaryOperation.Operator as BinaryOperator +import org.oppia.android.app.model.MathUnaryOperation.Operator as UnaryOperator + +class ExpressionToLatexConverter private constructor() { + companion object { + fun MathEquation.convertToLatex(divAsFraction: Boolean): String { + val lhs = leftSide + val rhs = rightSide + return "${lhs.convertToLatex(divAsFraction)} = ${rhs.convertToLatex(divAsFraction)}" + } + + fun MathExpression.convertToLatex(divAsFraction: Boolean): String { + return when (expressionTypeCase) { + CONSTANT -> constant.toPlainText() + VARIABLE -> variable + BINARY_OPERATION -> { + val lhsLatex = binaryOperation.leftOperand.convertToLatex(divAsFraction) + val rhsLatex = binaryOperation.rightOperand.convertToLatex(divAsFraction) + when (binaryOperation.operator) { + ADD -> "$lhsLatex + $rhsLatex" + SUBTRACT -> "$lhsLatex - $rhsLatex" + MULTIPLY -> if (binaryOperation.isImplicit) { + "$lhsLatex$rhsLatex" + } else "$lhsLatex \\times $rhsLatex" + DIVIDE -> if (divAsFraction) { + "\\frac{$lhsLatex}{$rhsLatex}" + } else "$lhsLatex \\div $rhsLatex" + EXPONENTIATE -> "$lhsLatex ^ {$rhsLatex}" + BinaryOperator.OPERATOR_UNSPECIFIED, BinaryOperator.UNRECOGNIZED, null -> + "$lhsLatex $rhsLatex" + } + } + UNARY_OPERATION -> { + val operandLatex = unaryOperation.operand.convertToLatex(divAsFraction) + when (unaryOperation.operator) { + NEGATE -> "-$operandLatex" + POSITIVE -> "+$operandLatex" + UnaryOperator.OPERATOR_UNSPECIFIED, UnaryOperator.UNRECOGNIZED, null -> operandLatex + } + } + FUNCTION_CALL -> { + val argumentLatex = functionCall.argument.convertToLatex(divAsFraction) + when (functionCall.functionType) { + SQUARE_ROOT -> "\\sqrt{$argumentLatex}" + FUNCTION_UNSPECIFIED, FunctionType.UNRECOGNIZED, null -> argumentLatex + } + } + GROUP -> "(${group.convertToLatex(divAsFraction)})" + EXPRESSIONTYPE_NOT_SET, null -> "" + } + } + } +} diff --git a/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt index 1da9fef1857..e229fd49e40 100644 --- a/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt @@ -1,6 +1,7 @@ package org.oppia.android.util.math import org.oppia.android.app.model.Fraction +import kotlin.math.absoluteValue /** Returns whether this fraction has a fractional component. */ fun Fraction.hasFractionalPart(): Boolean { @@ -15,6 +16,12 @@ fun Fraction.isOnlyWholeNumber(): Boolean { return !hasFractionalPart() } +/** + * Returns this fraction as a whole number. Note that this will not return a value that is + * mathematically equivalent to this fraction unless [isOnlyWholeNumber] returns true. + */ +fun Fraction.toWholeNumber(): Int = if (isNegative) -wholeNumber else wholeNumber + /** * Returns a [Double] version of this fraction. * @@ -68,6 +75,22 @@ fun Fraction.toSimplestForm(): Fraction { }.build() } +/** + * Returns this fraction in its proper form by first converting to simplest denominator, then + * extracting a whole number component. + * + * This function will properly convert a fraction whose denominator is 1 into a whole number-only + * fraction. + */ +fun Fraction.toProperForm(): Fraction { + return toSimplestForm().let { + it.toBuilder().apply { + wholeNumber = it.wholeNumber + (it.numerator / it.denominator) + numerator = it.numerator % it.denominator + }.build() + } +} + /** * Returns this fraction in an improper form (that is, with a 0 whole number and only fractional * parts). @@ -80,12 +103,126 @@ fun Fraction.toImproperForm(): Fraction { }.build() } +/** Returns the inverse improper fraction representation of this fraction. */ +fun Fraction.toInvertedImproperForm(): Fraction { + return toImproperForm().let { improper -> + improper.toBuilder().apply { + numerator = improper.denominator + denominator = improper.numerator + }.build() + } +} + /** Returns the negated form of this fraction. */ operator fun Fraction.unaryMinus(): Fraction { return toBuilder().apply { isNegative = !this@unaryMinus.isNegative }.build() } +/** Adds two fractions together and returns a new one in its proper form. */ +operator fun Fraction.plus(rhs: Fraction): Fraction { + // First, eliminate the whole number by computing improper fractions. + val leftFraction = toImproperForm() + val rightFraction = rhs.toImproperForm() + + // Second, find a common denominator and compute the new numerators. + val commonDenominator = lcm(leftFraction.denominator, rightFraction.denominator) + val leftFactor = commonDenominator / leftFraction.denominator + val rightFactor = commonDenominator / rightFraction.denominator + val leftNumerator = leftFraction.numerator * leftFactor + val rightNumerator = rightFraction.numerator * rightFactor + + // Third, determine how the numerators are combined (based on negatives) and whether the result is + // negative. + val leftNeg = leftFraction.isNegative + val rightNeg = rightFraction.isNegative + val (newNumerator, isNegative) = when { + leftNeg && rightNeg -> leftNumerator + rightNumerator to true + !leftNeg && !rightNeg -> leftNumerator + rightNumerator to false + leftNeg && !rightNeg -> + (-leftNumerator + rightNumerator).absoluteValue to (leftNumerator > rightNumerator) + !leftNeg && rightNeg -> + (leftNumerator - rightNumerator).absoluteValue to (rightNumerator > leftNumerator) + else -> throw Exception("Impossible case") + } + + // Finally, compute the new fraction and convert it to proper form to compute its whole number. + return Fraction.newBuilder().apply { + this.isNegative = isNegative + numerator = newNumerator + denominator = commonDenominator + }.build().toProperForm() +} + +/** + * Subtracts the specified fraction from this fraction and returns the result in its proper form. + */ +operator fun Fraction.minus(rhs: Fraction): Fraction { + // a - b = a + -b + return this + -rhs +} + +/** Multiples this fraction by the specified and returns the result in its proper form. */ +operator fun Fraction.times(rhs: Fraction): Fraction { + // First, convert both fractions into their improper forms. + val leftFraction = toImproperForm() + val rightFraction = rhs.toImproperForm() + + // Second, multiple the numerators and denominators piece-wise. + val newNumerator = leftFraction.numerator * rightFraction.numerator + val newDenominator = leftFraction.denominator * rightFraction.denominator + + // Third, determine negative (negative is retained if only one is negative). + val isNegative = leftFraction.isNegative xor rightFraction.isNegative + return Fraction.newBuilder().apply { + this.isNegative = isNegative + numerator = newNumerator + denominator = newDenominator + }.build().toProperForm() +} + +/** Returns the proper form of the division from this fraction by the specified fraction. */ +operator fun Fraction.div(rhs: Fraction): Fraction { + // a / b = a * b^-1 (b's inverse). + return this * rhs.toInvertedImproperForm() +} + +fun Fraction.pow(exp: Int): Fraction { + return when { + exp == 0 -> { + Fraction.newBuilder().apply { + wholeNumber = 1 + denominator = 1 + }.build() + } + exp == 1 -> this + // x^-2 == 1/(x^2). + exp < 1 -> pow(-exp).toInvertedImproperForm().toProperForm() + else -> { // i > 1 + var newValue = this + for (i in 1 until exp) newValue *= this + return newValue + } + } +} + +/** Returns the [Fraction] representation of this integer (as a whole number fraction). */ +fun Int.toWholeNumberFraction(): Fraction { + val intValue = this + return Fraction.newBuilder().apply { + isNegative = intValue < 0 + wholeNumber = kotlin.math.abs(intValue) + numerator = 0 + denominator = 1 + }.build() +} + /** Returns the greatest common divisor between two integers. */ fun gcd(x: Int, y: Int): Int { return if (y == 0) x else gcd(y, x % y) } + +/** Returns the least common multiple between two integers. */ +private fun lcm(x: Int, y: Int): Int { + // Reference: https://en.wikipedia.org/wiki/Least_common_multiple#Calculation. + return (x * y).absoluteValue / gcd(x, y) +} diff --git a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt new file mode 100644 index 00000000000..59b203a0d2d --- /dev/null +++ b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt @@ -0,0 +1,13 @@ +package org.oppia.android.util.math + +import org.oppia.android.app.model.MathEquation +import org.oppia.android.app.model.MathExpression +import org.oppia.android.app.model.Real +import org.oppia.android.util.math.ExpressionToLatexConverter.Companion.convertToLatex +import org.oppia.android.util.math.NumericExpressionEvaluator.Companion.evaluate + +fun MathEquation.toRawLatex(divAsFraction: Boolean): String = convertToLatex(divAsFraction) + +fun MathExpression.toRawLatex(divAsFraction: Boolean): String = convertToLatex(divAsFraction) + +fun MathExpression.evaluateAsNumericExpression(): Real? = evaluate() diff --git a/utility/src/main/java/org/oppia/android/util/math/NumericExpressionEvaluator.kt b/utility/src/main/java/org/oppia/android/util/math/NumericExpressionEvaluator.kt new file mode 100644 index 00000000000..766da590400 --- /dev/null +++ b/utility/src/main/java/org/oppia/android/util/math/NumericExpressionEvaluator.kt @@ -0,0 +1,70 @@ +package org.oppia.android.util.math + +import org.oppia.android.app.model.MathBinaryOperation +import org.oppia.android.app.model.MathBinaryOperation.Operator.ADD +import org.oppia.android.app.model.MathBinaryOperation.Operator.DIVIDE +import org.oppia.android.app.model.MathBinaryOperation.Operator.EXPONENTIATE +import org.oppia.android.app.model.MathBinaryOperation.Operator.MULTIPLY +import org.oppia.android.app.model.MathBinaryOperation.Operator.SUBTRACT +import org.oppia.android.app.model.MathExpression +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.BINARY_OPERATION +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.CONSTANT +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.EXPRESSIONTYPE_NOT_SET +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.FUNCTION_CALL +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.GROUP +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.UNARY_OPERATION +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.VARIABLE +import org.oppia.android.app.model.MathFunctionCall +import org.oppia.android.app.model.MathFunctionCall.FunctionType +import org.oppia.android.app.model.MathFunctionCall.FunctionType.FUNCTION_UNSPECIFIED +import org.oppia.android.app.model.MathFunctionCall.FunctionType.SQUARE_ROOT +import org.oppia.android.app.model.MathUnaryOperation +import org.oppia.android.app.model.MathUnaryOperation.Operator.NEGATE +import org.oppia.android.app.model.MathUnaryOperation.Operator.POSITIVE +import org.oppia.android.app.model.Real +import org.oppia.android.app.model.MathBinaryOperation.Operator as BinaryOperator +import org.oppia.android.app.model.MathUnaryOperation.Operator as UnaryOperator + +class NumericExpressionEvaluator private constructor() { + companion object { + fun MathExpression.evaluate(): Real? { + return when (expressionTypeCase) { + CONSTANT -> constant + VARIABLE -> null // Variables not supported in numeric expressions. + BINARY_OPERATION -> binaryOperation.evaluate() + UNARY_OPERATION -> unaryOperation.evaluate() + FUNCTION_CALL -> functionCall.evaluate() + GROUP -> group.evaluate() + EXPRESSIONTYPE_NOT_SET, null -> null + } + } + + private fun MathBinaryOperation.evaluate(): Real? { + return when (operator) { + ADD -> rightOperand.evaluate()?.let { leftOperand.evaluate()?.plus(it) } + SUBTRACT -> rightOperand.evaluate()?.let { leftOperand.evaluate()?.minus(it) } + MULTIPLY -> rightOperand.evaluate()?.let { leftOperand.evaluate()?.times(it) } + DIVIDE -> rightOperand.evaluate()?.let { leftOperand.evaluate()?.div(it) } + EXPONENTIATE -> rightOperand.evaluate()?.let { leftOperand.evaluate()?.pow(it) } + BinaryOperator.OPERATOR_UNSPECIFIED, BinaryOperator.UNRECOGNIZED, null -> null + } + } + + private fun MathUnaryOperation.evaluate(): Real? { + return when (operator) { + NEGATE -> operand.evaluate()?.let { -it } + POSITIVE -> operand.evaluate() // '+2' is the same as just '2'. + UnaryOperator.OPERATOR_UNSPECIFIED, UnaryOperator.UNRECOGNIZED, null -> null + } + } + + private fun MathFunctionCall.evaluate(): Real? { + return when (functionType) { + SQUARE_ROOT -> argument.evaluate()?.let { sqrt(it) } + FUNCTION_UNSPECIFIED, + FunctionType.UNRECOGNIZED, + null -> null + } + } + } +} diff --git a/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt index 6df36abd3b6..83605b05808 100644 --- a/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt @@ -1,13 +1,17 @@ package org.oppia.android.util.math +import org.oppia.android.app.model.Fraction import org.oppia.android.app.model.Real import org.oppia.android.app.model.Real.RealTypeCase.INTEGER import org.oppia.android.app.model.Real.RealTypeCase.IRRATIONAL import org.oppia.android.app.model.Real.RealTypeCase.RATIONAL import org.oppia.android.app.model.Real.RealTypeCase.REALTYPE_NOT_SET +import kotlin.math.pow fun Real.isRational(): Boolean = realTypeCase == RATIONAL +fun Real.isInteger(): Boolean = realTypeCase == INTEGER + fun Real.isNegative(): Boolean = when (realTypeCase) { RATIONAL -> rational.isNegative IRRATIONAL -> irrational < 0 @@ -46,8 +50,264 @@ operator fun Real.unaryMinus(): Real { } } +operator fun Real.plus(rhs: Real): Real { + return combine( + this, rhs, Fraction::plus, Fraction::plus, Fraction::plus, Double::plus, Double::plus, + Double::plus, Int::plus, Int::plus, Int::add + ) +} + +operator fun Real.minus(rhs: Real): Real { + return combine( + this, rhs, Fraction::minus, Fraction::minus, Fraction::minus, Double::minus, Double::minus, + Double::minus, Int::minus, Int::minus, Int::subtract + ) +} + +operator fun Real.times(rhs: Real): Real { + return combine( + this, rhs, Fraction::times, Fraction::times, Fraction::times, Double::times, Double::times, + Double::times, Int::times, Int::times, Int::multiply + ) +} + +operator fun Real.div(rhs: Real): Real { + return combine( + this, rhs, Fraction::div, Fraction::div, Fraction::div, Double::div, Double::div, Double::div, + Int::div, Int::div, Int::divide + ) +} + +fun Real.pow(rhs: Real): Real { + // Powers can really only be effectively done via floats or whole-number only fractions. + return when (realTypeCase) { + RATIONAL -> { + // Left-hand side is Fraction. + when (rhs.realTypeCase) { + RATIONAL -> recompute { + if (rhs.rational.isOnlyWholeNumber()) { + // The fraction can be retained. + it.setRational(rational.pow(rhs.rational.wholeNumber)) + } else { + // The fraction can't realistically be retained since it's being raised to an actual + // fraction, resulting in an irrational number. + it.setIrrational(rational.toDouble().pow(rhs.rational.toDouble())) + } + } + IRRATIONAL -> recompute { it.setIrrational(rational.pow(rhs.irrational)) } + INTEGER -> recompute { it.setRational(rational.pow(rhs.integer)) } + REALTYPE_NOT_SET, null -> throw Exception("Invalid real: $rhs.") + } + } + IRRATIONAL -> { + // Left-hand side is a double. + when (rhs.realTypeCase) { + RATIONAL -> recompute { it.setIrrational(irrational.pow(rhs.rational)) } + IRRATIONAL -> recompute { it.setIrrational(irrational.pow(rhs.irrational)) } + INTEGER -> recompute { it.setIrrational(irrational.pow(rhs.integer)) } + REALTYPE_NOT_SET, null -> throw Exception("Invalid real: $rhs.") + } + } + INTEGER -> { + // Left-hand side is an integer. + when (rhs.realTypeCase) { + RATIONAL -> { + if (rhs.rational.isOnlyWholeNumber()) { + // Whole number-only fractions are effectively just int^int. + integer.pow(rhs.rational.wholeNumber) + } else { + // Otherwise, raising by a fraction will result in an irrational number. + recompute { it.setIrrational(integer.toDouble().pow(rhs.rational.toDouble())) } + } + } + IRRATIONAL -> recompute { it.setIrrational(integer.toDouble().pow(rhs.irrational)) } + INTEGER -> integer.pow(rhs.integer) + REALTYPE_NOT_SET, null -> throw Exception("Invalid real: $rhs.") + } + } + REALTYPE_NOT_SET, null -> throw Exception("Invalid real: $this.") + } +} + +fun sqrt(real: Real): Real { + return when (real.realTypeCase) { + RATIONAL -> sqrt(real.rational) + IRRATIONAL -> real.recompute { it.setIrrational(kotlin.math.sqrt(real.irrational)) } + INTEGER -> sqrt(real.integer) + REALTYPE_NOT_SET, null -> throw Exception("Invalid real: $real.") + } +} + fun abs(real: Real): Real = if (real.isNegative()) -real else real +private operator fun Double.plus(rhs: Fraction): Double = this + rhs.toDouble() +private operator fun Fraction.plus(rhs: Double): Double = toDouble() + rhs +private operator fun Fraction.plus(rhs: Int): Fraction = this + rhs.toWholeNumberFraction() +private operator fun Int.plus(rhs: Fraction): Fraction = toWholeNumberFraction() + rhs +private operator fun Double.minus(rhs: Fraction): Double = this - rhs.toDouble() +private operator fun Fraction.minus(rhs: Double): Double = toDouble() - rhs +private operator fun Fraction.minus(rhs: Int): Fraction = this - rhs.toWholeNumberFraction() +private operator fun Int.minus(rhs: Fraction): Fraction = toWholeNumberFraction() - rhs +private operator fun Double.times(rhs: Fraction): Double = this * rhs.toDouble() +private operator fun Fraction.times(rhs: Double): Double = toDouble() * rhs +private operator fun Fraction.times(rhs: Int): Fraction = this * rhs.toWholeNumberFraction() +private operator fun Int.times(rhs: Fraction): Fraction = toWholeNumberFraction() * rhs +private operator fun Double.div(rhs: Fraction): Double = this / rhs.toDouble() +private operator fun Fraction.div(rhs: Double): Double = toDouble() / rhs +private operator fun Fraction.div(rhs: Int): Fraction = this / rhs.toWholeNumberFraction() +private operator fun Int.div(rhs: Fraction): Fraction = toWholeNumberFraction() / rhs + +private fun Int.add(rhs: Int): Real = Real.newBuilder().apply { integer = this@add + rhs }.build() +private fun Int.subtract(rhs: Int): Real = Real.newBuilder().apply { + integer = this@subtract - rhs +}.build() +private fun Int.multiply(rhs: Int): Real = Real.newBuilder().apply { + integer = this@multiply * rhs +}.build() +private fun Int.divide(rhs: Int): Real = Real.newBuilder().apply { + // If rhs divides this integer, retain the integer. + val lhs = this@divide + if ((lhs % rhs) == 0) { + integer = lhs / rhs + } else { + // Otherwise, keep precision by turning the division into a fraction. + rational = Fraction.newBuilder().apply { + isNegative = (lhs < 0) xor (rhs < 0) + numerator = kotlin.math.abs(lhs) + denominator = kotlin.math.abs(rhs) + }.build() + } +}.build() + +private fun Double.pow(rhs: Fraction): Double = this.pow(rhs.toDouble()) +private fun Fraction.pow(rhs: Double): Double = toDouble().pow(rhs) + +private fun Int.pow(exp: Int): Real { + return when { + exp == 0 -> Real.newBuilder().apply { integer = 0 }.build() + exp == 1 -> Real.newBuilder().apply { integer = this@pow }.build() + exp < 0 -> Real.newBuilder().apply { rational = toWholeNumberFraction().pow(exp) }.build() + else -> { + // exp > 1 + var computed = this + for (i in 0 until exp - 1) computed *= this + Real.newBuilder().apply { integer = computed }.build() + } + } +} + +private fun sqrt(fraction: Fraction): Real { + val improper = fraction.toImproperForm() + + // Attempt to take the root of the fraction's numerator & denominator. + val numeratorRoot = sqrt(improper.numerator) + val denominatorRoot = sqrt(improper.denominator) + + // If both values stayed as integers, the original fraction can be retained. Otherwise, the + // fraction must be evaluated by performing a division. + return Real.newBuilder().apply { + if (numeratorRoot.realTypeCase == denominatorRoot.realTypeCase && numeratorRoot.isInteger()) { + val rootedFraction = Fraction.newBuilder().apply { + isNegative = improper.isNegative + numerator = numeratorRoot.integer + denominator = denominatorRoot.integer + }.build().toProperForm() + if (rootedFraction.isOnlyWholeNumber()) { + // If the fractional form doesn't need to be kept, remove it. + integer = rootedFraction.toWholeNumber() + } else { + rational = rootedFraction + } + } else { + irrational = numeratorRoot.toDouble() + } + }.build() +} + +private fun sqrt(int: Int): Real { + // First, check if the integer is a square. Reference for possible methods: + // https://www.researchgate.net/post/How-to-decide-if-a-given-number-will-have-integer-square-root-or-not. + var potentialRoot = 2 + while ((potentialRoot * potentialRoot) < int) { + potentialRoot++ + } + if (potentialRoot * potentialRoot == int) { + // There's an exact integer representation of the root. + return Real.newBuilder().apply { + integer = potentialRoot + }.build() + } + + // Otherwise, compute the irrational square root. + return Real.newBuilder().apply { + irrational = kotlin.math.sqrt(int.toDouble()) + }.build() +} + private fun Real.recompute(transform: (Real.Builder) -> Real.Builder): Real { return transform(newBuilderForType()).build() } + +// TODO: consider replacing this with inline alternatives since they'll probably be simpler. +private fun combine( + lhs: Real, + rhs: Real, + leftRationalRightRationalOp: (Fraction, Fraction) -> Fraction, + leftRationalRightIrrationalOp: (Fraction, Double) -> Double, + leftRationalRightIntegerOp: (Fraction, Int) -> Fraction, + leftIrrationalRightRationalOp: (Double, Fraction) -> Double, + leftIrrationalRightIrrationalOp: (Double, Double) -> Double, + leftIrrationalRightIntegerOp: (Double, Int) -> Double, + leftIntegerRightRationalOp: (Int, Fraction) -> Fraction, + leftIntegerRightIrrationalOp: (Int, Double) -> Double, + leftIntegerRightIntegerOp: (Int, Int) -> Real, +): Real { + return when (lhs.realTypeCase) { + RATIONAL -> { + // Left-hand side is Fraction. + when (rhs.realTypeCase) { + RATIONAL -> + lhs.recompute { it.setRational(leftRationalRightRationalOp(lhs.rational, rhs.rational)) } + IRRATIONAL -> + lhs.recompute { + it.setIrrational(leftRationalRightIrrationalOp(lhs.rational, rhs.irrational)) + } + INTEGER -> + lhs.recompute { it.setRational(leftRationalRightIntegerOp(lhs.rational, rhs.integer)) } + REALTYPE_NOT_SET, null -> throw Exception("Invalid real: $rhs.") + } + } + IRRATIONAL -> { + // Left-hand side is a double. + when (rhs.realTypeCase) { + RATIONAL -> + lhs.recompute { + it.setIrrational(leftIrrationalRightRationalOp(lhs.irrational, rhs.rational)) + } + IRRATIONAL -> + lhs.recompute { + it.setIrrational(leftIrrationalRightIrrationalOp(lhs.irrational, rhs.irrational)) + } + INTEGER -> + lhs.recompute { + it.setIrrational(leftIrrationalRightIntegerOp(lhs.irrational, rhs.integer)) + } + REALTYPE_NOT_SET, null -> throw Exception("Invalid real: $rhs.") + } + } + INTEGER -> { + // Left-hand side is an integer. + when (rhs.realTypeCase) { + RATIONAL -> + lhs.recompute { it.setRational(leftIntegerRightRationalOp(lhs.integer, rhs.rational)) } + IRRATIONAL -> + lhs.recompute { + it.setIrrational(leftIntegerRightIrrationalOp(lhs.integer, rhs.irrational)) + } + INTEGER -> leftIntegerRightIntegerOp(lhs.integer, rhs.integer) + REALTYPE_NOT_SET, null -> throw Exception("Invalid real: $rhs.") + } + } + REALTYPE_NOT_SET, null -> throw Exception("Invalid real: $lhs.") + } +} diff --git a/utility/src/test/java/org/oppia/android/util/math/AlgebraicEquationParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/AlgebraicEquationParserTest.kt index 94fc6e50ab4..a2cb45387d9 100644 --- a/utility/src/test/java/org/oppia/android/util/math/AlgebraicEquationParserTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/AlgebraicEquationParserTest.kt @@ -25,6 +25,7 @@ class AlgebraicEquationParserTest { withNameThat().isEqualTo("x") } } + assertThat(equation1).hasRightHandSideThat().evaluatesToIntegerThat().isEqualTo(1) val equation2 = parseAlgebraicEquationSuccessfully( diff --git a/utility/src/test/java/org/oppia/android/util/math/AlgebraicExpressionParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/AlgebraicExpressionParserTest.kt index 9fea4084970..62c88f449a6 100644 --- a/utility/src/test/java/org/oppia/android/util/math/AlgebraicExpressionParserTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/AlgebraicExpressionParserTest.kt @@ -10,6 +10,7 @@ import org.oppia.android.testing.math.MathExpressionSubject.Companion.assertThat import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult import org.robolectric.annotation.LooperMode +import kotlin.math.sqrt /** Tests for [MathExpressionParser]. */ @RunWith(AndroidJUnit4::class) @@ -27,6 +28,7 @@ class AlgebraicExpressionParserTest { withValueThat().isIntegerThat().isEqualTo(1) } } + assertThat(expression1).evaluatesToIntegerThat().isEqualTo(1) val expression61 = parseAlgebraicExpressionWithAllErrors("x") assertThat(expression61).hasStructureThatMatches { @@ -41,6 +43,7 @@ class AlgebraicExpressionParserTest { withValueThat().isIntegerThat().isEqualTo(2) } } + assertThat(expression2).evaluatesToIntegerThat().isEqualTo(2) val expression3 = parseAlgebraicExpressionWithAllErrors(" 2.5 ") assertThat(expression3).hasStructureThatMatches { @@ -48,6 +51,7 @@ class AlgebraicExpressionParserTest { withValueThat().isIrrationalThat().isWithin(1e-5).of(2.5) } } + assertThat(expression3).evaluatesToIrrationalThat().isWithin(1e-5).of(2.5) val expression62 = parseAlgebraicExpressionWithAllErrors(" y ") assertThat(expression62).hasStructureThatMatches { @@ -96,6 +100,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression4).evaluatesToIntegerThat().isEqualTo(512) val expression23 = parseAlgebraicExpressionWithAllErrors("(2^3)^2") assertThat(expression23).hasStructureThatMatches { @@ -123,6 +128,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression23).evaluatesToIntegerThat().isEqualTo(64) val expression24 = parseAlgebraicExpressionWithAllErrors("512/32/4") assertThat(expression24).hasStructureThatMatches { @@ -148,6 +154,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression24).evaluatesToIntegerThat().isEqualTo(4) val expression25 = parseAlgebraicExpressionWithAllErrors("512/(32/4)") assertThat(expression25).hasStructureThatMatches { @@ -175,6 +182,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression25).evaluatesToIntegerThat().isEqualTo(64) val expression5 = parseAlgebraicExpressionWithAllErrors("sqrt(2)") assertThat(expression5).hasStructureThatMatches { @@ -186,6 +194,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression5).evaluatesToIrrationalThat().isWithin(1e-5).of(sqrt(2.0)) expectFailureWhenParsingAlgebraicExpression("sqr(2)") @@ -231,6 +240,7 @@ class AlgebraicExpressionParserTest { withValueThat().isIntegerThat().isEqualTo(732) } } + assertThat(expression6).evaluatesToIntegerThat().isEqualTo(732) expectFailureWhenParsingAlgebraicExpression("73 2") @@ -259,6 +269,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression32).evaluatesToIntegerThat().isEqualTo(1027) val expression7 = parseAlgebraicExpressionWithoutOptionalErrors("3*2-3+4^7*8/3*2+7") assertThat(expression7).hasStructureThatMatches { @@ -356,6 +367,11 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression7) + .evaluatesToRationalThat() + .evaluatesToRealThat() + .isWithin(1e-5) + .of(87391.333333333) expectFailureWhenParsingAlgebraicExpression("x = √2 × 7 ÷ 4") @@ -396,6 +412,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression8).evaluatesToIntegerThat().isEqualTo(21) // Right implicit multiplication of numbers isn't allowed. expectFailureWhenParsingAlgebraicExpression("(1+2)2") @@ -426,6 +443,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression10).evaluatesToIntegerThat().isEqualTo(6) // Right implicit multiplication of numbers isn't allowed. expectFailureWhenParsingAlgebraicExpression("sqrt(2)3") @@ -449,6 +467,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression12).evaluatesToIrrationalThat().isWithin(1e-5).of(3.0 * sqrt(2.0)) val expression65 = parseAlgebraicExpressionWithAllErrors("xsqrt(2)") assertThat(expression65).hasStructureThatMatches { @@ -529,6 +548,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression13).evaluatesToIrrationalThat().isWithin(1e-5).of(-123.036579926) val expression58 = parseAlgebraicExpressionWithAllErrors("sqrt(2)(1+2)(3-2^5)") assertThat(expression58).hasStructureThatMatches { @@ -589,6 +609,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression58).evaluatesToIrrationalThat().isWithin(1e-5).of(-123.036579926) val expression14 = parseAlgebraicExpressionWithoutOptionalErrors("((3))") assertThat(expression14).hasStructureThatMatches { @@ -600,6 +621,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression14).evaluatesToIntegerThat().isEqualTo(3) val expression15 = parseAlgebraicExpressionWithoutOptionalErrors("++3") assertThat(expression15).hasStructureThatMatches { @@ -615,6 +637,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression15).evaluatesToIntegerThat().isEqualTo(3) val expression16 = parseAlgebraicExpressionWithoutOptionalErrors("--4") assertThat(expression16).hasStructureThatMatches { @@ -630,6 +653,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression16).evaluatesToIntegerThat().isEqualTo(4) val expression17 = parseAlgebraicExpressionWithAllErrors("1+-4") assertThat(expression17).hasStructureThatMatches { @@ -650,6 +674,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression17).evaluatesToIntegerThat().isEqualTo(-3) val expression18 = parseAlgebraicExpressionWithAllErrors("1++4") assertThat(expression18).hasStructureThatMatches { @@ -670,6 +695,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression18).evaluatesToIntegerThat().isEqualTo(5) val expression19 = parseAlgebraicExpressionWithAllErrors("1--4") assertThat(expression19).hasStructureThatMatches { @@ -690,6 +716,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression19).evaluatesToIntegerThat().isEqualTo(5) expectFailureWhenParsingAlgebraicExpression("1-^-4") @@ -721,6 +748,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression20).evaluatesToIrrationalThat().isWithin(1e-5).of((sqrt(2.0) * 7.0) / 4.0) expectFailureWhenParsingAlgebraicExpression("1+2 &asdf") @@ -761,6 +789,10 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression21) + .evaluatesToIrrationalThat() + .isWithin(1e-5) + .of(sqrt(2.0) * sqrt(3.0) * sqrt(4.0)) val expression22 = parseAlgebraicExpressionWithAllErrors("(1+2)(3-7^2)(5+-17)") assertThat(expression22).hasStructureThatMatches { @@ -835,6 +867,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression22).evaluatesToIntegerThat().isEqualTo(1656) val expression26 = parseAlgebraicExpressionWithAllErrors("3^-2") assertThat(expression26).hasStructureThatMatches { @@ -855,6 +888,12 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression26).evaluatesToRationalThat().apply { + hasNegativePropertyThat().isFalse() + hasWholeNumberThat().isEqualTo(0) + hasNumeratorThat().isEqualTo(1) + hasDenominatorThat().isEqualTo(9) + } val expression27 = parseAlgebraicExpressionWithoutOptionalErrors("(3^-2)^(3^-2)") assertThat(expression27).hasStructureThatMatches { @@ -901,6 +940,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression27).evaluatesToIrrationalThat().isWithin(1e-5).of(0.78338103693) val expression28 = parseAlgebraicExpressionWithAllErrors("1-3^sqrt(4)") assertThat(expression28).hasStructureThatMatches { @@ -930,6 +970,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression28).evaluatesToIntegerThat().isEqualTo(-8) // "Hard" order of operation problems loosely based on & other problems that can often stump // people: https://www.basic-mathematics.com/hard-order-of-operations-problems.html. @@ -968,6 +1009,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression29).evaluatesToRationalThat().evaluatesToRealThat().isWithin(1e-5).of(10.5) val expression59 = parseAlgebraicExpressionWithAllErrors("3÷2(3+4)") assertThat(expression59).hasStructureThatMatches { @@ -1004,6 +1046,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression59).evaluatesToRationalThat().evaluatesToRealThat().isWithin(1e-5).of(10.5) // Numbers cannot have implicit multiplication unless they are in groups. expectFailureWhenParsingAlgebraicExpression("2 2") @@ -1042,6 +1085,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression31).evaluatesToIntegerThat().isEqualTo(60) val expression33 = parseAlgebraicExpressionWithoutOptionalErrors("2^(3)") assertThat(expression33).hasStructureThatMatches { @@ -1060,6 +1104,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression33).evaluatesToIntegerThat().isEqualTo(8) // Verify that implicit multiple has lower precedence than exponentiation. val expression34 = parseAlgebraicExpressionWithoutOptionalErrors("2^(3)(4)") @@ -1090,6 +1135,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression34).evaluatesToIntegerThat().isEqualTo(32) // An exponentiation can never be an implicit right operand. expectFailureWhenParsingAlgebraicExpression("2^(3)2^2") @@ -1129,6 +1175,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression35).evaluatesToIntegerThat().isEqualTo(32) // An exponentiation can be a right operand of an implicit mult if it's grouped. val expression36 = parseAlgebraicExpressionWithoutOptionalErrors("2^(3)(2^2)") @@ -1168,6 +1215,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression36).evaluatesToIntegerThat().isEqualTo(32) // An exponentiation can never be an implicit right operand. expectFailureWhenParsingAlgebraicExpression("2^3(4)2^3") @@ -1225,6 +1273,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression38).evaluatesToIntegerThat().isEqualTo(256) expectFailureWhenParsingAlgebraicExpression("2^2 2^2") expectFailureWhenParsingAlgebraicExpression("(3) 2^2") @@ -1255,6 +1304,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression39).evaluatesToIntegerThat().isEqualTo(-3) // Should pass for algebra. val expression66 = parseAlgebraicExpressionWithAllErrors("-2 x") @@ -1308,6 +1358,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression40).evaluatesToIntegerThat().isEqualTo(-6) val expression41 = parseAlgebraicExpressionWithoutOptionalErrors("-2^3(4)") assertThat(expression41).hasStructureThatMatches { @@ -1339,6 +1390,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression41).evaluatesToIntegerThat().isEqualTo(-32) val expression43 = parseAlgebraicExpressionWithoutOptionalErrors("√2^2(3)") assertThat(expression43).hasStructureThatMatches { @@ -1370,6 +1422,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression43).evaluatesToIrrationalThat().isWithin(1e-5).of(6.0) val expression60 = parseAlgebraicExpressionWithoutOptionalErrors("√(2^2(3))") assertThat(expression60).hasStructureThatMatches { @@ -1403,6 +1456,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression60).evaluatesToIrrationalThat().isWithin(1e-5).of(sqrt(12.0)) val expression42 = parseAlgebraicExpressionWithAllErrors("-2*-2") // Note that the following structure is not the same as (-2)*(-2) since unary negation has @@ -1430,6 +1484,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression42).evaluatesToIntegerThat().isEqualTo(4) val expression44 = parseAlgebraicExpressionWithoutOptionalErrors("2(2)") assertThat(expression44).hasStructureThatMatches { @@ -1448,6 +1503,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression44).evaluatesToIntegerThat().isEqualTo(4) val expression45 = parseAlgebraicExpressionWithAllErrors("2sqrt(2)") assertThat(expression45).hasStructureThatMatches { @@ -1468,6 +1524,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression45).evaluatesToIrrationalThat().isWithin(1e-5).of(2.0 * sqrt(2.0)) val expression46 = parseAlgebraicExpressionWithAllErrors("2√2") assertThat(expression46).hasStructureThatMatches { @@ -1488,6 +1545,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression46).evaluatesToIrrationalThat().isWithin(1e-5).of(2.0 * sqrt(2.0)) val expression47 = parseAlgebraicExpressionWithoutOptionalErrors("(2)(2)") assertThat(expression47).hasStructureThatMatches { @@ -1508,6 +1566,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression47).evaluatesToIntegerThat().isEqualTo(4) val expression48 = parseAlgebraicExpressionWithoutOptionalErrors("sqrt(2)(2)") assertThat(expression48).hasStructureThatMatches { @@ -1530,6 +1589,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression48).evaluatesToIrrationalThat().isWithin(1e-5).of(2.0 * sqrt(2.0)) val expression49 = parseAlgebraicExpressionWithAllErrors("sqrt(2)sqrt(2)") assertThat(expression49).hasStructureThatMatches { @@ -1554,6 +1614,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression49).evaluatesToIrrationalThat().isWithin(1e-5).of(sqrt(2.0) * sqrt(2.0)) val expression50 = parseAlgebraicExpressionWithAllErrors("√2√2") assertThat(expression50).hasStructureThatMatches { @@ -1578,6 +1639,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression50).evaluatesToIrrationalThat().isWithin(1e-5).of(sqrt(2.0) * sqrt(2.0)) val expression51 = parseAlgebraicExpressionWithoutOptionalErrors("(2)(2)(2)") assertThat(expression51).hasStructureThatMatches { @@ -1609,6 +1671,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression51).evaluatesToIntegerThat().isEqualTo(8) val expression52 = parseAlgebraicExpressionWithAllErrors("sqrt(2)sqrt(2)sqrt(2)") assertThat(expression52).hasStructureThatMatches { @@ -1646,6 +1709,8 @@ class AlgebraicExpressionParserTest { } } } + val sqrt2 = sqrt(2.0) + assertThat(expression52).evaluatesToIrrationalThat().isWithin(1e-5).of(sqrt2 * sqrt2 * sqrt2) val expression53 = parseAlgebraicExpressionWithAllErrors("√2√2√2") assertThat(expression53).hasStructureThatMatches { @@ -1683,6 +1748,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression53).evaluatesToIrrationalThat().isWithin(1e-5).of(sqrt2 * sqrt2 * sqrt2) // Should fail for algebra. expectFailureWhenParsingAlgebraicExpression("x7") @@ -1801,6 +1867,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression54).evaluatesToIntegerThat().isEqualTo(13) val expression55 = parseAlgebraicExpressionWithAllErrors("3/(1-2)") assertThat(expression55).hasStructureThatMatches { @@ -1828,6 +1895,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression55).evaluatesToIntegerThat().isEqualTo(-3) val expression56 = parseAlgebraicExpressionWithoutOptionalErrors("(3)/(1-2)") assertThat(expression56).hasStructureThatMatches { @@ -1857,6 +1925,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression56).evaluatesToIntegerThat().isEqualTo(-3) val expression57 = parseAlgebraicExpressionWithoutOptionalErrors("3/((1-2))") assertThat(expression57).hasStructureThatMatches { @@ -1886,6 +1955,7 @@ class AlgebraicExpressionParserTest { } } } + assertThat(expression57).evaluatesToIntegerThat().isEqualTo(-3) // TODO: add others, including tests for malformed expressions throughout the parser & // tokenizer. diff --git a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel index 2a8b0c295c4..282038acd19 100644 --- a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel @@ -49,6 +49,26 @@ oppia_android_test( ], ) +oppia_android_test( + name = "ExpressionToLatexTest", + srcs = ["ExpressionToLatexTest.kt"], + custom_package = "org.oppia.android.util.math", + test_class = "org.oppia.android.util.math.ExpressionToLatexTest", + test_manifest = "//utility:test_manifest", + deps = [ + "//model/src/main/proto:math_java_proto_lite", + "//testing/src/main/java/org/oppia/android/testing/math:math_equation_subject", + "//testing/src/main/java/org/oppia/android/testing/math:math_expression_subject", + "//third_party:androidx_test_ext_junit", + "//third_party:com_google_truth_extensions_truth-liteproto-extension", + "//third_party:com_google_truth_truth", + "//third_party:junit_junit", + "//third_party:org_robolectric_robolectric", + "//third_party:robolectric_android-all", + "//utility/src/main/java/org/oppia/android/util/math:parser", + ], +) + oppia_android_test( name = "MathExpressionParserTest", srcs = ["MathExpressionParserTest.kt"], diff --git a/utility/src/test/java/org/oppia/android/util/math/ExpressionToLatexTest.kt b/utility/src/test/java/org/oppia/android/util/math/ExpressionToLatexTest.kt new file mode 100644 index 00000000000..e56db9a7500 --- /dev/null +++ b/utility/src/test/java/org/oppia/android/util/math/ExpressionToLatexTest.kt @@ -0,0 +1,123 @@ +package org.oppia.android.util.math + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Test +import org.junit.runner.RunWith +import org.oppia.android.app.model.MathEquation +import org.oppia.android.app.model.MathExpression +import org.oppia.android.testing.math.MathEquationSubject.Companion.assertThat +import org.oppia.android.testing.math.MathExpressionSubject.Companion.assertThat +import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode +import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult +import org.robolectric.annotation.LooperMode + +/** Tests for [MathExpressionParser]. */ +@RunWith(AndroidJUnit4::class) +@LooperMode(LooperMode.Mode.PAUSED) +class ExpressionToLatexTest { + @Test + fun testLatex() { + // TODO: split up & move to separate test suites. Finish test cases. + + val exp1 = parseNumericExpressionWithAllErrors("1") + assertThat(exp1).convertsToLatexStringThat().isEqualTo("1") + + val exp2 = parseNumericExpressionWithAllErrors("1+2") + assertThat(exp2).convertsToLatexStringThat().isEqualTo("1 + 2") + + val exp3 = parseNumericExpressionWithAllErrors("1*2") + assertThat(exp3).convertsToLatexStringThat().isEqualTo("1 \\times 2") + + val exp4 = parseNumericExpressionWithAllErrors("1/2") + assertThat(exp4).convertsToLatexStringThat().isEqualTo("1 \\div 2") + + val exp5 = parseNumericExpressionWithAllErrors("1/2") + assertThat(exp5).convertsWithFractionsToLatexStringThat().isEqualTo("\\frac{1}{2}") + + val exp10 = parseNumericExpressionWithAllErrors("√2") + assertThat(exp10).convertsToLatexStringThat().isEqualTo("\\sqrt{2}") + + val exp11 = parseNumericExpressionWithAllErrors("√(1/2)") + assertThat(exp11).convertsToLatexStringThat().isEqualTo("\\sqrt{(1 \\div 2)}") + + val exp6 = parseAlgebraicExpressionWithAllErrors("x+y") + assertThat(exp6).convertsToLatexStringThat().isEqualTo("x + y") + + val exp7 = parseAlgebraicExpressionWithoutOptionalErrors("x^(1/y)") + assertThat(exp7).convertsToLatexStringThat().isEqualTo("x ^ {(1 \\div y)}") + + val exp8 = parseAlgebraicExpressionWithoutOptionalErrors("x^(1/y)") + assertThat(exp8).convertsWithFractionsToLatexStringThat().isEqualTo("x ^ {(\\frac{1}{y})}") + + val exp9 = parseAlgebraicExpressionWithoutOptionalErrors("x^y^z") + assertThat(exp9).convertsWithFractionsToLatexStringThat().isEqualTo("x ^ {y ^ {z}}") + + val eq1 = + parseAlgebraicEquationWithAllErrors( + "7a^2+b^2+c^2=0", allowedVariables = listOf("a", "b", "c") + ) + assertThat(eq1).convertsToLatexStringThat().isEqualTo("7a ^ {2} + b ^ {2} + c ^ {2} = 0") + + val eq2 = parseAlgebraicEquationWithAllErrors("sqrt(1+x)/x=1") + assertThat(eq2).convertsToLatexStringThat().isEqualTo("\\sqrt{1 + x} \\div x = 1") + + val eq3 = parseAlgebraicEquationWithAllErrors("sqrt(1+x)/x=1") + assertThat(eq3) + .convertsWithFractionsToLatexStringThat() + .isEqualTo("\\frac{\\sqrt{1 + x}}{x} = 1") + } + + private companion object { + // TODO: fix helper API. + + private fun parseNumericExpressionWithAllErrors(expression: String): MathExpression { + val result = + MathExpressionParser.parseNumericExpression(expression, ErrorCheckingMode.ALL_ERRORS) + return (result as MathParsingResult.Success).result + } + + private fun parseAlgebraicExpressionWithoutOptionalErrors( + expression: String, + allowedVariables: List = listOf("x", "y", "z") + ): MathExpression { + val result = + parseAlgebraicExpressionInternal( + expression, ErrorCheckingMode.REQUIRED_ONLY, allowedVariables + ) + return (result as MathParsingResult.Success).result + } + + private fun parseAlgebraicExpressionWithAllErrors( + expression: String, + allowedVariables: List = listOf("x", "y", "z") + ): MathExpression { + val result = + parseAlgebraicExpressionInternal( + expression, ErrorCheckingMode.ALL_ERRORS, allowedVariables + ) + return (result as MathParsingResult.Success).result + } + + private fun parseAlgebraicExpressionInternal( + expression: String, + errorCheckingMode: ErrorCheckingMode, + allowedVariables: List = listOf("x", "y", "z") + ): MathParsingResult { + return MathExpressionParser.parseAlgebraicExpression( + expression, allowedVariables, errorCheckingMode + ) + } + + private fun parseAlgebraicEquationWithAllErrors( + expression: String, + allowedVariables: List = listOf("x", "y", "z") + ): MathEquation { + val result = + MathExpressionParser.parseAlgebraicEquation( + expression, allowedVariables, + ErrorCheckingMode.ALL_ERRORS + ) + return (result as MathParsingResult.Success).result + } + } +} diff --git a/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt index d1f9e17b47d..2dacceff76e 100644 --- a/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt @@ -10,11 +10,13 @@ import org.oppia.android.testing.math.MathExpressionSubject.Companion.assertThat import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult import org.robolectric.annotation.LooperMode +import kotlin.math.sqrt /** Tests for [MathExpressionParser]. */ @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) class NumericExpressionParserTest { + @Test fun testLotsOfCasesForNumericExpression() { // TODO: split this up @@ -27,6 +29,7 @@ class NumericExpressionParserTest { withValueThat().isIntegerThat().isEqualTo(1) } } + assertThat(expression1).evaluatesToIntegerThat().isEqualTo(1) expectFailureWhenParsingNumericExpression("x") @@ -36,6 +39,7 @@ class NumericExpressionParserTest { withValueThat().isIntegerThat().isEqualTo(2) } } + assertThat(expression2).evaluatesToIntegerThat().isEqualTo(2) val expression3 = parseNumericExpressionWithAllErrors(" 2.5 ") assertThat(expression3).hasStructureThatMatches { @@ -43,6 +47,7 @@ class NumericExpressionParserTest { withValueThat().isIrrationalThat().isWithin(1e-5).of(2.5) } } + assertThat(expression3).evaluatesToIrrationalThat().isWithin(1e-5).of(2.5) expectFailureWhenParsingNumericExpression(" x ") @@ -72,6 +77,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression4).evaluatesToIntegerThat().isEqualTo(512) val expression23 = parseNumericExpressionWithAllErrors("(2^3)^2") assertThat(expression23).hasStructureThatMatches { @@ -99,6 +105,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression23).evaluatesToIntegerThat().isEqualTo(64) val expression24 = parseNumericExpressionWithAllErrors("512/32/4") assertThat(expression24).hasStructureThatMatches { @@ -124,6 +131,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression24).evaluatesToIntegerThat().isEqualTo(4) val expression25 = parseNumericExpressionWithAllErrors("512/(32/4)") assertThat(expression25).hasStructureThatMatches { @@ -151,6 +159,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression25).evaluatesToIntegerThat().isEqualTo(64) val expression5 = parseNumericExpressionWithAllErrors("sqrt(2)") assertThat(expression5).hasStructureThatMatches { @@ -162,6 +171,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression5).evaluatesToIrrationalThat().isWithin(1e-5).of(sqrt(2.0)) expectFailureWhenParsingNumericExpression("sqr(2)") @@ -173,6 +183,7 @@ class NumericExpressionParserTest { withValueThat().isIntegerThat().isEqualTo(732) } } + assertThat(expression6).evaluatesToIntegerThat().isEqualTo(732) // Verify order of operations between higher & lower precedent operators. val expression32 = parseNumericExpressionWithAllErrors("3+4^5") @@ -199,6 +210,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression32).evaluatesToIntegerThat().isEqualTo(1027) val expression7 = parseNumericExpressionWithoutOptionalErrors("3*2-3+4^7*8/3*2+7") assertThat(expression7).hasStructureThatMatches { @@ -296,6 +308,11 @@ class NumericExpressionParserTest { } } } + assertThat(expression7) + .evaluatesToRationalThat() + .evaluatesToRealThat() + .isWithin(1e-5) + .of(87391.333333333) expectFailureWhenParsingNumericExpression("x = √2 × 7 ÷ 4") @@ -336,6 +353,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression8).evaluatesToIntegerThat().isEqualTo(21) // Right implicit multiplication of numbers isn't allowed. expectFailureWhenParsingNumericExpression("(1+2)2") @@ -366,6 +384,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression10).evaluatesToIntegerThat().isEqualTo(6) // Right implicit multiplication of numbers isn't allowed. expectFailureWhenParsingNumericExpression("sqrt(2)3") @@ -389,6 +408,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression12).evaluatesToIrrationalThat().isWithin(1e-5).of(3.0 * sqrt(2.0)) expectFailureWhenParsingNumericExpression("xsqrt(2)") @@ -451,6 +471,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression13).evaluatesToIrrationalThat().isWithin(1e-5).of(-123.036579926) val expression58 = parseNumericExpressionWithAllErrors("sqrt(2)(1+2)(3-2^5)") assertThat(expression58).hasStructureThatMatches { @@ -511,6 +532,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression58).evaluatesToIrrationalThat().isWithin(1e-5).of(-123.036579926) val expression14 = parseNumericExpressionWithoutOptionalErrors("((3))") assertThat(expression14).hasStructureThatMatches { @@ -522,6 +544,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression14).evaluatesToIntegerThat().isEqualTo(3) val expression15 = parseNumericExpressionWithoutOptionalErrors("++3") assertThat(expression15).hasStructureThatMatches { @@ -537,6 +560,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression15).evaluatesToIntegerThat().isEqualTo(3) val expression16 = parseNumericExpressionWithoutOptionalErrors("--4") assertThat(expression16).hasStructureThatMatches { @@ -552,6 +576,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression16).evaluatesToIntegerThat().isEqualTo(4) val expression17 = parseNumericExpressionWithAllErrors("1+-4") assertThat(expression17).hasStructureThatMatches { @@ -572,6 +597,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression17).evaluatesToIntegerThat().isEqualTo(-3) val expression18 = parseNumericExpressionWithAllErrors("1++4") assertThat(expression18).hasStructureThatMatches { @@ -592,6 +618,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression18).evaluatesToIntegerThat().isEqualTo(5) val expression19 = parseNumericExpressionWithAllErrors("1--4") assertThat(expression19).hasStructureThatMatches { @@ -612,6 +639,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression19).evaluatesToIntegerThat().isEqualTo(5) expectFailureWhenParsingNumericExpression("1-^-4") @@ -643,6 +671,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression20).evaluatesToIrrationalThat().isWithin(1e-5).of((sqrt(2.0) * 7.0) / 4.0) expectFailureWhenParsingNumericExpression("1+2 &asdf") @@ -683,6 +712,10 @@ class NumericExpressionParserTest { } } } + assertThat(expression21) + .evaluatesToIrrationalThat() + .isWithin(1e-5) + .of(sqrt(2.0) * sqrt(3.0) * sqrt(4.0)) val expression22 = parseNumericExpressionWithAllErrors("(1+2)(3-7^2)(5+-17)") assertThat(expression22).hasStructureThatMatches { @@ -757,6 +790,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression22).evaluatesToIntegerThat().isEqualTo(1656) val expression26 = parseNumericExpressionWithAllErrors("3^-2") assertThat(expression26).hasStructureThatMatches { @@ -777,6 +811,12 @@ class NumericExpressionParserTest { } } } + assertThat(expression26).evaluatesToRationalThat().apply { + hasNegativePropertyThat().isFalse() + hasWholeNumberThat().isEqualTo(0) + hasNumeratorThat().isEqualTo(1) + hasDenominatorThat().isEqualTo(9) + } val expression27 = parseNumericExpressionWithoutOptionalErrors("(3^-2)^(3^-2)") assertThat(expression27).hasStructureThatMatches { @@ -823,6 +863,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression27).evaluatesToIrrationalThat().isWithin(1e-5).of(0.78338103693) val expression28 = parseNumericExpressionWithAllErrors("1-3^sqrt(4)") assertThat(expression28).hasStructureThatMatches { @@ -852,6 +893,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression28).evaluatesToIntegerThat().isEqualTo(-8) // "Hard" order of operation problems loosely based on & other problems that can often stump // people: https://www.basic-mathematics.com/hard-order-of-operations-problems.html. @@ -890,6 +932,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression29).evaluatesToRationalThat().evaluatesToRealThat().isWithin(1e-5).of(10.5) val expression59 = parseNumericExpressionWithAllErrors("3÷2(3+4)") assertThat(expression59).hasStructureThatMatches { @@ -926,6 +969,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression59).evaluatesToRationalThat().evaluatesToRealThat().isWithin(1e-5).of(10.5) // Numbers cannot have implicit multiplication unless they are in groups. expectFailureWhenParsingNumericExpression("2 2") @@ -964,6 +1008,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression31).evaluatesToIntegerThat().isEqualTo(60) val expression33 = parseNumericExpressionWithoutOptionalErrors("2^(3)") assertThat(expression33).hasStructureThatMatches { @@ -982,6 +1027,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression33).evaluatesToIntegerThat().isEqualTo(8) // Verify that implicit multiple has lower precedence than exponentiation. val expression34 = parseNumericExpressionWithoutOptionalErrors("2^(3)(4)") @@ -1012,6 +1058,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression34).evaluatesToIntegerThat().isEqualTo(32) // An exponentiation can never be an implicit right operand. expectFailureWhenParsingNumericExpression("2^(3)2^2") @@ -1051,6 +1098,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression35).evaluatesToIntegerThat().isEqualTo(32) // An exponentiation can be a right operand of an implicit mult if it's grouped. val expression36 = parseNumericExpressionWithoutOptionalErrors("2^(3)(2^2)") @@ -1090,6 +1138,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression36).evaluatesToIntegerThat().isEqualTo(32) // An exponentiation can never be an implicit right operand. expectFailureWhenParsingNumericExpression("2^3(4)2^3") @@ -1147,6 +1196,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression38).evaluatesToIntegerThat().isEqualTo(256) expectFailureWhenParsingNumericExpression("2^2 2^2") expectFailureWhenParsingNumericExpression("(3) 2^2") @@ -1177,6 +1227,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression39).evaluatesToIntegerThat().isEqualTo(-3) // Should pass for algebra. expectFailureWhenParsingNumericExpression("-2 x") @@ -1212,6 +1263,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression40).evaluatesToIntegerThat().isEqualTo(-6) val expression41 = parseNumericExpressionWithoutOptionalErrors("-2^3(4)") assertThat(expression41).hasStructureThatMatches { @@ -1243,6 +1295,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression41).evaluatesToIntegerThat().isEqualTo(-32) val expression43 = parseNumericExpressionWithoutOptionalErrors("√2^2(3)") assertThat(expression43).hasStructureThatMatches { @@ -1274,6 +1327,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression43).evaluatesToIrrationalThat().isWithin(1e-5).of(6.0) val expression60 = parseNumericExpressionWithoutOptionalErrors("√(2^2(3))") assertThat(expression60).hasStructureThatMatches { @@ -1307,6 +1361,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression60).evaluatesToIrrationalThat().isWithin(1e-5).of(sqrt(12.0)) val expression42 = parseNumericExpressionWithAllErrors("-2*-2") // Note that the following structure is not the same as (-2)*(-2) since unary negation has @@ -1334,6 +1389,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression42).evaluatesToIntegerThat().isEqualTo(4) // TODO: Here & elsewhere, fix the fact that this is actually a valid use of single-term // parentheses (there's a bug in the current error detection logic). @@ -1354,6 +1410,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression44).evaluatesToIntegerThat().isEqualTo(4) val expression45 = parseNumericExpressionWithAllErrors("2sqrt(2)") assertThat(expression45).hasStructureThatMatches { @@ -1374,6 +1431,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression45).evaluatesToIrrationalThat().isWithin(1e-5).of(2.0 * sqrt(2.0)) val expression46 = parseNumericExpressionWithAllErrors("2√2") assertThat(expression46).hasStructureThatMatches { @@ -1394,6 +1452,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression46).evaluatesToIrrationalThat().isWithin(1e-5).of(2.0 * sqrt(2.0)) val expression47 = parseNumericExpressionWithoutOptionalErrors("(2)(2)") assertThat(expression47).hasStructureThatMatches { @@ -1414,6 +1473,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression47).evaluatesToIntegerThat().isEqualTo(4) val expression48 = parseNumericExpressionWithoutOptionalErrors("sqrt(2)(2)") assertThat(expression48).hasStructureThatMatches { @@ -1436,6 +1496,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression48).evaluatesToIrrationalThat().isWithin(1e-5).of(2.0 * sqrt(2.0)) val expression49 = parseNumericExpressionWithAllErrors("sqrt(2)sqrt(2)") assertThat(expression49).hasStructureThatMatches { @@ -1460,6 +1521,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression49).evaluatesToIrrationalThat().isWithin(1e-5).of(sqrt(2.0) * sqrt(2.0)) val expression50 = parseNumericExpressionWithAllErrors("√2√2") assertThat(expression50).hasStructureThatMatches { @@ -1484,6 +1546,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression50).evaluatesToIrrationalThat().isWithin(1e-5).of(sqrt(2.0) * sqrt(2.0)) val expression51 = parseNumericExpressionWithoutOptionalErrors("(2)(2)(2)") assertThat(expression51).hasStructureThatMatches { @@ -1515,6 +1578,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression51).evaluatesToIntegerThat().isEqualTo(8) val expression52 = parseNumericExpressionWithAllErrors("sqrt(2)sqrt(2)sqrt(2)") assertThat(expression52).hasStructureThatMatches { @@ -1552,6 +1616,8 @@ class NumericExpressionParserTest { } } } + val sqrt2 = sqrt(2.0) + assertThat(expression52).evaluatesToIrrationalThat().isWithin(1e-5).of(sqrt2 * sqrt2 * sqrt2) val expression53 = parseNumericExpressionWithAllErrors("√2√2√2") assertThat(expression53).hasStructureThatMatches { @@ -1589,6 +1655,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression53).evaluatesToIrrationalThat().isWithin(1e-5).of(sqrt2 * sqrt2 * sqrt2) // Should fail for algebra. expectFailureWhenParsingNumericExpression("x7") @@ -1652,6 +1719,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression54).evaluatesToIntegerThat().isEqualTo(13) val expression55 = parseNumericExpressionWithAllErrors("3/(1-2)") assertThat(expression55).hasStructureThatMatches { @@ -1679,6 +1747,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression55).evaluatesToIntegerThat().isEqualTo(-3) val expression56 = parseNumericExpressionWithoutOptionalErrors("(3)/(1-2)") assertThat(expression56).hasStructureThatMatches { @@ -1708,6 +1777,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression56).evaluatesToIntegerThat().isEqualTo(-3) val expression57 = parseNumericExpressionWithoutOptionalErrors("3/((1-2))") assertThat(expression57).hasStructureThatMatches { @@ -1737,6 +1807,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression57).evaluatesToIntegerThat().isEqualTo(-3) // TODO: add others, including tests for malformed expressions throughout the parser & // tokenizer. From d9d4963fc292d23bb765a95170b1fe9bae10f855 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 15 Dec 2021 11:29:25 -0800 Subject: [PATCH 014/162] Remove unneeded comment lines. --- .../src/test/java/org/oppia/android/util/math/BUILD.bazel | 7 ------- 1 file changed, 7 deletions(-) diff --git a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel index 2a8b0c295c4..c4791dffc69 100644 --- a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel @@ -4,13 +4,6 @@ TODO: document load("//:oppia_android_test.bzl", "oppia_android_test") -# "//testing/src/main/java/org/oppia/android/testing/math:comparable_operation_list_subject", -# "//testing/src/main/java/org/oppia/android/testing/math:fraction_subject", -# "//testing/src/main/java/org/oppia/android/testing/math:math_equation_subject", -# "//testing/src/main/java/org/oppia/android/testing/math:math_expression_subject", -# "//testing/src/main/java/org/oppia/android/testing/math:polynomial_subject", -# "//testing/src/main/java/org/oppia/android/testing/math:real_subject", - oppia_android_test( name = "AlgebraicEquationParserTest", srcs = ["AlgebraicEquationParserTest.kt"], From 1a8a8e85ce1c15f0e3565d10aa1392136ee94e7f Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 15 Dec 2021 13:39:57 -0800 Subject: [PATCH 015/162] Add expr->comparable operation list conv support. This enables the ability to compare two expressions such that operation associativity and commutativity is considered (i.e. items can be rearranged using those rules without breaking expression equality). This is mostly copied from #2173. --- .../org/oppia/android/util/math/BUILD.bazel | 21 + .../android/util/math/ComparatorExtensions.kt | 57 + ...ssionToComparableOperationListConverter.kt | 300 +++ .../util/math/MathExpressionExtensions.kt | 35 + .../oppia/android/util/math/RealExtensions.kt | 2 + .../org/oppia/android/util/math/BUILD.bazel | 19 + ...ExpressionToComparableOperationListTest.kt | 1637 +++++++++++++++++ 7 files changed, 2071 insertions(+) create mode 100644 utility/src/main/java/org/oppia/android/util/math/ComparatorExtensions.kt create mode 100644 utility/src/main/java/org/oppia/android/util/math/ExpressionToComparableOperationListConverter.kt create mode 100644 utility/src/test/java/org/oppia/android/util/math/ExpressionToComparableOperationListTest.kt diff --git a/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel b/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel index 351b04e5f33..fbd04084430 100644 --- a/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel @@ -10,6 +10,7 @@ android_library( "//:oppia_api_visibility", ], exports = [ + ":comparator_extensions", ":float_extensions", ":fraction_extensions", ":math_expression_extensions", @@ -70,6 +71,13 @@ kt_android_library( ], ) +kt_android_library( + name = "comparator_extensions", + srcs = [ + "ComparatorExtensions.kt", + ], +) + kt_android_library( name = "float_extensions", srcs = [ @@ -96,6 +104,7 @@ kt_android_library( "MathExpressionExtensions.kt", ], deps = [ + ":expression_to_comparable_operation_list_converter", ":expression_to_latex_converter", ":numeric_expression_evaluator", "//model/src/main/proto:math_java_proto_lite", @@ -136,6 +145,18 @@ kt_android_library( ], ) +kt_android_library( + name = "expression_to_comparable_operation_list_converter", + srcs = [ + "ExpressionToComparableOperationListConverter.kt", + ], + deps = [ + ":comparator_extensions", + ":real_extensions", + "//model/src/main/proto:math_java_proto_lite", + ], +) + kt_android_library( name = "expression_to_latex_converter", srcs = [ diff --git a/utility/src/main/java/org/oppia/android/util/math/ComparatorExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/ComparatorExtensions.kt new file mode 100644 index 00000000000..284ec69fe69 --- /dev/null +++ b/utility/src/main/java/org/oppia/android/util/math/ComparatorExtensions.kt @@ -0,0 +1,57 @@ +package org.oppia.android.util.math + +import java.util.SortedSet + +fun comparingDeferred( + keySelector: (T) -> U, + comparatorSelector: () -> Comparator +): Comparator { + // Store as captured val for memoization. + val comparator by lazy { comparatorSelector() } + return Comparator.comparing(keySelector) { o1, o2 -> + comparator.compare(o1, o2) + } +} + +fun > Comparator.thenComparingReversed( + keySelector: (T) -> U +): Comparator = thenComparing(Comparator.comparing(keySelector).reversed()) + +fun > Comparator.thenSelectAmong( + enumSelector: (T) -> E, + vararg comparators: Pair> +): Comparator { + val comparatorMap = comparators.toMap() + return thenComparing( + Comparator { o1, o2 -> + val enum1 = enumSelector(o1) + val enum2 = enumSelector(o2) + check(enum1 == enum2) { + "Expected objects to have the same enum values: $o1 ($enum1), $o2 ($enum2)" + } + val comparator = + checkNotNull(comparatorMap[enum1]) { "No comparator for matched enum: $enum1" } + return@Comparator comparator.compare(o1, o2) + } + ) +} + +fun Comparator.toSetComparator(): Comparator> { + val itemComparator = this + return Comparator { first, second -> + // Reference: https://stackoverflow.com/a/30107086. + val firstIter = first.iterator() + val secondIter = second.iterator() + while (firstIter.hasNext() && secondIter.hasNext()) { + val comparison = itemComparator.compare(firstIter.next(), secondIter.next()) + if (comparison != 0) return@Comparator comparison // Found a different item. + } + + // Everything is equal up to here, see if the lists are different length. + return@Comparator when { + firstIter.hasNext() -> 1 // The first list is longer, therefore "greater." + secondIter.hasNext() -> -1 // Ditto, but for the second list. + else -> 0 // Otherwise, they're the same length with all equal items (and are thus equal). + } + } +} diff --git a/utility/src/main/java/org/oppia/android/util/math/ExpressionToComparableOperationListConverter.kt b/utility/src/main/java/org/oppia/android/util/math/ExpressionToComparableOperationListConverter.kt new file mode 100644 index 00000000000..4cfa9acec66 --- /dev/null +++ b/utility/src/main/java/org/oppia/android/util/math/ExpressionToComparableOperationListConverter.kt @@ -0,0 +1,300 @@ +package org.oppia.android.util.math + +import org.oppia.android.app.model.ComparableOperationList +import org.oppia.android.app.model.ComparableOperationList.CommutativeAccumulation +import org.oppia.android.app.model.ComparableOperationList.CommutativeAccumulation.AccumulationType +import org.oppia.android.app.model.ComparableOperationList.CommutativeAccumulation.AccumulationType.ACCUMULATION_TYPE_UNSPECIFIED +import org.oppia.android.app.model.ComparableOperationList.CommutativeAccumulation.AccumulationType.PRODUCT +import org.oppia.android.app.model.ComparableOperationList.CommutativeAccumulation.AccumulationType.SUMMATION +import org.oppia.android.app.model.ComparableOperationList.ComparableOperation +import org.oppia.android.app.model.ComparableOperationList.ComparableOperation.ComparisonTypeCase.COMPARISONTYPE_NOT_SET +import org.oppia.android.app.model.ComparableOperationList.ComparableOperation.ComparisonTypeCase.CONSTANT_TERM +import org.oppia.android.app.model.ComparableOperationList.ComparableOperation.ComparisonTypeCase.NON_COMMUTATIVE_OPERATION +import org.oppia.android.app.model.ComparableOperationList.ComparableOperation.ComparisonTypeCase.VARIABLE_TERM +import org.oppia.android.app.model.ComparableOperationList.NonCommutativeOperation +import org.oppia.android.app.model.ComparableOperationList.NonCommutativeOperation.OperationTypeCase +import org.oppia.android.app.model.MathBinaryOperation.Operator.ADD +import org.oppia.android.app.model.MathBinaryOperation.Operator.DIVIDE +import org.oppia.android.app.model.MathBinaryOperation.Operator.EXPONENTIATE +import org.oppia.android.app.model.MathBinaryOperation.Operator.MULTIPLY +import org.oppia.android.app.model.MathBinaryOperation.Operator.SUBTRACT +import org.oppia.android.app.model.MathExpression +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.BINARY_OPERATION +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.CONSTANT +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.EXPRESSIONTYPE_NOT_SET +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.FUNCTION_CALL +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.GROUP +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.UNARY_OPERATION +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.VARIABLE +import org.oppia.android.app.model.MathFunctionCall.FunctionType +import org.oppia.android.app.model.MathFunctionCall.FunctionType.SQUARE_ROOT +import org.oppia.android.app.model.MathUnaryOperation.Operator.NEGATE +import org.oppia.android.app.model.MathUnaryOperation.Operator.POSITIVE +import org.oppia.android.app.model.MathBinaryOperation.Operator as BinaryOperator +import org.oppia.android.app.model.MathUnaryOperation.Operator as UnaryOperator + +class ExpressionToComparableOperationListConverter private constructor() { + companion object { + private val COMPARABLE_OPERATION_COMPARATOR: Comparator by lazy { + // Some of the comparators must be deferred since they indirectly reference this comparator + // (which isn't valid until it's fully assembled). + Comparator.comparing(ComparableOperation::getComparisonTypeCase) + .thenComparing(ComparableOperation::getIsNegated) + .thenComparing(ComparableOperation::getIsInverted) + .thenSelectAmong( + ComparableOperation::getComparisonTypeCase, + ComparableOperation.ComparisonTypeCase.COMMUTATIVE_ACCUMULATION to comparingDeferred( + ComparableOperation::getCommutativeAccumulation + ) { COMMUTATIVE_ACCUMULATION_COMPARATOR }, + NON_COMMUTATIVE_OPERATION to comparingDeferred( + ComparableOperation::getNonCommutativeOperation + ) { NON_COMMUTATIVE_OPERATION_COMPARATOR }, + CONSTANT_TERM to Comparator.comparing( + ComparableOperation::getConstantTerm, REAL_COMPARATOR + ), + VARIABLE_TERM to Comparator.comparing(ComparableOperation::getVariableTerm) + ) + } + + private val COMMUTATIVE_ACCUMULATION_COMPARATOR: Comparator by lazy { + Comparator.comparing(CommutativeAccumulation::getAccumulationType) + .thenComparing( + { accumulation -> + accumulation.combinedOperationsList.toSortedSet(COMPARABLE_OPERATION_COMPARATOR) + }, + COMPARABLE_OPERATION_COMPARATOR.toSetComparator() + ) + } + + private val NON_COMMUTATIVE_BINARY_OPERATION_COMPARATOR by lazy { + Comparator.comparing( + NonCommutativeOperation.BinaryOperation::getLeftOperand, COMPARABLE_OPERATION_COMPARATOR + ).thenComparing( + NonCommutativeOperation.BinaryOperation::getRightOperand, COMPARABLE_OPERATION_COMPARATOR + ) + } + + private val NON_COMMUTATIVE_OPERATION_COMPARATOR: Comparator by lazy { + Comparator.comparing(NonCommutativeOperation::getOperationTypeCase) + .thenSelectAmong( + NonCommutativeOperation::getOperationTypeCase, + OperationTypeCase.EXPONENTIATION to Comparator.comparing( + NonCommutativeOperation::getExponentiation, NON_COMMUTATIVE_BINARY_OPERATION_COMPARATOR + ), + OperationTypeCase.SQUARE_ROOT to Comparator.comparing( + NonCommutativeOperation::getSquareRoot, COMPARABLE_OPERATION_COMPARATOR + ), + ) + } + + fun MathExpression.toComparable(): ComparableOperationList { + return ComparableOperationList.newBuilder().apply { + rootOperation = toComparableOperation().stabilizeNegation().sort() + }.build() + } + + private fun MathExpression.toComparableOperation(): ComparableOperation { + return when (expressionTypeCase) { + CONSTANT -> ComparableOperation.newBuilder().apply { + constantTerm = constant + }.build() + VARIABLE -> ComparableOperation.newBuilder().apply { + variableTerm = variable + }.build() + BINARY_OPERATION -> when (binaryOperation.operator) { + ADD -> toSummation(isRhsNegative = false) + SUBTRACT -> toSummation(isRhsNegative = true) + MULTIPLY -> toProduct(isRhsInverted = false) + DIVIDE -> toProduct(isRhsInverted = true) + EXPONENTIATE -> + toNonCommutativeOperation(NonCommutativeOperation.Builder::setExponentiation) + BinaryOperator.OPERATOR_UNSPECIFIED, BinaryOperator.UNRECOGNIZED, null -> + ComparableOperation.getDefaultInstance() + } + UNARY_OPERATION -> when (unaryOperation.operator) { + NEGATE -> unaryOperation.operand.toComparableOperation().makeNegative() + POSITIVE -> unaryOperation.operand.toComparableOperation() + UnaryOperator.OPERATOR_UNSPECIFIED, UnaryOperator.UNRECOGNIZED, null -> + ComparableOperation.getDefaultInstance() + } + FUNCTION_CALL -> when (functionCall.functionType) { + SQUARE_ROOT -> ComparableOperation.newBuilder().apply { + nonCommutativeOperation = NonCommutativeOperation.newBuilder().apply { + squareRoot = functionCall.argument.toComparableOperation() + }.build() + }.build() + FunctionType.FUNCTION_UNSPECIFIED, FunctionType.UNRECOGNIZED, null -> + ComparableOperation.getDefaultInstance() + } + GROUP -> group.toComparableOperation() + EXPRESSIONTYPE_NOT_SET, null -> ComparableOperation.getDefaultInstance() + } + } + + private fun MathExpression.toSummation(isRhsNegative: Boolean): ComparableOperation { + return ComparableOperation.newBuilder().apply { + commutativeAccumulation = CommutativeAccumulation.newBuilder().apply { + accumulationType = SUMMATION + addOperationToSum(binaryOperation.leftOperand, forceNegative = false) + addOperationToSum(binaryOperation.rightOperand, forceNegative = isRhsNegative) + }.build() + }.build() + } + + private fun MathExpression.toProduct(isRhsInverted: Boolean): ComparableOperation { + return ComparableOperation.newBuilder().apply { + commutativeAccumulation = CommutativeAccumulation.newBuilder().apply { + accumulationType = PRODUCT + addOperationToProduct(binaryOperation.leftOperand, forceInverse = false) + addOperationToProduct(binaryOperation.rightOperand, forceInverse = isRhsInverted) + }.build() + }.build() + } + + private fun CommutativeAccumulation.Builder.addOperationToSum( + expression: MathExpression, + forceNegative: Boolean + ) { + when (expression.binaryOperation.operator) { + ADD -> { + // If the whole operation is negative, carry it to the left-hand side of the operation. + addOperationToSum(expression.binaryOperation.leftOperand, forceNegative) + addOperationToSum(expression.binaryOperation.rightOperand, forceNegative = false) + } + SUBTRACT -> { + addOperationToSum(expression.binaryOperation.leftOperand, forceNegative) + addOperationToSum(expression.binaryOperation.rightOperand, forceNegative = true) + } + else -> if (forceNegative) { + addCombinedOperations(expression.toComparableOperation().makeNegative()) + } else addCombinedOperations(expression.toComparableOperation()) + } + } + + private fun CommutativeAccumulation.Builder.addOperationToProduct( + expression: MathExpression, + forceInverse: Boolean + ) { + when (expression.binaryOperation.operator) { + MULTIPLY -> { + // If the whole operation is inverted, carry it to the left-hand side of the operation. + addOperationToProduct(expression.binaryOperation.leftOperand, forceInverse) + addOperationToProduct(expression.binaryOperation.rightOperand, forceInverse = false) + } + DIVIDE -> { + addOperationToProduct(expression.binaryOperation.leftOperand, forceInverse) + addOperationToProduct(expression.binaryOperation.rightOperand, forceInverse = true) + } + else -> if (forceInverse) { + addCombinedOperations(expression.toComparableOperation().makeInverted()) + } else addCombinedOperations(expression.toComparableOperation()) + } + } + + private fun MathExpression.toNonCommutativeOperation( + setOperation: NonCommutativeOperation.Builder.( + NonCommutativeOperation.BinaryOperation + ) -> NonCommutativeOperation.Builder + ): ComparableOperation { + return ComparableOperation.newBuilder().apply { + nonCommutativeOperation = NonCommutativeOperation.newBuilder().apply { + setOperation( + NonCommutativeOperation.BinaryOperation.newBuilder().apply { + leftOperand = binaryOperation.leftOperand.toComparableOperation() + rightOperand = binaryOperation.rightOperand.toComparableOperation() + }.build() + ) + }.build() + }.build() + } + + private fun ComparableOperation.makePositive(): ComparableOperation = + toBuilder().apply { isNegated = false }.build() + + private fun ComparableOperation.makeNegative(): ComparableOperation = + toBuilder().apply { isNegated = true }.build() + + private fun ComparableOperation.makeInverted(): ComparableOperation = + toBuilder().apply { isInverted = true }.build() + + private fun ComparableOperation.stabilizeNegation(): ComparableOperation { + return when (comparisonTypeCase) { + ComparableOperation.ComparisonTypeCase.COMMUTATIVE_ACCUMULATION -> { + val stabilizedOperations = + commutativeAccumulation.combinedOperationsList.map { it.stabilizeNegation() } + when (commutativeAccumulation.accumulationType) { + SUMMATION -> toBuilder().apply { + commutativeAccumulation = commutativeAccumulation.toBuilder().apply { + clearCombinedOperations() + addAllCombinedOperations(stabilizedOperations) + }.build() + }.build() + PRODUCT -> { + // Negations can be combined for all constituent operations & brought up to the + // top-level operation. + val negativeCount = stabilizedOperations.count { + it.isNegated + } + if (isNegated) 1 else 0 + val positiveOperations = stabilizedOperations.map { it.makePositive() } + toBuilder().apply { + isNegated = (negativeCount % 2) == 1 + commutativeAccumulation = commutativeAccumulation.toBuilder().apply { + clearCombinedOperations() + addAllCombinedOperations(positiveOperations) + }.build() + }.build() + } + ACCUMULATION_TYPE_UNSPECIFIED, AccumulationType.UNRECOGNIZED, null -> this + } + } + NON_COMMUTATIVE_OPERATION -> toBuilder().apply { + // Negation can't be extracted from commutative operations. + nonCommutativeOperation = when (nonCommutativeOperation.operationTypeCase) { + OperationTypeCase.EXPONENTIATION -> nonCommutativeOperation.toBuilder().apply { + exponentiation = nonCommutativeOperation.exponentiation.toBuilder().apply { + leftOperand = nonCommutativeOperation.exponentiation.leftOperand.stabilizeNegation() + rightOperand = + nonCommutativeOperation.exponentiation.rightOperand.stabilizeNegation() + }.build() + }.build() + OperationTypeCase.SQUARE_ROOT -> nonCommutativeOperation.toBuilder().apply { + squareRoot = nonCommutativeOperation.squareRoot.stabilizeNegation() + }.build() + OperationTypeCase.OPERATIONTYPE_NOT_SET, null -> nonCommutativeOperation + } + }.build() + CONSTANT_TERM -> this + VARIABLE_TERM -> this + COMPARISONTYPE_NOT_SET, null -> this + } + } + + private fun ComparableOperation.sort(): ComparableOperation { + return when (comparisonTypeCase) { + ComparableOperation.ComparisonTypeCase.COMMUTATIVE_ACCUMULATION -> toBuilder().apply { + commutativeAccumulation = commutativeAccumulation.toBuilder().apply { + clearCombinedOperations() + // Sort the operations themselves before sorting them relative to each other. + val innerSortedList = commutativeAccumulation.combinedOperationsList.map { it.sort() } + addAllCombinedOperations(innerSortedList.sortedWith(COMPARABLE_OPERATION_COMPARATOR)) + }.build() + }.build() + NON_COMMUTATIVE_OPERATION -> toBuilder().apply { + nonCommutativeOperation = when (nonCommutativeOperation.operationTypeCase) { + OperationTypeCase.EXPONENTIATION -> nonCommutativeOperation.toBuilder().apply { + exponentiation = nonCommutativeOperation.exponentiation.toBuilder().apply { + leftOperand = nonCommutativeOperation.exponentiation.leftOperand.sort() + rightOperand = nonCommutativeOperation.exponentiation.rightOperand.sort() + }.build() + }.build() + OperationTypeCase.SQUARE_ROOT -> nonCommutativeOperation.toBuilder().apply { + squareRoot = nonCommutativeOperation.squareRoot.sort() + }.build() + OperationTypeCase.OPERATIONTYPE_NOT_SET, null -> nonCommutativeOperation + } + }.build() + CONSTANT_TERM, VARIABLE_TERM, COMPARISONTYPE_NOT_SET, null -> this + } + } + } +} diff --git a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt index 59b203a0d2d..70fa465e30a 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt @@ -1,8 +1,17 @@ package org.oppia.android.util.math +import org.oppia.android.app.model.ComparableOperationList import org.oppia.android.app.model.MathEquation import org.oppia.android.app.model.MathExpression +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.BINARY_OPERATION +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.CONSTANT +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.EXPRESSIONTYPE_NOT_SET +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.FUNCTION_CALL +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.GROUP +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.UNARY_OPERATION +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.VARIABLE import org.oppia.android.app.model.Real +import org.oppia.android.util.math.ExpressionToComparableOperationListConverter.Companion.toComparable import org.oppia.android.util.math.ExpressionToLatexConverter.Companion.convertToLatex import org.oppia.android.util.math.NumericExpressionEvaluator.Companion.evaluate @@ -11,3 +20,29 @@ fun MathEquation.toRawLatex(divAsFraction: Boolean): String = convertToLatex(div fun MathExpression.toRawLatex(divAsFraction: Boolean): String = convertToLatex(divAsFraction) fun MathExpression.evaluateAsNumericExpression(): Real? = evaluate() + +fun MathExpression.toComparableOperationList(): ComparableOperationList = + stripGroups().toComparable() + +private fun MathExpression.stripGroups(): MathExpression { + return when (expressionTypeCase) { + BINARY_OPERATION -> toBuilder().apply { + binaryOperation = binaryOperation.toBuilder().apply { + leftOperand = binaryOperation.leftOperand.stripGroups() + rightOperand = binaryOperation.rightOperand.stripGroups() + }.build() + }.build() + UNARY_OPERATION -> toBuilder().apply { + unaryOperation = unaryOperation.toBuilder().apply { + operand = unaryOperation.operand.stripGroups() + }.build() + }.build() + FUNCTION_CALL -> toBuilder().apply { + functionCall = functionCall.toBuilder().apply { + argument = functionCall.argument.stripGroups() + }.build() + }.build() + GROUP -> group.stripGroups() + CONSTANT, VARIABLE, EXPRESSIONTYPE_NOT_SET, null -> this + } +} diff --git a/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt index 83605b05808..cbe17f0dc92 100644 --- a/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt @@ -8,6 +8,8 @@ import org.oppia.android.app.model.Real.RealTypeCase.RATIONAL import org.oppia.android.app.model.Real.RealTypeCase.REALTYPE_NOT_SET import kotlin.math.pow +val REAL_COMPARATOR: Comparator by lazy { Comparator.comparing(Real::toDouble) } + fun Real.isRational(): Boolean = realTypeCase == RATIONAL fun Real.isInteger(): Boolean = realTypeCase == INTEGER diff --git a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel index 69d9dab509a..fcce3805364 100644 --- a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel @@ -42,6 +42,25 @@ oppia_android_test( ], ) +oppia_android_test( + name = "ExpressionToComparableOperationListTest", + srcs = ["ExpressionToComparableOperationListTest.kt"], + custom_package = "org.oppia.android.util.math", + test_class = "org.oppia.android.util.math.ExpressionToComparableOperationListTest", + test_manifest = "//utility:test_manifest", + deps = [ + "//model/src/main/proto:math_java_proto_lite", + "//testing/src/main/java/org/oppia/android/testing/math:comparable_operation_list_subject", + "//third_party:androidx_test_ext_junit", + "//third_party:com_google_truth_truth", + "//third_party:junit_junit", + "//third_party:org_robolectric_robolectric", + "//third_party:robolectric_android-all", + "//utility/src/main/java/org/oppia/android/util/math:extensions", + "//utility/src/main/java/org/oppia/android/util/math:parser", + ], +) + oppia_android_test( name = "ExpressionToLatexTest", srcs = ["ExpressionToLatexTest.kt"], diff --git a/utility/src/test/java/org/oppia/android/util/math/ExpressionToComparableOperationListTest.kt b/utility/src/test/java/org/oppia/android/util/math/ExpressionToComparableOperationListTest.kt new file mode 100644 index 00000000000..d9599c0b32d --- /dev/null +++ b/utility/src/test/java/org/oppia/android/util/math/ExpressionToComparableOperationListTest.kt @@ -0,0 +1,1637 @@ +package org.oppia.android.util.math + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Test +import org.junit.runner.RunWith +import org.oppia.android.app.model.ComparableOperationList.CommutativeAccumulation.AccumulationType.PRODUCT +import org.oppia.android.app.model.ComparableOperationList.CommutativeAccumulation.AccumulationType.SUMMATION +import org.oppia.android.app.model.MathExpression +import org.oppia.android.testing.math.ComparableOperationListSubject.Companion.assertThat +import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode +import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult +import org.robolectric.annotation.LooperMode + +/** Tests for [MathExpressionParser]. */ +// SameParameterValue: tests should have specific context included/excluded for readability. +@Suppress("SameParameterValue") +@RunWith(AndroidJUnit4::class) +@LooperMode(LooperMode.Mode.PAUSED) +class ExpressionToComparableOperationListTest { + // TODO: add high-level checks for the three types, but don't test in detail since there are + // separate suites. Also, document the separate suites' existence in this suites's KDoc. + + @Test + fun testToComparableOperation() { + // TODO: split up & move to separate test suites. Finish test cases (if anymore are needed). + + val exp1 = parseNumericExpressionSuccessfullyWithAllErrors("1") + assertThat(exp1.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + + val exp2 = parseNumericExpressionSuccessfullyWithAllErrors("-1") + assertThat(exp2.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + + val exp3 = parseNumericExpressionSuccessfullyWithAllErrors("1+3+4") + assertThat(exp3.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + + val exp4 = parseNumericExpressionSuccessfullyWithAllErrors("-1-2-3") + assertThat(exp4.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(1) { + hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(2) { + hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + + val exp5 = parseNumericExpressionSuccessfullyWithAllErrors("1+2-3") + assertThat(exp5.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(2) { + hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + + val exp6 = parseNumericExpressionSuccessfullyWithAllErrors("2*3*4") + assertThat(exp6.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + + val exp7 = parseNumericExpressionSuccessfullyWithAllErrors("1-2*3") + assertThat(exp7.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + } + } + + val exp8 = parseNumericExpressionSuccessfullyWithAllErrors("2*3-4") + assertThat(exp8.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + index(1) { + hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + + val exp9 = parseNumericExpressionSuccessfullyWithAllErrors("1+2*3-4+8*7*6-9") + assertThat(exp9.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(5) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(6) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(7) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(8) + } + } + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(3) { + hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + index(4) { + hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(9) + } + } + } + } + + val exp10 = parseNumericExpressionSuccessfullyWithAllErrors("2/3/4") + assertThat(exp10.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + + val exp11 = parseNumericExpressionSuccessfullyWithoutOptionalErrors("2^3^4") + assertThat(exp11.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + nonCommutativeOperation { + exponentiation { + leftOperand { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + nonCommutativeOperation { + exponentiation { + leftOperand { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + } + } + + val exp12 = parseNumericExpressionSuccessfullyWithAllErrors("1+2/3+3") + assertThat(exp12.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + + val exp13 = parseNumericExpressionSuccessfullyWithAllErrors("1+(2/3)+3") + assertThat(exp13.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + + val exp14 = parseNumericExpressionSuccessfullyWithAllErrors("1+2^3+3") + assertThat(exp14.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + nonCommutativeOperation { + exponentiation { + leftOperand { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + + val exp15 = parseNumericExpressionSuccessfullyWithAllErrors("1+(2^3)+3") + assertThat(exp15.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + nonCommutativeOperation { + exponentiation { + leftOperand { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + + val exp16 = parseNumericExpressionSuccessfullyWithAllErrors("2*3/4*7") + assertThat(exp16.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(4) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(7) + } + } + index(3) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + + val exp17 = parseNumericExpressionSuccessfullyWithAllErrors("2*(3/4)*7") + assertThat(exp17.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(4) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(7) + } + } + index(3) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + + val exp18 = parseNumericExpressionSuccessfullyWithAllErrors("-3*sqrt(2)") + assertThat(exp18.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + nonCommutativeOperation { + squareRootWithArgument { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + + val exp19 = parseNumericExpressionSuccessfullyWithAllErrors("1+(2+(3+(4+5)))") + assertThat(exp19.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(5) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + index(3) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + index(4) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + } + } + + val exp20 = parseNumericExpressionSuccessfullyWithAllErrors("2*(3*(4*(5*6)))") + assertThat(exp20.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(5) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + index(3) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + index(4) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(6) + } + } + } + } + + val exp21 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x") + assertThat(exp21.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + + val exp22 = parseAlgebraicExpressionSuccessfullyWithAllErrors("-x") + assertThat(exp22.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + + val exp23 = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+x+y") + assertThat(exp23.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("y") + } + } + } + } + + val exp24 = parseAlgebraicExpressionSuccessfullyWithAllErrors("-1-x-y") + assertThat(exp24.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(1) { + hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + index(2) { + hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("y") + } + } + } + } + + val exp25 = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+x-y") + assertThat(exp25.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + index(2) { + hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("y") + } + } + } + } + + val exp26 = parseAlgebraicExpressionSuccessfullyWithAllErrors("2xy") + assertThat(exp26.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("y") + } + } + } + } + + val exp27 = parseAlgebraicExpressionSuccessfullyWithAllErrors("1-xy") + assertThat(exp27.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("y") + } + } + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + } + } + + val exp28 = parseAlgebraicExpressionSuccessfullyWithAllErrors("xy-4") + assertThat(exp28.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("y") + } + } + } + } + index(1) { + hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + + val exp29 = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+xy-4+yz-9") + assertThat(exp29.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(5) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("y") + } + } + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("y") + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("z") + } + } + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(3) { + hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + index(4) { + hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(9) + } + } + } + } + + val exp30 = parseAlgebraicExpressionSuccessfullyWithAllErrors("2/x/y") + assertThat(exp30.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isTrue() + variableTerm { + withNameThat().isEqualTo("x") + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isTrue() + variableTerm { + withNameThat().isEqualTo("y") + } + } + } + } + + val exp31 = parseAlgebraicExpressionSuccessfullyWithoutOptionalErrors("x^3^4") + assertThat(exp31.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + nonCommutativeOperation { + exponentiation { + leftOperand { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + rightOperand { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + nonCommutativeOperation { + exponentiation { + leftOperand { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + } + } + + val exp32 = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+x/y+z") + assertThat(exp32.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isTrue() + variableTerm { + withNameThat().isEqualTo("y") + } + } + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("z") + } + } + } + } + + val exp33 = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+(x/y)+z") + assertThat(exp33.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isTrue() + variableTerm { + withNameThat().isEqualTo("y") + } + } + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("z") + } + } + } + } + + val exp34 = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+x^3+y") + assertThat(exp34.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + nonCommutativeOperation { + exponentiation { + leftOperand { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + rightOperand { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("y") + } + } + } + } + + val exp35 = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+(x^3)+y") + assertThat(exp35.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + nonCommutativeOperation { + exponentiation { + leftOperand { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + rightOperand { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("y") + } + } + } + } + + val exp36 = parseAlgebraicExpressionSuccessfullyWithAllErrors("2*x/y*z") + assertThat(exp36.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(4) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("z") + } + } + index(3) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isTrue() + variableTerm { + withNameThat().isEqualTo("y") + } + } + } + } + + val exp37 = parseAlgebraicExpressionSuccessfullyWithAllErrors("2*(x/y)*z") + assertThat(exp37.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(4) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("z") + } + } + index(3) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isTrue() + variableTerm { + withNameThat().isEqualTo("y") + } + } + } + } + + val exp38 = parseAlgebraicExpressionSuccessfullyWithAllErrors("-2*sqrt(x)") + assertThat(exp38.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + nonCommutativeOperation { + squareRootWithArgument { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + + val exp39 = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+(x+(3+(z+y)))") + assertThat(exp39.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(5) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + index(3) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("y") + } + } + index(4) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("z") + } + } + } + } + + val exp40 = parseAlgebraicExpressionSuccessfullyWithAllErrors("2*(x*(4*(zy)))") + assertThat(exp40.toComparableOperationList()).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(5) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + index(3) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("y") + } + } + index(4) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("z") + } + } + } + } + + // Equality tests: + val list1 = createComparableOperationListFromNumericExpression("(1+2)+3") + val list2 = createComparableOperationListFromNumericExpression("1+(2+3)") + assertThat(list1).isEqualTo(list2) + + val list3 = createComparableOperationListFromNumericExpression("1+2+3") + val list4 = createComparableOperationListFromNumericExpression("3+2+1") + assertThat(list3).isEqualTo(list4) + + val list5 = createComparableOperationListFromNumericExpression("1-2-3") + val list6 = createComparableOperationListFromNumericExpression("-3 + -2 + 1") + assertThat(list5).isEqualTo(list6) + + val list7 = createComparableOperationListFromNumericExpression("1-2-3") + val list8 = createComparableOperationListFromNumericExpression("-3-2+1") + assertThat(list7).isEqualTo(list8) + + val list9 = createComparableOperationListFromNumericExpression("1-2-3") + val list10 = createComparableOperationListFromNumericExpression("-3-2+1") + assertThat(list9).isEqualTo(list10) + + val list11 = createComparableOperationListFromNumericExpression("1-2-3") + val list12 = createComparableOperationListFromNumericExpression("3-2-1") + assertThat(list11).isNotEqualTo(list12) + + val list13 = createComparableOperationListFromNumericExpression("2*3*4") + val list14 = createComparableOperationListFromNumericExpression("4*3*2") + assertThat(list13).isEqualTo(list14) + + val list15 = createComparableOperationListFromNumericExpression("2*(3/4)") + val list16 = createComparableOperationListFromNumericExpression("3/4*2") + assertThat(list15).isEqualTo(list16) + + val list17 = createComparableOperationListFromNumericExpression("2*3/4") + val list18 = createComparableOperationListFromNumericExpression("3/4*2") + assertThat(list17).isEqualTo(list18) + + val list45 = createComparableOperationListFromNumericExpression("2*3/4") + val list46 = createComparableOperationListFromNumericExpression("2*3*4") + assertThat(list45).isNotEqualTo(list46) + + val list19 = createComparableOperationListFromNumericExpression("2*3/4") + val list20 = createComparableOperationListFromNumericExpression("2*4/3") + assertThat(list19).isNotEqualTo(list20) + + val list21 = createComparableOperationListFromNumericExpression("2*3/4*7") + val list22 = createComparableOperationListFromNumericExpression("3/4*7*2") + assertThat(list21).isEqualTo(list22) + + val list23 = createComparableOperationListFromNumericExpression("2*3/4*7") + val list24 = createComparableOperationListFromNumericExpression("7*(3*2/4)") + assertThat(list23).isEqualTo(list24) + + val list25 = createComparableOperationListFromNumericExpression("2*3/4*7") + val list26 = createComparableOperationListFromNumericExpression("7*3*2/4") + assertThat(list25).isEqualTo(list26) + + val list27 = createComparableOperationListFromNumericExpression("-2*3") + val list28 = createComparableOperationListFromNumericExpression("3*-2") + assertThat(list27).isEqualTo(list28) + + val list29 = createComparableOperationListFromNumericExpression("2^3") + val list30 = createComparableOperationListFromNumericExpression("3^2") + assertThat(list29).isNotEqualTo(list30) + + val list31 = createComparableOperationListFromNumericExpression("-(1+2)") + val list32 = createComparableOperationListFromNumericExpression("-1+2") + assertThat(list31).isNotEqualTo(list32) + + val list33 = createComparableOperationListFromNumericExpression("-(1+2)") + val list34 = createComparableOperationListFromNumericExpression("-1-2") + assertThat(list33).isNotEqualTo(list34) + + val list35 = createComparableOperationListFromAlgebraicExpression("x(x+1)") + val list36 = createComparableOperationListFromAlgebraicExpression("(1+x)x") + assertThat(list35).isEqualTo(list36) + + val list37 = createComparableOperationListFromAlgebraicExpression("x(x+1)") + val list38 = createComparableOperationListFromAlgebraicExpression("x^2+x") + assertThat(list37).isNotEqualTo(list38) + + val list39 = createComparableOperationListFromAlgebraicExpression("x^2*sqrt(x)") + val list40 = createComparableOperationListFromAlgebraicExpression("x") + assertThat(list39).isNotEqualTo(list40) + + val list41 = createComparableOperationListFromAlgebraicExpression("xyz") + val list42 = createComparableOperationListFromAlgebraicExpression("zyx") + assertThat(list41).isEqualTo(list42) + + val list43 = createComparableOperationListFromAlgebraicExpression("1+xy-2") + val list44 = createComparableOperationListFromAlgebraicExpression("-2+1+yx") + assertThat(list43).isEqualTo(list44) + + // TODO: add tests for comparator/sorting & negation simplification? + } + + private fun createComparableOperationListFromNumericExpression(expression: String) = + parseNumericExpressionSuccessfullyWithAllErrors(expression).toComparableOperationList() + + private fun createComparableOperationListFromAlgebraicExpression(expression: String) = + parseAlgebraicExpressionSuccessfullyWithAllErrors(expression).toComparableOperationList() + + private companion object { + // TODO: fix helper API. + + private fun parseNumericExpressionSuccessfullyWithAllErrors( + expression: String + ): MathExpression { + val result = parseNumericExpressionInternal(expression, ErrorCheckingMode.ALL_ERRORS) + return (result as MathParsingResult.Success).result + } + + private fun parseNumericExpressionSuccessfullyWithoutOptionalErrors( + expression: String + ): MathExpression { + val result = + parseNumericExpressionInternal( + expression, ErrorCheckingMode.REQUIRED_ONLY + ) + return (result as MathParsingResult.Success).result + } + + private fun parseNumericExpressionInternal( + expression: String, + errorCheckingMode: ErrorCheckingMode + ): MathParsingResult { + return MathExpressionParser.parseNumericExpression(expression, errorCheckingMode) + } + + private fun parseAlgebraicExpressionSuccessfullyWithAllErrors( + expression: String, + allowedVariables: List = listOf("x", "y", "z") + ): MathExpression { + val result = + parseAlgebraicExpressionInternal(expression, ErrorCheckingMode.ALL_ERRORS, allowedVariables) + return (result as MathParsingResult.Success).result + } + + private fun parseAlgebraicExpressionSuccessfullyWithoutOptionalErrors( + expression: String, + allowedVariables: List = listOf("x", "y", "z") + ): MathExpression { + val result = + parseAlgebraicExpressionInternal( + expression, ErrorCheckingMode.REQUIRED_ONLY, allowedVariables + ) + return (result as MathParsingResult.Success).result + } + + private fun parseAlgebraicExpressionInternal( + expression: String, + errorCheckingMode: ErrorCheckingMode, + allowedVariables: List = listOf("x", "y", "z") + ): MathParsingResult { + return MathExpressionParser.parseAlgebraicExpression( + expression, allowedVariables, errorCheckingMode + ) + } + } +} From 917c9093c052df08eaa838e109c588c0d277f319 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 15 Dec 2021 14:41:11 -0800 Subject: [PATCH 016/162] Add support for expression->polynomial conversion. This is mostly copied from #2173. --- .../org/oppia/android/util/math/BUILD.bazel | 14 + .../math/ExpressionToPolynomialConverter.kt | 110 ++ .../util/math/MathExpressionExtensions.kt | 4 + .../android/util/math/PolynomialExtensions.kt | 307 ++++++ .../oppia/android/util/math/RealExtensions.kt | 44 +- .../org/oppia/android/util/math/BUILD.bazel | 18 + .../util/math/ExpressionToPolynomialTest.kt | 945 ++++++++++++++++++ 7 files changed, 1438 insertions(+), 4 deletions(-) create mode 100644 utility/src/main/java/org/oppia/android/util/math/ExpressionToPolynomialConverter.kt create mode 100644 utility/src/test/java/org/oppia/android/util/math/ExpressionToPolynomialTest.kt diff --git a/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel b/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel index fbd04084430..3bdacb733c2 100644 --- a/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel @@ -106,6 +106,7 @@ kt_android_library( deps = [ ":expression_to_comparable_operation_list_converter", ":expression_to_latex_converter", + ":expression_to_polynomial_converter", ":numeric_expression_evaluator", "//model/src/main/proto:math_java_proto_lite", ], @@ -117,6 +118,7 @@ kt_android_library( "PolynomialExtensions.kt", ], deps = [ + ":comparator_extensions", ":real_extensions", "//model/src/main/proto:math_java_proto_lite", ], @@ -168,6 +170,18 @@ kt_android_library( ], ) +kt_android_library( + name = "expression_to_polynomial_converter", + srcs = [ + "ExpressionToPolynomialConverter.kt", + ], + deps = [ + ":polynomial_extensions", + ":real_extensions", + "//model/src/main/proto:math_java_proto_lite", + ], +) + kt_android_library( name = "numeric_expression_evaluator", srcs = [ diff --git a/utility/src/main/java/org/oppia/android/util/math/ExpressionToPolynomialConverter.kt b/utility/src/main/java/org/oppia/android/util/math/ExpressionToPolynomialConverter.kt new file mode 100644 index 00000000000..d16ac87fce1 --- /dev/null +++ b/utility/src/main/java/org/oppia/android/util/math/ExpressionToPolynomialConverter.kt @@ -0,0 +1,110 @@ +package org.oppia.android.util.math + +import org.oppia.android.app.model.MathBinaryOperation +import org.oppia.android.app.model.MathBinaryOperation.Operator.ADD +import org.oppia.android.app.model.MathBinaryOperation.Operator.DIVIDE +import org.oppia.android.app.model.MathBinaryOperation.Operator.EXPONENTIATE +import org.oppia.android.app.model.MathBinaryOperation.Operator.MULTIPLY +import org.oppia.android.app.model.MathBinaryOperation.Operator.SUBTRACT +import org.oppia.android.app.model.MathExpression +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.BINARY_OPERATION +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.CONSTANT +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.EXPRESSIONTYPE_NOT_SET +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.FUNCTION_CALL +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.GROUP +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.UNARY_OPERATION +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.VARIABLE +import org.oppia.android.app.model.MathFunctionCall.FunctionType +import org.oppia.android.app.model.MathFunctionCall.FunctionType.FUNCTION_UNSPECIFIED +import org.oppia.android.app.model.MathFunctionCall.FunctionType.SQUARE_ROOT +import org.oppia.android.app.model.MathUnaryOperation +import org.oppia.android.app.model.MathUnaryOperation.Operator.NEGATE +import org.oppia.android.app.model.MathUnaryOperation.Operator.POSITIVE +import org.oppia.android.app.model.Polynomial +import org.oppia.android.app.model.MathBinaryOperation.Operator as BinaryOperator +import org.oppia.android.app.model.MathUnaryOperation.Operator as UnaryOperator + +class ExpressionToPolynomialConverter private constructor() { + companion object { + fun MathExpression.reduceToPolynomial(): Polynomial? = + replaceSquareRoots().reduceToPolynomialAux()?.removeUnnecessaryVariables()?.sort() + + private fun MathExpression.replaceSquareRoots(): MathExpression { + return when (expressionTypeCase) { + BINARY_OPERATION -> toBuilder().apply { + binaryOperation = binaryOperation.toBuilder().apply { + leftOperand = binaryOperation.leftOperand.replaceSquareRoots() + rightOperand = binaryOperation.rightOperand.replaceSquareRoots() + }.build() + }.build() + UNARY_OPERATION -> toBuilder().apply { + unaryOperation = unaryOperation.toBuilder().apply { + operand = unaryOperation.operand.replaceSquareRoots() + }.build() + }.build() + FUNCTION_CALL -> when (functionCall.functionType) { + SQUARE_ROOT -> toBuilder().apply { + // Replace the square root function call with the equivalent exponentiation. That is, + // sqrt(x)=x^(1/2). + binaryOperation = MathBinaryOperation.newBuilder().apply { + operator = EXPONENTIATE + leftOperand = functionCall.argument.replaceSquareRoots() + rightOperand = MathExpression.newBuilder().apply { + constant = ONE_HALF + }.build() + }.build() + }.build() + FUNCTION_UNSPECIFIED, FunctionType.UNRECOGNIZED, null -> this + } + GROUP -> group.replaceSquareRoots() + CONSTANT, VARIABLE, EXPRESSIONTYPE_NOT_SET, null -> this + } + } + + private fun MathExpression.reduceToPolynomialAux(): Polynomial? { + return when (expressionTypeCase) { + CONSTANT -> createConstantPolynomial(constant) + VARIABLE -> createSingleVariablePolynomial(variable) + BINARY_OPERATION -> binaryOperation.reduceToPolynomial() + UNARY_OPERATION -> unaryOperation.reduceToPolynomial() + // Both functions & groups should be removed ahead of polynomial reduction. + FUNCTION_CALL, GROUP, EXPRESSIONTYPE_NOT_SET, null -> null + } + } + + private fun MathBinaryOperation.reduceToPolynomial(): Polynomial? { + val leftPolynomial = leftOperand.reduceToPolynomialAux() ?: return null + val rightPolynomial = rightOperand.reduceToPolynomialAux() ?: return null + return when (operator) { + ADD -> leftPolynomial + rightPolynomial + SUBTRACT -> leftPolynomial - rightPolynomial + MULTIPLY -> leftPolynomial * rightPolynomial + DIVIDE -> leftPolynomial / rightPolynomial + EXPONENTIATE -> leftPolynomial.pow(rightPolynomial) + BinaryOperator.OPERATOR_UNSPECIFIED, BinaryOperator.UNRECOGNIZED, null -> null + } + } + + private fun MathUnaryOperation.reduceToPolynomial(): Polynomial? { + return when (operator) { + NEGATE -> -(operand.reduceToPolynomialAux() ?: return null) + POSITIVE -> operand.reduceToPolynomialAux() // Positive unary changes nothing. + UnaryOperator.OPERATOR_UNSPECIFIED, UnaryOperator.UNRECOGNIZED, null -> null + } + } + + private fun createSingleVariablePolynomial(variableName: String): Polynomial { + return createSingleTermPolynomial( + Polynomial.Term.newBuilder().apply { + coefficient = ONE + addVariable( + Polynomial.Term.Variable.newBuilder().apply { + name = variableName + power = 1 + }.build() + ) + }.build() + ) + } + } +} diff --git a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt index 70fa465e30a..39e59ce99bb 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt @@ -10,9 +10,11 @@ import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.FUNCTION_CA import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.GROUP import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.UNARY_OPERATION import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.VARIABLE +import org.oppia.android.app.model.Polynomial import org.oppia.android.app.model.Real import org.oppia.android.util.math.ExpressionToComparableOperationListConverter.Companion.toComparable import org.oppia.android.util.math.ExpressionToLatexConverter.Companion.convertToLatex +import org.oppia.android.util.math.ExpressionToPolynomialConverter.Companion.reduceToPolynomial import org.oppia.android.util.math.NumericExpressionEvaluator.Companion.evaluate fun MathEquation.toRawLatex(divAsFraction: Boolean): String = convertToLatex(divAsFraction) @@ -24,6 +26,8 @@ fun MathExpression.evaluateAsNumericExpression(): Real? = evaluate() fun MathExpression.toComparableOperationList(): ComparableOperationList = stripGroups().toComparable() +fun MathExpression.toPolynomial(): Polynomial? = stripGroups().reduceToPolynomial() + private fun MathExpression.stripGroups(): MathExpression { return when (expressionTypeCase) { BINARY_OPERATION -> toBuilder().apply { diff --git a/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt index a4ba72213be..74b1187b6e0 100644 --- a/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt @@ -1,13 +1,43 @@ package org.oppia.android.util.math +import org.oppia.android.app.model.Fraction import org.oppia.android.app.model.Polynomial import org.oppia.android.app.model.Polynomial.Term import org.oppia.android.app.model.Polynomial.Term.Variable import org.oppia.android.app.model.Real +import java.util.SortedSet + +private val POLYNOMIAL_VARIABLE_COMPARATOR: Comparator by lazy { + // Note that power is reversed because larger powers should actually be sorted ahead of smaller + // powers for the same variable name (but variable name still takes precedence). This ensures + // cases like x^2y+y^2x are sorted in that order. + Comparator.comparing(Variable::getName).thenComparingReversed(Variable::getPower) +} + +private val POLYNOMIAL_TERM_COMPARATOR: Comparator by lazy { + // First, sort by all variable names to ensure xy is placed ahead of xz. Then, sort by variable + // powers in order of the variables (such that x^2y is ranked higher thank xy). Finally, sort by + // the coefficient to ensure equality through the comparator works correctly (though in practice + // like terms should always be combined). Note the specific reversing happening here. It's done in + // this way so that sorted set bigger/smaller list is reversed (which matches expectations since + // larger terms should appear earlier in the results). This is implementing an ordering similar to + // https://en.wikipedia.org/wiki/Polynomial#Definition, except for multi-variable functions (where + // variables of higher degree are preferred over lower degree by lexicographical order of variable + // names). + Comparator.comparing>( + { term -> term.variableList.toSortedSet(POLYNOMIAL_VARIABLE_COMPARATOR) }, + POLYNOMIAL_VARIABLE_COMPARATOR.reversed().toSetComparator() + ).reversed().thenComparing(Term::getCoefficient, REAL_COMPARATOR) +} /** Returns whether this polynomial is a constant-only polynomial (contains no variables). */ fun Polynomial.isConstant(): Boolean = termCount == 1 && getTerm(0).variableCount == 0 +fun Polynomial.isSingleTerm(): Boolean = termList.size == 1 + +fun Polynomial.isApproximatelyZero(): Boolean = + termList.all { it.coefficient.isApproximatelyZero() } // Zero polynomials only have 0 coefs. + /** * Returns the first term coefficient from this polynomial. This corresponds to the whole value of * the polynomial iff isConstant() returns true, otherwise this value isn't useful. @@ -17,6 +47,10 @@ fun Polynomial.isConstant(): Boolean = termCount == 1 && getTerm(0).variableCoun */ fun Polynomial.getConstant(): Real = getTerm(0).coefficient +// Return the highest power to represent the degree of the polynomial. Reference: +// https://www.varsitytutors.com/algebra_1-help/how-to-find-the-degree-of-a-polynomial. +fun Polynomial.getDegree(): Int = getLeadingTerm().highestDegree() + fun Polynomial.toPlainText(): String { return termList.map { it.toPlainText() }.reduce { acc, termAnswerStr -> if (termAnswerStr.startsWith("-")) { @@ -49,3 +83,276 @@ private fun Term.toPlainText(): String { private fun Variable.toPlainText(): String { return if (power > 1) "$name^$power" else name } + +fun Polynomial.combineLikeTerms(): Polynomial { + // The following algorithm is expected to grow in space by O(N*M) and in time by O(N*m*log(m)) + // where N is the total number of terms, M is the total number of variables, and m is the largest + // single count of variables among all terms (this is assuming constant-time insertion for the + // underlying hashtable). + val newTerms = termList.groupBy { + it.variableList.sortedWith(POLYNOMIAL_VARIABLE_COMPARATOR) + }.mapValues { (_, coefficientTerms) -> + coefficientTerms.map { it.coefficient } + }.mapNotNull { (variables, coefficients) -> + // Combine like terms by summing their coefficients. + val newCoefficient = coefficients.reduce(Real::plus) + return@mapNotNull if (!newCoefficient.isApproximatelyZero()) { + Term.newBuilder().apply { + coefficient = newCoefficient + + // Remove variables with zero powers (since they evaluate to '1'). + addAllVariable(variables.filter { variable -> variable.power != 0 }) + }.build() + } else null // Zero terms should be removed. + } + return Polynomial.newBuilder().apply { + addAllTerm(newTerms) + }.build().ensureAtLeastConstant() +} + +fun Polynomial.removeUnnecessaryVariables(): Polynomial { + return Polynomial.newBuilder().apply { + addAllTerm( + this@removeUnnecessaryVariables.termList.filter { term -> + !term.coefficient.isApproximatelyZero() + } + ) + }.build().ensureAtLeastConstant() +} + +fun Polynomial.sort(): Polynomial = Polynomial.newBuilder().apply { + addAllTerm(this@sort.termList.sortedWith(POLYNOMIAL_TERM_COMPARATOR)) +}.build() + +operator fun Polynomial.unaryMinus(): Polynomial { + // Negating a polynomial just requires flipping the signs on all coefficients. + return toBuilder() + .clearTerm() + .addAllTerm(termList.map { it.toBuilder().setCoefficient(-it.coefficient).build() }) + .build() +} + +operator fun Polynomial.plus(rhs: Polynomial): Polynomial { + // Adding two polynomials just requires combining their terms lists (taking into account combining + // common terms). + return Polynomial.newBuilder().apply { + addAllTerm(this@plus.termList + rhs.termList) + }.build().combineLikeTerms().removeUnnecessaryVariables() +} + +operator fun Polynomial.minus(rhs: Polynomial): Polynomial { + // a - b = a + -b + return this + -rhs +} + +operator fun Polynomial.times(rhs: Polynomial): Polynomial { + // Polynomial multiplication is simply multiplying each term in one by each term in the other. + val crossMultipliedTerms = termList.flatMap { leftTerm -> + rhs.termList.map { rightTerm -> leftTerm * rightTerm } + } + + // Treat each multiplied term as a unique polynomial, then add them together (so that like terms + // can be properly combined). + return crossMultipliedTerms.map { createSingleTermPolynomial(it) }.reduce(Polynomial::plus) +} + +operator fun Polynomial.div(rhs: Polynomial): Polynomial? { + // See https://en.wikipedia.org/wiki/Polynomial_long_division#Pseudocode for reference. + if (rhs.isApproximatelyZero()) { + return null // Dividing by zero is invalid and thus cannot yield a polynomial. + } + + var quotient = createConstantPolynomial(ZERO) + var remainder = this + val leadingDivisorTerm = rhs.getLeadingTerm() + val divisorVariable = leadingDivisorTerm.highestDegreeVariable() + val divisorVariableName = divisorVariable?.name + val divisorDegree = leadingDivisorTerm.highestDegree() + while (!remainder.isApproximatelyZero() && remainder.getDegree() >= divisorDegree) { + // Attempt to divide the leading terms (this may fail). Note that the leading term should always + // be based on the divisor variable being used (otherwise subsequent division steps will be + // inconsistent and potentially fail to resolve). + val newTerm = + remainder.getLeadingTerm(matchedVariable = divisorVariableName) / leadingDivisorTerm + ?: return null + quotient += newTerm.toPolynomial() + remainder -= newTerm.toPolynomial() * rhs + } + return when { + remainder.isApproximatelyZero() -> quotient // Exact division (i.e. with no remainder). + remainder.isConstant() && rhs.isConstant() -> { + // Remainder is a constant term. + val remainingTerm = remainder.getConstant() / rhs.getConstant() + quotient + createConstantPolynomial(remainingTerm) + } + else -> null // Remainder is a polynomial, so the division failed. + } +} + +fun Polynomial.pow(exp: Polynomial): Polynomial? { + // Polynomial exponentiation is only supported if the right side is a constant polynomial, + // otherwise the result cannot be a polynomial (though could still be compared to another + // expression by utilizing sampling techniques). + return if (exp.isConstant()) pow(exp.getConstant()) else null +} + +fun createConstantPolynomial(constant: Real): Polynomial = + createSingleTermPolynomial(Term.newBuilder().setCoefficient(constant).build()) + +fun createSingleTermPolynomial(term: Term): Polynomial = + Polynomial.newBuilder().apply { addTerm(term) }.build() + +private fun Polynomial.pow(exp: Int): Polynomial { + // Anything raised to the power of 0 is 1. + if (exp == 0) return createConstantPolynomial(ONE) + if (exp == 1) return this + var newValue = this + for (i in 1 until exp) newValue *= this + return newValue +} + +private fun Polynomial.pow(rational: Fraction): Polynomial? { + // Polynomials with addition require factoring. + return if (isSingleTerm()) { + termList.first().pow(rational)?.toPolynomial() + } else null +} + +private fun Polynomial.pow(exp: Real): Polynomial? { + val shouldBeInverted = exp.isNegative() + val positivePower = if (shouldBeInverted) -exp else exp + val exponentiation = when { + // Constant polynomials can be raised by any constant. + isConstant() -> createConstantPolynomial(getConstant().pow(positivePower)) + + // Polynomials can only be raised to positive integers (or zero). + exp.isWholeNumber() -> exp.asWholeNumber()?.let { pow(it) } + + // Polynomials can potentially be raised by a fractional power. + exp.isRational() -> pow(exp.rational) + + // All other cases require factoring will definitely not compute to polynomials (such as + // irrational exponents). + else -> null + } + return if (shouldBeInverted) { + val onePolynomial = createConstantPolynomial(ONE) + // Note that this division is guaranteed to fail if the exponentiation result is a polynomial. + // Future implementations may leverage root-finding algorithms to factor for integer inverse + // powers (such as square root, cubic root, etc.). Non-integer inverse powers will require + // sampling. + exponentiation?.let { onePolynomial / it } + } else exponentiation +} + +private operator fun Term.times(rhs: Term): Term { + // The coefficients are always multiplied. + val combinedCoefficient = coefficient * rhs.coefficient + + // Next, create a combined list of new variables. + val combinedVariables = variableList + rhs.variableList + + // Simplify the variables by combining the exponents of like variables. Start with a map of 0 + // powers, then add in the powers of each variable and collect the final list of unique terms. + val variableNamesMap = mutableMapOf() + combinedVariables.forEach { + variableNamesMap.compute(it.name) { _, power -> + if (power != null) power + it.power else it.power + } + } + val newVariableList = variableNamesMap.map { (name, power) -> + Variable.newBuilder().setName(name).setPower(power).build() + } + + return Term.newBuilder() + .setCoefficient(combinedCoefficient) + .addAllVariable(newVariableList) + .build() +} + +private operator fun Term.div(rhs: Term): Term? { + val dividendPowerMap = variableList.toPowerMap() + val divisorPowerMap = rhs.variableList.toPowerMap() + + // If any variables are present in the divisor and not the dividend, this division won't work + // effectively. + if (!dividendPowerMap.keys.containsAll(divisorPowerMap.keys)) return null + + // Division is simply subtracting the powers of terms in the divisor from those in the dividend. + val quotientPowerMap = dividendPowerMap.mapValues { (name, power) -> + power - divisorPowerMap.getOrDefault(name, defaultValue = 0) + } + + // If there are any negative powers, the divisor can't effectively divide this value. + if (quotientPowerMap.values.any { it < 0 }) return null + + // Remove variables with powers of 0 since those have been fully divided. Also, divide the + // coefficients to finish the division. + return Term.newBuilder() + .setCoefficient(coefficient / rhs.coefficient) + .addAllVariable(quotientPowerMap.filter { (_, power) -> power > 0 }.toVariableList()) + .build() +} + +private fun Term.pow(rational: Fraction): Term? { + // Raising an exponent by an exponent just requires multiplying the two together. + val newVariablePowers = variableList.map { variable -> + variable.power.toWholeNumberFraction() * rational + } + + // If any powers are not whole numbers then the rational is likely representing a root and the + // term in question is not rootable to that degree. + if (newVariablePowers.any { !it.isOnlyWholeNumber() }) return null + + return Term.newBuilder().apply { + coefficient = this@pow.coefficient + addAllVariable( + this@pow.variableList.zip(newVariablePowers).map { (variable, newPower) -> + variable.toBuilder().apply { + power = newPower.toWholeNumber() + }.build() + } + ) + }.build() +} + +private fun Polynomial.ensureAtLeastConstant(): Polynomial { + return if (termCount == 0) { + Polynomial.newBuilder().apply { + addTerm( + Term.newBuilder().apply { + coefficient = ZERO + }.build() + ) + }.build() + } else this +} + +private fun Polynomial.getLeadingTerm(matchedVariable: String? = null): Term { + // Return the leading term. Reference: https://undergroundmathematics.org/glossary/leading-term. + return termList.filter { term -> + matchedVariable?.let { variableName -> + term.variableList.any { it.name == variableName } + } ?: true + }.reduce { maxTerm, term -> + val maxTermDegree = maxTerm.highestDegree() + val termDegree = term.highestDegree() + return@reduce if (termDegree > maxTermDegree) term else maxTerm + } +} + +private fun Term.highestDegreeVariable(): Variable? = variableList.maxByOrNull(Variable::getPower) + +private fun Term.highestDegree(): Int = highestDegreeVariable()?.power ?: 0 + +private fun Term.toPolynomial(): Polynomial { + return Polynomial.newBuilder().addTerm(this).build() +} + +private fun List.toPowerMap(): Map { + return associateBy({ it.name }, { it.power }) +} + +private fun Map.toVariableList(): List { + return map { (name, power) -> Variable.newBuilder().setName(name).setPower(power).build() } +} diff --git a/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt index cbe17f0dc92..4359fee66aa 100644 --- a/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt @@ -8,12 +8,37 @@ import org.oppia.android.app.model.Real.RealTypeCase.RATIONAL import org.oppia.android.app.model.Real.RealTypeCase.REALTYPE_NOT_SET import kotlin.math.pow +val ZERO: Real by lazy { + Real.newBuilder().apply { integer = 0 }.build() +} + +val ONE: Real by lazy { + Real.newBuilder().apply { integer = 1 }.build() +} + +val ONE_HALF: Real by lazy { + Real.newBuilder().apply { + rational = Fraction.newBuilder().apply { + numerator = 1 + denominator = 2 + }.build() + }.build() +} + val REAL_COMPARATOR: Comparator by lazy { Comparator.comparing(Real::toDouble) } fun Real.isRational(): Boolean = realTypeCase == RATIONAL fun Real.isInteger(): Boolean = realTypeCase == INTEGER +fun Real.isWholeNumber(): Boolean { + return when (realTypeCase) { + RATIONAL -> rational.isOnlyWholeNumber() + INTEGER -> true + IRRATIONAL, REALTYPE_NOT_SET, null -> false + } +} + fun Real.isNegative(): Boolean = when (realTypeCase) { RATIONAL -> rational.isNegative IRRATIONAL -> irrational < 0 @@ -21,6 +46,12 @@ fun Real.isNegative(): Boolean = when (realTypeCase) { REALTYPE_NOT_SET, null -> throw Exception("Invalid real: $this.") } +fun Real.isApproximatelyEqualTo(value: Double): Boolean { + return toDouble().approximatelyEquals(value) +} + +fun Real.isApproximatelyZero(): Boolean = isApproximatelyEqualTo(0.0) + fun Real.toDouble(): Double { return when (realTypeCase) { RATIONAL -> rational.toDouble() @@ -30,6 +61,15 @@ fun Real.toDouble(): Double { } } +fun Real.asWholeNumber(): Int? { + return when (realTypeCase) { + RATIONAL -> if (rational.isOnlyWholeNumber()) rational.toWholeNumber() else null + INTEGER -> integer + IRRATIONAL -> null + REALTYPE_NOT_SET, null -> throw Exception("Invalid real: $this.") + } +} + fun Real.toPlainText(): String = when (realTypeCase) { // Note that the rational part is first converted to an improper fraction since mixed fractions // can't be expressed as a single coefficient in typical polynomial syntax). @@ -39,10 +79,6 @@ fun Real.toPlainText(): String = when (realTypeCase) { REALTYPE_NOT_SET, null -> "" } -fun Real.isApproximatelyEqualTo(value: Double): Boolean { - return toDouble().approximatelyEquals(value) -} - operator fun Real.unaryMinus(): Real { return when (realTypeCase) { RATIONAL -> recompute { it.setRational(-rational) } diff --git a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel index fcce3805364..f5784442a09 100644 --- a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel @@ -81,6 +81,24 @@ oppia_android_test( ], ) +oppia_android_test( + name = "ExpressionToPolynomialTest", + srcs = ["ExpressionToPolynomialTest.kt"], + custom_package = "org.oppia.android.util.math", + test_class = "org.oppia.android.util.math.ExpressionToPolynomialTest", + test_manifest = "//utility:test_manifest", + deps = [ + "//model/src/main/proto:math_java_proto_lite", + "//testing/src/main/java/org/oppia/android/testing/math:polynomial_subject", + "//third_party:androidx_test_ext_junit", + "//third_party:com_google_truth_truth", + "//third_party:junit_junit", + "//third_party:org_robolectric_robolectric", + "//third_party:robolectric_android-all", + "//utility/src/main/java/org/oppia/android/util/math:parser", + ], +) + oppia_android_test( name = "MathExpressionParserTest", srcs = ["MathExpressionParserTest.kt"], diff --git a/utility/src/test/java/org/oppia/android/util/math/ExpressionToPolynomialTest.kt b/utility/src/test/java/org/oppia/android/util/math/ExpressionToPolynomialTest.kt new file mode 100644 index 00000000000..f8163f88c3c --- /dev/null +++ b/utility/src/test/java/org/oppia/android/util/math/ExpressionToPolynomialTest.kt @@ -0,0 +1,945 @@ +package org.oppia.android.util.math + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Test +import org.junit.runner.RunWith +import org.oppia.android.app.model.MathExpression +import org.oppia.android.testing.math.PolynomialSubject.Companion.assertThat +import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode +import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult +import org.robolectric.annotation.LooperMode + +/** Tests for [MathExpressionParser]. */ +@RunWith(AndroidJUnit4::class) +@LooperMode(LooperMode.Mode.PAUSED) +class ExpressionToPolynomialTest { + // TODO: add high-level checks for the three types, but don't test in detail since there are + // separate suites. Also, document the separate suites' existence in this suites's KDoc. + + @Test + fun testPolynomials() { + // TODO: split up & move to separate test suites. Finish test cases (if anymore are needed). + + val poly1 = parseNumericExpressionSuccessfully("1").toPolynomial() + assertThat(poly1).evaluatesToPlainTextThat().isEqualTo("1") + assertThat(poly1).isConstantThat().isIntegerThat().isEqualTo(1) + + val poly13 = parseNumericExpressionSuccessfully("1-1").toPolynomial() + assertThat(poly13).evaluatesToPlainTextThat().isEqualTo("0") + assertThat(poly13).isConstantThat().isIntegerThat().isEqualTo(0) + + val poly2 = parseNumericExpressionSuccessfully("3 + 4 * 2 / (1 - 5) ^ 2").toPolynomial() + assertThat(poly2).evaluatesToPlainTextThat().isEqualTo("7/2") + assertThat(poly2).isConstantThat().isRationalThat().apply { + hasNegativePropertyThat().isFalse() + hasWholeNumberThat().isEqualTo(3) + hasNumeratorThat().isEqualTo(1) + hasDenominatorThat().isEqualTo(2) + } + + val poly3 = + parseAlgebraicExpressionSuccessfullyWithAllErrors("133+3.14*x/(11-15)^2").toPolynomial() + assertThat(poly3).evaluatesToPlainTextThat().isEqualTo("0.19625x + 133") + assertThat(poly3).hasTermCountThat().isEqualTo(2) + assertThat(poly3).term(0).hasCoefficientThat().isIrrationalThat().isWithin(1e-5).of(0.19625) + assertThat(poly3).term(0).hasVariableCountThat().isEqualTo(1) + assertThat(poly3).term(0).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly3).term(0).variable(0).hasPowerThat().isEqualTo(1) + assertThat(poly3).term(1).hasCoefficientThat().isIntegerThat().isEqualTo(133) + assertThat(poly3).term(1).hasVariableCountThat().isEqualTo(0) + + val poly4 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x^2").toPolynomial() + assertThat(poly4).evaluatesToPlainTextThat().isEqualTo("x^2") + assertThat(poly4).hasTermCountThat().isEqualTo(1) + assertThat(poly4).term(0).hasVariableCountThat().isEqualTo(1) + assertThat(poly4).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly4).term(0).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly4).term(0).variable(0).hasPowerThat().isEqualTo(2) + + val poly5 = parseAlgebraicExpressionSuccessfullyWithAllErrors("xy+x").toPolynomial() + assertThat(poly5).evaluatesToPlainTextThat().isEqualTo("xy + x") + assertThat(poly5).hasTermCountThat().isEqualTo(2) + assertThat(poly5).term(0).hasVariableCountThat().isEqualTo(2) + assertThat(poly5).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly5).term(0).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly5).term(0).variable(0).hasPowerThat().isEqualTo(1) + assertThat(poly5).term(0).variable(1).hasNameThat().isEqualTo("y") + assertThat(poly5).term(0).variable(1).hasPowerThat().isEqualTo(1) + assertThat(poly5).term(1).hasVariableCountThat().isEqualTo(1) + assertThat(poly5).term(1).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly5).term(1).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly5).term(1).variable(0).hasPowerThat().isEqualTo(1) + + val poly6 = parseAlgebraicExpressionSuccessfullyWithAllErrors("2x").toPolynomial() + assertThat(poly6).evaluatesToPlainTextThat().isEqualTo("2x") + assertThat(poly6).hasTermCountThat().isEqualTo(1) + assertThat(poly6).term(0).hasVariableCountThat().isEqualTo(1) + assertThat(poly6).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(2) + assertThat(poly6).term(0).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly6).term(0).variable(0).hasPowerThat().isEqualTo(1) + + val poly30 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x+2").toPolynomial() + assertThat(poly30).evaluatesToPlainTextThat().isEqualTo("x + 2") + assertThat(poly30).hasTermCountThat().isEqualTo(2) + assertThat(poly30).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + assertThat(poly30).term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(2) + hasVariableCountThat().isEqualTo(0) + } + + val poly29 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x^2-3*x-10").toPolynomial() + assertThat(poly29).evaluatesToPlainTextThat().isEqualTo("x^2 - 3x - 10") + assertThat(poly29).hasTermCountThat().isEqualTo(3) + assertThat(poly29).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + assertThat(poly29).term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-3) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + assertThat(poly29).term(2).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-10) + hasVariableCountThat().isEqualTo(0) + } + + val poly31 = parseAlgebraicExpressionSuccessfullyWithAllErrors("4*(x+2)").toPolynomial() + assertThat(poly31).evaluatesToPlainTextThat().isEqualTo("4x + 8") + assertThat(poly31).hasTermCountThat().isEqualTo(2) + assertThat(poly31).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(4) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + assertThat(poly31).term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(8) + hasVariableCountThat().isEqualTo(0) + } + + val poly7 = parseAlgebraicExpressionSuccessfullyWithAllErrors("2xy^2z^3").toPolynomial() + assertThat(poly7).evaluatesToPlainTextThat().isEqualTo("2xy^2z^3") + assertThat(poly7).hasTermCountThat().isEqualTo(1) + assertThat(poly7).term(0).hasVariableCountThat().isEqualTo(3) + assertThat(poly7).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(2) + assertThat(poly7).term(0).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly7).term(0).variable(0).hasPowerThat().isEqualTo(1) + assertThat(poly7).term(0).variable(1).hasNameThat().isEqualTo("y") + assertThat(poly7).term(0).variable(1).hasPowerThat().isEqualTo(2) + assertThat(poly7).term(0).variable(2).hasNameThat().isEqualTo("z") + assertThat(poly7).term(0).variable(2).hasPowerThat().isEqualTo(3) + + // Show that 7+xy+yz-3-xz-yz+3xy-4 combines into 4xy-xz (the eliminated terms should be gone). + val poly8 = parseAlgebraicExpressionSuccessfullyWithAllErrors("xy+yz-xz-yz+3xy").toPolynomial() + assertThat(poly8).evaluatesToPlainTextThat().isEqualTo("4xy - xz") + assertThat(poly8).hasTermCountThat().isEqualTo(2) + assertThat(poly8).term(0).hasVariableCountThat().isEqualTo(2) + assertThat(poly8).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(4) + assertThat(poly8).term(0).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly8).term(0).variable(0).hasPowerThat().isEqualTo(1) + assertThat(poly8).term(0).variable(1).hasNameThat().isEqualTo("y") + assertThat(poly8).term(0).variable(1).hasPowerThat().isEqualTo(1) + assertThat(poly8).term(1).hasVariableCountThat().isEqualTo(2) + assertThat(poly8).term(1).hasCoefficientThat().isIntegerThat().isEqualTo(-1) + assertThat(poly8).term(1).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly8).term(1).variable(0).hasPowerThat().isEqualTo(1) + assertThat(poly8).term(1).variable(1).hasNameThat().isEqualTo("z") + assertThat(poly8).term(1).variable(1).hasPowerThat().isEqualTo(1) + + // x+2x should become 3x since like terms are combined. + val poly9 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x+2x").toPolynomial() + assertThat(poly9).evaluatesToPlainTextThat().isEqualTo("3x") + assertThat(poly9).hasTermCountThat().isEqualTo(1) + assertThat(poly9).term(0).hasVariableCountThat().isEqualTo(1) + assertThat(poly9).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(3) + assertThat(poly9).term(0).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly9).term(0).variable(0).hasPowerThat().isEqualTo(1) + + // xx^2 should become x^3 since like terms are combined. + val poly10 = parseAlgebraicExpressionSuccessfullyWithAllErrors("xx^2").toPolynomial() + assertThat(poly10).evaluatesToPlainTextThat().isEqualTo("x^3") + assertThat(poly10).hasTermCountThat().isEqualTo(1) + assertThat(poly10).term(0).hasVariableCountThat().isEqualTo(1) + assertThat(poly10).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly10).term(0).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly10).term(0).variable(0).hasPowerThat().isEqualTo(3) + + // No terms in this polynomial should be combined. + val poly11 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x^2+x+1").toPolynomial() + assertThat(poly11).evaluatesToPlainTextThat().isEqualTo("x^2 + x + 1") + assertThat(poly11).hasTermCountThat().isEqualTo(3) + assertThat(poly11).term(0).hasVariableCountThat().isEqualTo(1) + assertThat(poly11).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly11).term(0).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly11).term(0).variable(0).hasPowerThat().isEqualTo(2) + assertThat(poly11).term(1).hasVariableCountThat().isEqualTo(1) + assertThat(poly11).term(1).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly11).term(1).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly11).term(1).variable(0).hasPowerThat().isEqualTo(1) + assertThat(poly11).term(2).hasVariableCountThat().isEqualTo(0) + assertThat(poly11).term(2).hasCoefficientThat().isIntegerThat().isEqualTo(1) + + // No terms in this polynomial should be combined. + val poly12 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x^2 + x^2y").toPolynomial() + assertThat(poly12).evaluatesToPlainTextThat().isEqualTo("x^2y + x^2") + assertThat(poly12).hasTermCountThat().isEqualTo(2) + assertThat(poly12).term(0).hasVariableCountThat().isEqualTo(2) + assertThat(poly12).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly12).term(0).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly12).term(0).variable(0).hasPowerThat().isEqualTo(2) + assertThat(poly12).term(0).variable(1).hasNameThat().isEqualTo("y") + assertThat(poly12).term(0).variable(1).hasPowerThat().isEqualTo(1) + assertThat(poly12).term(1).hasVariableCountThat().isEqualTo(1) + assertThat(poly12).term(1).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly12).term(1).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly12).term(1).variable(0).hasPowerThat().isEqualTo(2) + + // Ordering tests. Verify that ordering matches + // https://en.wikipedia.org/wiki/Polynomial#Definition (where multiple variables are sorted + // lexicographically). + + // The order of the terms in this polynomial should be reversed. + val poly14 = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+x+x^2+x^3").toPolynomial() + assertThat(poly14).evaluatesToPlainTextThat().isEqualTo("x^3 + x^2 + x + 1") + assertThat(poly14).hasTermCountThat().isEqualTo(4) + assertThat(poly14).term(0).hasVariableCountThat().isEqualTo(1) + assertThat(poly14).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly14).term(0).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly14).term(0).variable(0).hasPowerThat().isEqualTo(3) + assertThat(poly14).term(1).hasVariableCountThat().isEqualTo(1) + assertThat(poly14).term(1).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly14).term(1).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly14).term(1).variable(0).hasPowerThat().isEqualTo(2) + assertThat(poly14).term(2).hasVariableCountThat().isEqualTo(1) + assertThat(poly14).term(2).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly14).term(2).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly14).term(2).variable(0).hasPowerThat().isEqualTo(1) + assertThat(poly14).term(3).hasVariableCountThat().isEqualTo(0) + assertThat(poly14).term(3).hasCoefficientThat().isIntegerThat().isEqualTo(1) + + // The order of the terms in this polynomial should be preserved. + val poly15 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x^3+x^2+x+1").toPolynomial() + assertThat(poly15).evaluatesToPlainTextThat().isEqualTo("x^3 + x^2 + x + 1") + assertThat(poly15).hasTermCountThat().isEqualTo(4) + assertThat(poly15).term(0).hasVariableCountThat().isEqualTo(1) + assertThat(poly15).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly15).term(0).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly15).term(0).variable(0).hasPowerThat().isEqualTo(3) + assertThat(poly15).term(1).hasVariableCountThat().isEqualTo(1) + assertThat(poly15).term(1).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly15).term(1).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly15).term(1).variable(0).hasPowerThat().isEqualTo(2) + assertThat(poly15).term(2).hasVariableCountThat().isEqualTo(1) + assertThat(poly15).term(2).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly15).term(2).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly15).term(2).variable(0).hasPowerThat().isEqualTo(1) + assertThat(poly15).term(3).hasVariableCountThat().isEqualTo(0) + assertThat(poly15).term(3).hasCoefficientThat().isIntegerThat().isEqualTo(1) + + // The order of the terms in this polynomial should be reversed. + val poly16 = parseAlgebraicExpressionSuccessfullyWithAllErrors("xy+xz+yz").toPolynomial() + assertThat(poly16).evaluatesToPlainTextThat().isEqualTo("xy + xz + yz") + assertThat(poly16).hasTermCountThat().isEqualTo(3) + assertThat(poly16).term(0).hasVariableCountThat().isEqualTo(2) + assertThat(poly16).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly16).term(0).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly16).term(0).variable(0).hasPowerThat().isEqualTo(1) + assertThat(poly16).term(0).variable(1).hasNameThat().isEqualTo("y") + assertThat(poly16).term(0).variable(1).hasPowerThat().isEqualTo(1) + assertThat(poly16).term(1).hasVariableCountThat().isEqualTo(2) + assertThat(poly16).term(1).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly16).term(1).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly16).term(1).variable(0).hasPowerThat().isEqualTo(1) + assertThat(poly16).term(1).variable(1).hasNameThat().isEqualTo("z") + assertThat(poly16).term(1).variable(1).hasPowerThat().isEqualTo(1) + assertThat(poly16).term(2).hasVariableCountThat().isEqualTo(2) + assertThat(poly16).term(2).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly16).term(2).variable(0).hasNameThat().isEqualTo("y") + assertThat(poly16).term(2).variable(0).hasPowerThat().isEqualTo(1) + assertThat(poly16).term(2).variable(1).hasNameThat().isEqualTo("z") + assertThat(poly16).term(2).variable(1).hasPowerThat().isEqualTo(1) + + // The order of the terms in this polynomial should be preserved. + val poly17 = parseAlgebraicExpressionSuccessfullyWithAllErrors("yz+xz+xy").toPolynomial() + assertThat(poly17).evaluatesToPlainTextThat().isEqualTo("xy + xz + yz") + assertThat(poly17).hasTermCountThat().isEqualTo(3) + assertThat(poly17).term(0).hasVariableCountThat().isEqualTo(2) + assertThat(poly17).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly17).term(0).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly17).term(0).variable(0).hasPowerThat().isEqualTo(1) + assertThat(poly17).term(0).variable(1).hasNameThat().isEqualTo("y") + assertThat(poly17).term(0).variable(1).hasPowerThat().isEqualTo(1) + assertThat(poly17).term(1).hasVariableCountThat().isEqualTo(2) + assertThat(poly17).term(1).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly17).term(1).variable(0).hasNameThat().isEqualTo("x") + assertThat(poly17).term(1).variable(0).hasPowerThat().isEqualTo(1) + assertThat(poly17).term(1).variable(1).hasNameThat().isEqualTo("z") + assertThat(poly17).term(1).variable(1).hasPowerThat().isEqualTo(1) + assertThat(poly17).term(2).hasVariableCountThat().isEqualTo(2) + assertThat(poly17).term(2).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(poly17).term(2).variable(0).hasNameThat().isEqualTo("y") + assertThat(poly17).term(2).variable(0).hasPowerThat().isEqualTo(1) + assertThat(poly17).term(2).variable(1).hasNameThat().isEqualTo("z") + assertThat(poly17).term(2).variable(1).hasPowerThat().isEqualTo(1) + + val poly18 = + parseAlgebraicExpressionSuccessfullyWithAllErrors("3+x+y+xy+x^2y+xy^2+x^2y^2").toPolynomial() + assertThat(poly18).evaluatesToPlainTextThat().isEqualTo("x^2y^2 + x^2y + xy^2 + xy + x + y + 3") + assertThat(poly18).hasTermCountThat().isEqualTo(7) + assertThat(poly18).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(2) + } + } + assertThat(poly18).term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + assertThat(poly18).term(2).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(2) + } + } + assertThat(poly18).term(3).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + assertThat(poly18).term(4).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + assertThat(poly18).term(5).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + assertThat(poly18).term(6).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(3) + hasVariableCountThat().isEqualTo(0) + } + + // Ensure variables of coefficient and power of 0 are removed. + val poly22 = parseAlgebraicExpressionSuccessfullyWithAllErrors("0x").toPolynomial() + assertThat(poly22).evaluatesToPlainTextThat().isEqualTo("0") + assertThat(poly22).hasTermCountThat().isEqualTo(1) + assertThat(poly22).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(0) + hasVariableCountThat().isEqualTo(0) + } + + val poly23 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x-x").toPolynomial() + assertThat(poly23).evaluatesToPlainTextThat().isEqualTo("0") + assertThat(poly23).hasTermCountThat().isEqualTo(1) + assertThat(poly23).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(0) + hasVariableCountThat().isEqualTo(0) + } + + val poly24 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x^0").toPolynomial() + assertThat(poly24).evaluatesToPlainTextThat().isEqualTo("1") + assertThat(poly24).hasTermCountThat().isEqualTo(1) + assertThat(poly24).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(0) + } + + val poly25 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x/x").toPolynomial() + assertThat(poly25).evaluatesToPlainTextThat().isEqualTo("1") + assertThat(poly25).hasTermCountThat().isEqualTo(1) + assertThat(poly25).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(0) + } + + val poly26 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x^(2-2)").toPolynomial() + assertThat(poly26).evaluatesToPlainTextThat().isEqualTo("1") + assertThat(poly26).hasTermCountThat().isEqualTo(1) + assertThat(poly26).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(0) + } + + val poly28 = parseAlgebraicExpressionSuccessfullyWithAllErrors("(x+1)/2").toPolynomial() + assertThat(poly28).evaluatesToPlainTextThat().isEqualTo("(1/2)x + 1/2") + assertThat(poly28).hasTermCountThat().isEqualTo(2) + assertThat(poly28).term(0).apply { + hasCoefficientThat().isRationalThat().apply { + hasNegativePropertyThat().isFalse() + hasWholeNumberThat().isEqualTo(0) + hasNumeratorThat().isEqualTo(1) + hasDenominatorThat().isEqualTo(2) + } + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + assertThat(poly28).term(1).apply { + hasCoefficientThat().isRationalThat().apply { + hasNegativePropertyThat().isFalse() + hasWholeNumberThat().isEqualTo(0) + hasNumeratorThat().isEqualTo(1) + hasDenominatorThat().isEqualTo(2) + } + hasVariableCountThat().isEqualTo(0) + } + + // Ensure like terms are combined after polynomial multiplication. + val poly20 = parseAlgebraicExpressionSuccessfullyWithAllErrors("(x-5)(x+2)").toPolynomial() + assertThat(poly20).evaluatesToPlainTextThat().isEqualTo("x^2 - 3x - 10") + assertThat(poly20).hasTermCountThat().isEqualTo(3) + assertThat(poly20).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + assertThat(poly20).term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-3) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + assertThat(poly20).term(2).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-10) + hasVariableCountThat().isEqualTo(0) + } + + val poly21 = parseAlgebraicExpressionSuccessfullyWithAllErrors("(1+x)^3").toPolynomial() + assertThat(poly21).evaluatesToPlainTextThat().isEqualTo("x^3 + 3x^2 + 3x + 1") + assertThat(poly21).hasTermCountThat().isEqualTo(4) + assertThat(poly21).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(3) + } + } + assertThat(poly21).term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(3) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + assertThat(poly21).term(2).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(3) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + assertThat(poly21).term(3).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(0) + } + + val poly27 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x^2*y^2 + 2").toPolynomial() + assertThat(poly27).evaluatesToPlainTextThat().isEqualTo("x^2y^2 + 2") + assertThat(poly27).hasTermCountThat().isEqualTo(2) + assertThat(poly27).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(2) + } + } + assertThat(poly27).term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(2) + hasVariableCountThat().isEqualTo(0) + } + + val poly32 = + parseAlgebraicExpressionSuccessfullyWithAllErrors("(x^2-3*x-10)*(x+2)").toPolynomial() + assertThat(poly32).evaluatesToPlainTextThat().isEqualTo("x^3 - x^2 - 16x - 20") + assertThat(poly32).hasTermCountThat().isEqualTo(4) + assertThat(poly32).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(3) + } + } + assertThat(poly32).term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + assertThat(poly32).term(2).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-16) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + assertThat(poly32).term(3).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-20) + hasVariableCountThat().isEqualTo(0) + } + + val poly33 = parseAlgebraicExpressionSuccessfullyWithAllErrors("(x-y)^3").toPolynomial() + assertThat(poly33).evaluatesToPlainTextThat().isEqualTo("x^3 - 3x^2y + 3xy^2 - y^3") + assertThat(poly33).hasTermCountThat().isEqualTo(4) + assertThat(poly33).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(3) + } + } + assertThat(poly33).term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-3) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + assertThat(poly33).term(2).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(3) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(2) + } + } + assertThat(poly33).term(3).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(3) + } + } + + // Ensure polynomial division works. + val poly19 = + parseAlgebraicExpressionSuccessfullyWithAllErrors("(x^2-3*x-10)/(x+2)").toPolynomial() + assertThat(poly19).evaluatesToPlainTextThat().isEqualTo("x - 5") + assertThat(poly19).hasTermCountThat().isEqualTo(2) + assertThat(poly19).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + assertThat(poly19).term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-5) + hasVariableCountThat().isEqualTo(0) + } + + val poly35 = parseAlgebraicExpressionSuccessfullyWithAllErrors("(xy-5y)/y").toPolynomial() + assertThat(poly35).evaluatesToPlainTextThat().isEqualTo("x - 5") + assertThat(poly35).hasTermCountThat().isEqualTo(2) + assertThat(poly35).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + assertThat(poly35).term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-5) + hasVariableCountThat().isEqualTo(0) + } + + val poly36 = + parseAlgebraicExpressionSuccessfullyWithAllErrors("(x^2-2xy+y^2)/(x-y)").toPolynomial() + assertThat(poly36).evaluatesToPlainTextThat().isEqualTo("x - y") + assertThat(poly36).hasTermCountThat().isEqualTo(2) + assertThat(poly36).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + assertThat(poly36).term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + + // Example from https://www.kristakingmath.com/blog/predator-prey-systems-ghtcp-5e2r4-427ab. + val poly37 = parseAlgebraicExpressionSuccessfullyWithAllErrors("(x^3-y^3)/(x-y)").toPolynomial() + assertThat(poly37).evaluatesToPlainTextThat().isEqualTo("x^2 + xy + y^2") + assertThat(poly37).hasTermCountThat().isEqualTo(3) + assertThat(poly37).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + assertThat(poly37).term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + assertThat(poly37).term(2).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(2) + } + } + + // Multi-variable & more complex division. + val poly34 = + parseAlgebraicExpressionSuccessfullyWithAllErrors( + "(x^3-3x^2y+3xy^2-y^3)/(x-y)^2" + ).toPolynomial() + assertThat(poly34).evaluatesToPlainTextThat().isEqualTo("x - y") + assertThat(poly34).hasTermCountThat().isEqualTo(2) + assertThat(poly34).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + assertThat(poly34).term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + + val poly38 = parseNumericExpressionSuccessfully("2^-4").toPolynomial() + assertThat(poly38).evaluatesToPlainTextThat().isEqualTo("1/16") + assertThat(poly38).hasTermCountThat().isEqualTo(1) + assertThat(poly38).term(0).apply { + hasCoefficientThat().isRationalThat().apply { + hasNegativePropertyThat().isFalse() + hasWholeNumberThat().isEqualTo(0) + hasNumeratorThat().isEqualTo(1) + hasDenominatorThat().isEqualTo(16) + } + hasVariableCountThat().isEqualTo(0) + } + + val poly39 = parseNumericExpressionSuccessfully("2^(3-6)").toPolynomial() + assertThat(poly39).evaluatesToPlainTextThat().isEqualTo("1/8") + assertThat(poly39).hasTermCountThat().isEqualTo(1) + assertThat(poly39).term(0).apply { + hasCoefficientThat().isRationalThat().apply { + hasNegativePropertyThat().isFalse() + hasWholeNumberThat().isEqualTo(0) + hasNumeratorThat().isEqualTo(1) + hasDenominatorThat().isEqualTo(8) + } + hasVariableCountThat().isEqualTo(0) + } + + // x^-3 is not a valid polynomial (since polynomials can't have negative powers). + val poly40 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x^(3-6)").toPolynomial() + assertThat(poly40).isNotValidPolynomial() + + // 2^x is not a polynomial. + val poly41 = parseAlgebraicExpressionSuccessfullyWithoutOptionalErrors("2^x").toPolynomial() + assertThat(poly41).isNotValidPolynomial() + + // 1/x is not a polynomial. + val poly42 = parseAlgebraicExpressionSuccessfullyWithoutOptionalErrors("1/x").toPolynomial() + assertThat(poly42).isNotValidPolynomial() + + val poly43 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x/2").toPolynomial() + assertThat(poly43).evaluatesToPlainTextThat().isEqualTo("(1/2)x") + assertThat(poly43).hasTermCountThat().isEqualTo(1) + assertThat(poly43).term(0).apply { + hasCoefficientThat().isRationalThat().apply { + hasNegativePropertyThat().isFalse() + hasWholeNumberThat().isEqualTo(0) + hasNumeratorThat().isEqualTo(1) + hasDenominatorThat().isEqualTo(2) + } + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + + val poly44 = parseAlgebraicExpressionSuccessfullyWithAllErrors("(x-3)/2").toPolynomial() + assertThat(poly44).evaluatesToPlainTextThat().isEqualTo("(1/2)x - 3/2") + assertThat(poly44).hasTermCountThat().isEqualTo(2) + assertThat(poly44).term(0).apply { + hasCoefficientThat().isRationalThat().apply { + hasNegativePropertyThat().isFalse() + hasWholeNumberThat().isEqualTo(0) + hasNumeratorThat().isEqualTo(1) + hasDenominatorThat().isEqualTo(2) + } + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + assertThat(poly44).term(1).apply { + hasCoefficientThat().isRationalThat().apply { + hasNegativePropertyThat().isTrue() + hasWholeNumberThat().isEqualTo(0) + hasNumeratorThat().isEqualTo(3) + hasDenominatorThat().isEqualTo(2) + } + hasVariableCountThat().isEqualTo(0) + } + + val poly45 = parseAlgebraicExpressionSuccessfullyWithAllErrors("(x-1)(x+1)").toPolynomial() + assertThat(poly45).evaluatesToPlainTextThat().isEqualTo("x^2 - 1") + assertThat(poly45).hasTermCountThat().isEqualTo(2) + assertThat(poly45).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + assertThat(poly45).term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-1) + hasVariableCountThat().isEqualTo(0) + } + + // √x is not a polynomial. + val poly46 = parseAlgebraicExpressionSuccessfullyWithAllErrors("sqrt(x)").toPolynomial() + assertThat(poly46).isNotValidPolynomial() + + val poly47 = parseAlgebraicExpressionSuccessfullyWithAllErrors("√(x^2)").toPolynomial() + assertThat(poly47).evaluatesToPlainTextThat().isEqualTo("x") + assertThat(poly47).hasTermCountThat().isEqualTo(1) + assertThat(poly47).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + + val poly51 = parseAlgebraicExpressionSuccessfullyWithAllErrors("√(x^2y^2)").toPolynomial() + assertThat(poly51).evaluatesToPlainTextThat().isEqualTo("xy") + assertThat(poly51).hasTermCountThat().isEqualTo(1) + assertThat(poly51).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + + // A limitation in the current polynomial conversion is that sqrt(x) will fail due to it not + // have any polynomial representation. + val poly48 = parseAlgebraicExpressionSuccessfullyWithAllErrors("√x^2").toPolynomial() + assertThat(poly48).isNotValidPolynomial() + + // √(x^2+2) may evaluate to a polynomial, but it requires factoring (which isn't yet supported). + val poly50 = parseAlgebraicExpressionSuccessfullyWithAllErrors("√(x^2+2)").toPolynomial() + assertThat(poly50).isNotValidPolynomial() + + // Division by zero is undefined, so a polynomial can't be constructed. + val poly49 = parseAlgebraicExpressionSuccessfullyWithoutOptionalErrors("(x+2)/0").toPolynomial() + assertThat(poly49).isNotValidPolynomial() + + val poly52 = parsePolynomialFromNumericExpression("1") + val poly53 = parsePolynomialFromNumericExpression("0") + assertThat(poly52).isNotEqualTo(poly53) + + val poly54 = parsePolynomialFromNumericExpression("1+2") + val poly55 = parsePolynomialFromNumericExpression("3") + assertThat(poly54).isEqualTo(poly55) + + val poly56 = parsePolynomialFromNumericExpression("1-2") + val poly57 = parsePolynomialFromNumericExpression("-1") + assertThat(poly56).isEqualTo(poly57) + + val poly58 = parsePolynomialFromNumericExpression("2*3") + val poly59 = parsePolynomialFromNumericExpression("6") + assertThat(poly58).isEqualTo(poly59) + + val poly60 = parsePolynomialFromNumericExpression("2^3") + val poly61 = parsePolynomialFromNumericExpression("8") + assertThat(poly60).isEqualTo(poly61) + + val poly62 = parsePolynomialFromAlgebraicExpression("1+x") + val poly63 = parsePolynomialFromAlgebraicExpression("x+1") + assertThat(poly62).isEqualTo(poly63) + + val poly64 = parsePolynomialFromAlgebraicExpression("y+x") + val poly65 = parsePolynomialFromAlgebraicExpression("x+y") + assertThat(poly64).isEqualTo(poly65) + + val poly66 = parsePolynomialFromAlgebraicExpression("(x+1)^2") + val poly67 = parsePolynomialFromAlgebraicExpression("x^2+2x+1") + assertThat(poly66).isEqualTo(poly67) + + val poly68 = parsePolynomialFromAlgebraicExpression("(x+1)/2") + val poly69 = parsePolynomialFromAlgebraicExpression("x/2+(1/2)") + assertThat(poly68).isEqualTo(poly69) + + val poly70 = parsePolynomialFromAlgebraicExpression("x*2") + val poly71 = parsePolynomialFromAlgebraicExpression("2x") + assertThat(poly70).isEqualTo(poly71) + + val poly72 = parsePolynomialFromAlgebraicExpression("x(x+1)") + val poly73 = parsePolynomialFromAlgebraicExpression("x^2+x") + assertThat(poly72).isEqualTo(poly73) + } + + private fun parsePolynomialFromNumericExpression(expression: String) = + parseNumericExpressionSuccessfully(expression).toPolynomial() + + private fun parsePolynomialFromAlgebraicExpression(expression: String) = + parseAlgebraicExpressionSuccessfullyWithAllErrors(expression).toPolynomial() + + private companion object { + // TODO: fix helper API. + + private fun parseNumericExpressionSuccessfully(expression: String): MathExpression { + val result = parseNumericExpressionWithAllErrors(expression) + return (result as MathParsingResult.Success).result + } + + private fun parseNumericExpressionWithAllErrors( + expression: String + ): MathParsingResult { + return MathExpressionParser.parseNumericExpression(expression, ErrorCheckingMode.ALL_ERRORS) + } + + private fun parseAlgebraicExpressionSuccessfullyWithAllErrors( + expression: String, + allowedVariables: List = listOf("x", "y", "z") + ): MathExpression { + val result = + parseAlgebraicExpressionInternal(expression, ErrorCheckingMode.ALL_ERRORS, allowedVariables) + return (result as MathParsingResult.Success).result + } + + private fun parseAlgebraicExpressionSuccessfullyWithoutOptionalErrors( + expression: String, + allowedVariables: List = listOf("x", "y", "z") + ): MathExpression { + val result = + parseAlgebraicExpressionInternal( + expression, ErrorCheckingMode.REQUIRED_ONLY, allowedVariables + ) + return (result as MathParsingResult.Success).result + } + + private fun parseAlgebraicExpressionInternal( + expression: String, + errorCheckingMode: ErrorCheckingMode, + allowedVariables: List = listOf("x", "y", "z") + ): MathParsingResult { + return MathExpressionParser.parseAlgebraicExpression( + expression, allowedVariables, errorCheckingMode + ) + } + } +} From 961b3d05c7c3af64c5fae3a08e9611b976ee8ae0 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 15 Dec 2021 15:25:05 -0800 Subject: [PATCH 017/162] Add NumericExpressionInput classifiers. This doesn't hook them up to the application or tests yet (that will happen in a later PR). --- domain/BUILD.bazel | 1 + ...nteractionObjectTypeExtractorRepository.kt | 1 + ...putIsEquivalentToRuleClassifierProvider.kt | 58 +++++++++++++++++++ ...atchesExactlyWithRuleClassifierProvider.kt | 49 ++++++++++++++++ ...vialManipulationsRuleClassifierProvider.kt | 51 ++++++++++++++++ .../NumericExpressionInputModule.kt | 36 ++++++++++++ .../util/InteractionObjectExtensions.kt | 2 + model/src/main/proto/interaction_object.proto | 1 + 8 files changed, 199 insertions(+) create mode 100644 domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProvider.kt create mode 100644 domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProvider.kt create mode 100644 domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt create mode 100644 domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputModule.kt diff --git a/domain/BUILD.bazel b/domain/BUILD.bazel index cf4b8098c8e..fed48085058 100755 --- a/domain/BUILD.bazel +++ b/domain/BUILD.bazel @@ -125,6 +125,7 @@ kt_android_library( "//utility/src/main/java/org/oppia/android/util/logging:event_logger", "//utility/src/main/java/org/oppia/android/util/logging:log_uploader", "//utility/src/main/java/org/oppia/android/util/math:extensions", + "//utility/src/main/java/org/oppia/android/util/math:parser", "//utility/src/main/java/org/oppia/android/util/networking:network_connection_util", "//utility/src/main/java/org/oppia/android/util/profile:directory_management_util", ], diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/InteractionObjectTypeExtractorRepository.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/InteractionObjectTypeExtractorRepository.kt index 9b72e5dea69..48bec5b8f5c 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/InteractionObjectTypeExtractorRepository.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/InteractionObjectTypeExtractorRepository.kt @@ -76,6 +76,7 @@ class InteractionObjectTypeExtractorRepository @Inject constructor() { createMapping(InteractionObject::getListOfSetsOfTranslatableHtmlContentIds) ObjectTypeCase.TRANSLATABLE_SET_OF_NORMALIZED_STRING -> createMapping(InteractionObject::getTranslatableSetOfNormalizedString) + ObjectTypeCase.MATH_EXPRESSION -> createMapping(InteractionObject::getMathExpression) ObjectTypeCase.OBJECTTYPE_NOT_SET -> createMapping { error("Invalid object type") } } } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProvider.kt new file mode 100644 index 00000000000..ecf89663802 --- /dev/null +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProvider.kt @@ -0,0 +1,58 @@ +package org.oppia.android.domain.classify.rules.numericexpressioninput + +import org.oppia.android.app.model.InteractionObject +import org.oppia.android.app.model.Polynomial +import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.RuleClassifier +import org.oppia.android.domain.classify.rules.GenericRuleClassifier +import org.oppia.android.domain.classify.rules.RuleClassifierProvider +import org.oppia.android.util.logging.ConsoleLogger +import org.oppia.android.util.math.MathExpressionParser +import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult +import org.oppia.android.util.math.toPolynomial +import javax.inject.Inject + +class NumericExpressionInputIsEquivalentToRuleClassifierProvider @Inject constructor( + private val classifierFactory: GenericRuleClassifier.Factory, + private val consoleLogger: ConsoleLogger +) : RuleClassifierProvider, GenericRuleClassifier.SingleInputMatcher { + override fun createRuleClassifier(): RuleClassifier { + return classifierFactory.createSingleInputClassifier( + expectedObjectType = InteractionObject.ObjectTypeCase.MATH_EXPRESSION, + inputParameterName = "x", + matcher = this + ) + } + + override fun matches( + answer: String, + input: String, + writtenTranslationContext: WrittenTranslationContext + ): Boolean { + val answerExpression = parsePolynomial(answer) ?: return false + val inputExpression = parsePolynomial(input) ?: return false + return answerExpression == inputExpression + } + + private fun parsePolynomial(rawExpression: String): Polynomial? { + return when (val expResult = MathExpressionParser.parseNumericExpression(rawExpression)) { + is MathParsingResult.Success -> { + expResult.result.toPolynomial().also { + if (it == null) { + consoleLogger.w( + "NumericExpEquivalent", "Expression is not a supported polynomial: $rawExpression." + ) + } + } + } + is MathParsingResult.Failure -> { + consoleLogger.e( + "NumericExpEquivalent", + "Encountered expression that failed parsing. Expression: $rawExpression." + + " Failure: ${expResult.error}." + ) + null + } + } + } +} diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProvider.kt new file mode 100644 index 00000000000..9ce52a59519 --- /dev/null +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProvider.kt @@ -0,0 +1,49 @@ +package org.oppia.android.domain.classify.rules.numericexpressioninput + +import org.oppia.android.app.model.InteractionObject +import org.oppia.android.app.model.MathExpression +import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.RuleClassifier +import org.oppia.android.domain.classify.rules.GenericRuleClassifier +import org.oppia.android.domain.classify.rules.RuleClassifierProvider +import org.oppia.android.util.logging.ConsoleLogger +import org.oppia.android.util.math.MathExpressionParser +import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult +import javax.inject.Inject + +class NumericExpressionInputMatchesExactlyWithRuleClassifierProvider @Inject constructor( + private val classifierFactory: GenericRuleClassifier.Factory, + private val consoleLogger: ConsoleLogger +) : RuleClassifierProvider, GenericRuleClassifier.SingleInputMatcher { + override fun createRuleClassifier(): RuleClassifier { + return classifierFactory.createSingleInputClassifier( + expectedObjectType = InteractionObject.ObjectTypeCase.MATH_EXPRESSION, + inputParameterName = "x", + matcher = this + ) + } + + override fun matches( + answer: String, + input: String, + writtenTranslationContext: WrittenTranslationContext + ): Boolean { + val answerExpression = parseNumericExpression(answer) ?: return false + val inputExpression = parseNumericExpression(input) ?: return false + return answerExpression == inputExpression + } + + private fun parseNumericExpression(rawExpression: String): MathExpression? { + return when (val expResult = MathExpressionParser.parseNumericExpression(rawExpression)) { + is MathParsingResult.Success -> expResult.result + is MathParsingResult.Failure -> { + consoleLogger.e( + "NumericExpMatchesExact", + "Encountered expression that failed parsing. Expression: $rawExpression." + + " Failure: ${expResult.error}." + ) + null + } + } + } +} diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt new file mode 100644 index 00000000000..d1ea260e948 --- /dev/null +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt @@ -0,0 +1,51 @@ +package org.oppia.android.domain.classify.rules.numericexpressioninput + +import org.oppia.android.app.model.ComparableOperationList +import org.oppia.android.app.model.InteractionObject +import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.RuleClassifier +import org.oppia.android.domain.classify.rules.GenericRuleClassifier +import org.oppia.android.domain.classify.rules.RuleClassifierProvider +import org.oppia.android.util.logging.ConsoleLogger +import org.oppia.android.util.math.MathExpressionParser +import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult +import org.oppia.android.util.math.toComparableOperationList +import javax.inject.Inject + +class NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider +@Inject constructor( + private val classifierFactory: GenericRuleClassifier.Factory, + private val consoleLogger: ConsoleLogger +) : RuleClassifierProvider, GenericRuleClassifier.SingleInputMatcher { + override fun createRuleClassifier(): RuleClassifier { + return classifierFactory.createSingleInputClassifier( + expectedObjectType = InteractionObject.ObjectTypeCase.MATH_EXPRESSION, + inputParameterName = "x", + matcher = this + ) + } + + override fun matches( + answer: String, + input: String, + writtenTranslationContext: WrittenTranslationContext + ): Boolean { + val answerExpression = parseComparableOperationList(answer) ?: return false + val inputExpression = parseComparableOperationList(input) ?: return false + return answerExpression == inputExpression + } + + private fun parseComparableOperationList(rawExpression: String): ComparableOperationList? { + return when (val expResult = MathExpressionParser.parseNumericExpression(rawExpression)) { + is MathParsingResult.Success -> expResult.result.toComparableOperationList() + is MathParsingResult.Failure -> { + consoleLogger.e( + "NumericExpTrivialManips", + "Encountered expression that failed parsing. Expression: $rawExpression." + + " Failure: ${expResult.error}." + ) + null + } + } + } +} diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputModule.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputModule.kt new file mode 100644 index 00000000000..cb010da48de --- /dev/null +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputModule.kt @@ -0,0 +1,36 @@ +package org.oppia.android.domain.classify.rules.numericexpressioninput + +import dagger.Module +import dagger.Provides +import dagger.multibindings.IntoMap +import dagger.multibindings.StringKey +import org.oppia.android.domain.classify.RuleClassifier +import org.oppia.android.domain.classify.rules.FractionInputRules + +/** Module that binds rule classifiers corresponding to the numeric expression input interaction. */ +@Module +class NumericExpressionInputModule { + @Provides + @IntoMap + @StringKey("MatchesExactlyWith") + @FractionInputRules + internal fun provideNumericExpressionInputMatchesExactlyWithRuleClassifierProvider( + classifierProvider: NumericExpressionInputMatchesExactlyWithRuleClassifierProvider + ): RuleClassifier = classifierProvider.createRuleClassifier() + + @Provides + @IntoMap + @StringKey("MatchesUpToTrivialManipulations") + @FractionInputRules + internal fun provideNumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifier( + classifierProvider: NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider + ): RuleClassifier = classifierProvider.createRuleClassifier() + + @Provides + @IntoMap + @StringKey("IsEquivalentTo") + @FractionInputRules + internal fun provideNumericExpressionInputIsEquivalentToRuleClassifier( + classifierProvider: NumericExpressionInputIsEquivalentToRuleClassifierProvider + ): RuleClassifier = classifierProvider.createRuleClassifier() +} diff --git a/domain/src/main/java/org/oppia/android/domain/util/InteractionObjectExtensions.kt b/domain/src/main/java/org/oppia/android/domain/util/InteractionObjectExtensions.kt index 32f9123e852..8ad6c07ecd1 100644 --- a/domain/src/main/java/org/oppia/android/domain/util/InteractionObjectExtensions.kt +++ b/domain/src/main/java/org/oppia/android/domain/util/InteractionObjectExtensions.kt @@ -9,6 +9,7 @@ import org.oppia.android.app.model.InteractionObject.ObjectTypeCase.FRACTION import org.oppia.android.app.model.InteractionObject.ObjectTypeCase.IMAGE_WITH_REGIONS import org.oppia.android.app.model.InteractionObject.ObjectTypeCase.LIST_OF_SETS_OF_HTML_STRING import org.oppia.android.app.model.InteractionObject.ObjectTypeCase.LIST_OF_SETS_OF_TRANSLATABLE_HTML_CONTENT_IDS +import org.oppia.android.app.model.InteractionObject.ObjectTypeCase.MATH_EXPRESSION import org.oppia.android.app.model.InteractionObject.ObjectTypeCase.NON_NEGATIVE_INT import org.oppia.android.app.model.InteractionObject.ObjectTypeCase.NORMALIZED_STRING import org.oppia.android.app.model.InteractionObject.ObjectTypeCase.NUMBER_WITH_UNITS @@ -53,6 +54,7 @@ fun InteractionObject.toAnswerString(): String { LIST_OF_SETS_OF_TRANSLATABLE_HTML_CONTENT_IDS -> listOfSetsOfTranslatableHtmlContentIds.toAnswerString() TRANSLATABLE_SET_OF_NORMALIZED_STRING -> translatableSetOfNormalizedString.toAnswerString() + MATH_EXPRESSION -> mathExpression OBJECTTYPE_NOT_SET -> "" // The default InteractionObject should be an empty string. } } diff --git a/model/src/main/proto/interaction_object.proto b/model/src/main/proto/interaction_object.proto index bb6154f9255..1e7b95ed7ef 100644 --- a/model/src/main/proto/interaction_object.proto +++ b/model/src/main/proto/interaction_object.proto @@ -29,6 +29,7 @@ message InteractionObject { TranslatableHtmlContentId translatable_html_content_id = 14; SetOfTranslatableHtmlContentIds set_of_translatable_html_content_ids = 15; ListOfSetsOfTranslatableHtmlContentIds list_of_sets_of_translatable_html_content_ids = 16; + string math_expression = 17; } } From 535ae2a8108f49dd8cb68e2443550b21d9e4be1f Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 15 Dec 2021 17:35:27 -0800 Subject: [PATCH 018/162] Introduce ClassificationContext. This refactor introduces support for passing customization arguments down to classifiers (which is needed for algebraic expression input). --- .../AnswerClassificationController.kt | 7 ++- .../domain/classify/ClassificationContext.kt | 10 +++ .../android/domain/classify/RuleClassifier.kt | 3 +- .../classify/rules/GenericRuleClassifier.kt | 48 +++++++------- ...asElementXAtPositionYClassifierProvider.kt | 4 +- ...lementXBeforeElementYClassifierProvider.kt | 4 +- ...nputIsEqualToOrderingClassifierProvider.kt | 4 +- ...emAtIncorrectPositionClassifierProvider.kt | 4 +- ...enominatorEqualToRuleClassifierProvider.kt | 4 +- ...artExactlyEqualToRuleClassifierProvider.kt | 4 +- ...ntegerPartEqualToRuleClassifierProvider.kt | 4 +- ...sNoFractionalPartRuleClassifierProvider.kt | 4 +- ...sNumeratorEqualToRuleClassifierProvider.kt | 4 +- ...AndInSimplestFormRuleClassifierProvider.kt | 4 +- ...putIsEquivalentToRuleClassifierProvider.kt | 4 +- ...tIsExactlyEqualToRuleClassifierProvider.kt | 4 +- ...nputIsGreaterThanRuleClassifierProvider.kt | 4 +- ...onInputIsLessThanRuleClassifierProvider.kt | 4 +- ...ckInputIsInRegionRuleClassifierProvider.kt | 4 +- ...tainsAtLeastOneOfRuleClassifierProvider.kt | 4 +- ...ntainAtLeastOneOfRuleClassifierProvider.kt | 4 +- ...ectionInputEqualsRuleClassifierProvider.kt | 4 +- ...tIsProperSubsetOfRuleClassifierProvider.kt | 4 +- ...ChoiceInputEqualsRuleClassifierProvider.kt | 4 +- ...ithUnitsIsEqualToRuleClassifierProvider.kt | 4 +- ...itsIsEquivalentToRuleClassifierProvider.kt | 4 +- ...putIsEquivalentToRuleClassifierProvider.kt | 4 +- ...atchesExactlyWithRuleClassifierProvider.kt | 4 +- ...vialManipulationsRuleClassifierProvider.kt | 4 +- ...umericInputEqualsRuleClassifierProvider.kt | 4 +- ...aterThanOrEqualToRuleClassifierProvider.kt | 4 +- ...nputIsGreaterThanRuleClassifierProvider.kt | 4 +- ...nclusivelyBetweenRuleClassifierProvider.kt | 4 +- ...LessThanOrEqualToRuleClassifierProvider.kt | 4 +- ...icInputIsLessThanRuleClassifierProvider.kt | 4 +- ...IsWithinToleranceRuleClassifierProvider.kt | 4 +- .../RatioInputEqualsRuleClassifierProvider.kt | 4 +- ...sNumberOfTermsEqualToClassifierProvider.kt | 4 +- ...ecificTermEqualToRuleClassifierProvider.kt | 4 +- ...InputIsEquivalentRuleClassifierProvider.kt | 4 +- ...TextInputContainsRuleClassifierProvider.kt | 9 ++- .../TextInputEqualsRuleClassifierProvider.kt | 9 ++- ...tInputFuzzyEqualsRuleClassifierProvider.kt | 9 ++- ...xtInputStartsWithRuleClassifierProvider.kt | 9 ++- ...tXAtPositionYRuleClassifierProviderTest.kt | 22 +++---- ...eforeElementYRuleClassifierProviderTest.kt | 20 +++--- ...IsEqualToOrderingClassifierProviderTest.kt | 14 ++--- ...IncorrectPositionClassifierProviderTest.kt | 14 ++--- ...inatorEqualToRuleClassifierProviderTest.kt | 14 ++--- ...xactlyEqualToRuleClassifierProviderTest.kt | 22 +++---- ...erPartEqualToRuleClassifierProviderTest.kt | 44 ++++++------- ...ractionalPartRuleClassifierProviderTest.kt | 18 +++--- ...eratorEqualToRuleClassifierProviderTest.kt | 16 ++--- ...nSimplestFormRuleClassifierProviderTest.kt | 32 +++++----- ...sEquivalentToRuleClassifierProviderTest.kt | 26 ++++---- ...xactlyEqualToRuleClassifierProviderTest.kt | 26 ++++---- ...IsGreaterThanRuleClassifierProviderTest.kt | 54 ++++++++-------- ...putIsLessThanRuleClassifierProviderTest.kt | 54 ++++++++-------- ...putIsInRegionRuleClassifierProviderTest.kt | 12 ++-- ...sAtLeastOneOfRuleClassifierProviderTest.kt | 14 ++--- ...nAtLeastOneOfRuleClassifierProviderTest.kt | 24 +++---- ...onInputEqualsRuleClassifierProviderTest.kt | 20 +++--- ...roperSubsetOfRuleClassifierProviderTest.kt | 26 ++++---- ...ceInputEqualsRuleClassifierProviderTest.kt | 12 ++-- ...nitsIsEqualToRuleClassifierProviderTest.kt | 16 ++--- ...sEquivalentToRuleClassifierProviderTest.kt | 18 +++--- ...icInputEqualsRuleClassifierProviderTest.kt | 20 +++--- ...ThanOrEqualToRuleClassifierProviderTest.kt | 26 ++++---- ...IsGreaterThanRuleClassifierProviderTest.kt | 26 ++++---- ...sivelyBetweenRuleClassifierProviderTest.kt | 38 ++++++------ ...ThanOrEqualToRuleClassifierProviderTest.kt | 30 ++++----- ...putIsLessThanRuleClassifierProviderTest.kt | 30 ++++----- ...thinToleranceRuleClassifierProviderTest.kt | 62 +++++++++---------- ...ioInputEqualsRuleClassifierProviderTest.kt | 12 ++-- ...berOfTermsEqualToClassifierProviderTest.kt | 10 +-- ...icTermEqualToRuleClassifierProviderTest.kt | 32 +++++----- ...tIsEquivalentRuleClassifierProviderTest.kt | 18 +++--- ...InputContainsRuleClassifierProviderTest.kt | 48 +++++++------- ...xtInputEqualsRuleClassifierProviderTest.kt | 40 +++++++----- ...utFuzzyEqualsRuleClassifierProviderTest.kt | 58 ++++++++++------- ...putStartsWithRuleClassifierProviderTest.kt | 52 +++++++++------- 81 files changed, 658 insertions(+), 610 deletions(-) create mode 100644 domain/src/main/java/org/oppia/android/domain/classify/ClassificationContext.kt diff --git a/domain/src/main/java/org/oppia/android/domain/classify/AnswerClassificationController.kt b/domain/src/main/java/org/oppia/android/domain/classify/AnswerClassificationController.kt index 7281ff50485..55eb6023584 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/AnswerClassificationController.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/AnswerClassificationController.kt @@ -36,13 +36,14 @@ class AnswerClassificationController @Inject constructor( "expected one of: ${interactionClassifiers.keys}" } // TODO(#207): Add support for additional classification types. + interaction.customizationArgsMap return classifyAnswer( answer, interaction.answerGroupsList, interaction.defaultOutcome, interactionClassifier, interaction.id, - writtenTranslationContext + ClassificationContext(writtenTranslationContext, interaction.customizationArgsMap) ) } @@ -54,7 +55,7 @@ class AnswerClassificationController @Inject constructor( defaultOutcome: Outcome, interactionClassifier: InteractionClassifier, interactionId: String, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): ClassificationResult { for (answerGroup in answerGroups) { for (ruleSpec in answerGroup.ruleSpecsList) { @@ -65,7 +66,7 @@ class AnswerClassificationController @Inject constructor( " has: ${interactionClassifier.getRuleTypes()}" } try { - if (ruleClassifier.matches(answer, ruleSpec.inputMap, writtenTranslationContext)) { + if (ruleClassifier.matches(answer, ruleSpec.inputMap, classificationContext)) { // Explicit classification matched. return if (!answerGroup.hasTaggedSkillMisconception()) { ClassificationResult.OutcomeOnly(answerGroup.outcome) diff --git a/domain/src/main/java/org/oppia/android/domain/classify/ClassificationContext.kt b/domain/src/main/java/org/oppia/android/domain/classify/ClassificationContext.kt new file mode 100644 index 00000000000..01415330ce2 --- /dev/null +++ b/domain/src/main/java/org/oppia/android/domain/classify/ClassificationContext.kt @@ -0,0 +1,10 @@ +package org.oppia.android.domain.classify + +import org.oppia.android.app.model.SchemaObject +import org.oppia.android.app.model.WrittenTranslationContext + +data class ClassificationContext( + val writtenTranslationContext: WrittenTranslationContext = + WrittenTranslationContext.getDefaultInstance(), + val customizationArgs: Map = mapOf() +) diff --git a/domain/src/main/java/org/oppia/android/domain/classify/RuleClassifier.kt b/domain/src/main/java/org/oppia/android/domain/classify/RuleClassifier.kt index f4fba3fc301..973b97e4fb2 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/RuleClassifier.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/RuleClassifier.kt @@ -1,7 +1,6 @@ package org.oppia.android.domain.classify import org.oppia.android.app.model.InteractionObject -import org.oppia.android.app.model.WrittenTranslationContext /** An answer classifier for a specific interaction rule. */ interface RuleClassifier { @@ -12,6 +11,6 @@ interface RuleClassifier { fun matches( answer: InteractionObject, inputs: Map, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/GenericRuleClassifier.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/GenericRuleClassifier.kt index 95f81ee1e9b..ed6efda5b4a 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/GenericRuleClassifier.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/GenericRuleClassifier.kt @@ -1,7 +1,7 @@ package org.oppia.android.domain.classify.rules import org.oppia.android.app.model.InteractionObject -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import javax.inject.Inject @@ -15,15 +15,15 @@ import javax.inject.Inject */ // TODO(#1580): Re-restrict access using Bazel visibilities class GenericRuleClassifier constructor( - val expectedAnswerObjectType: InteractionObject.ObjectTypeCase, - val orderedExpectedParameterTypes: LinkedHashMap< + private val expectedAnswerObjectType: InteractionObject.ObjectTypeCase, + private val orderedExpectedParameterTypes: LinkedHashMap< String, InteractionObject.ObjectTypeCase>, - val matcherDelegate: MatcherDelegate + private val matcherDelegate: MatcherDelegate ) : RuleClassifier { override fun matches( answer: InteractionObject, inputs: Map, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean { check(answer.objectTypeCase == expectedAnswerObjectType) { "Expected answer to be of type ${expectedAnswerObjectType.name} " + @@ -34,7 +34,7 @@ class GenericRuleClassifier constructor( .map { (parameterName, expectedObjectType) -> retrieveInputObject(parameterName, expectedObjectType, inputs) } - return matcherDelegate.matches(answer, parameterInputs, writtenTranslationContext) + return matcherDelegate.matches(answer, parameterInputs, classificationContext) } private fun retrieveInputObject( @@ -58,7 +58,7 @@ class GenericRuleClassifier constructor( * Returns whether the validated and extracted answer matches the expectations per the * specification of this classifier. */ - fun matches(answer: T, writtenTranslationContext: WrittenTranslationContext): Boolean + fun matches(answer: T, classificationContext: ClassificationContext): Boolean } interface SingleInputMatcher { @@ -66,7 +66,7 @@ class GenericRuleClassifier constructor( * Returns whether the validated and extracted answer matches the single validated and extracted * input parameter per the specification of this classifier. */ - fun matches(answer: T, input: T, writtenTranslationContext: WrittenTranslationContext): Boolean + fun matches(answer: T, input: T, classificationContext: ClassificationContext): Boolean } interface MultiTypeSingleInputMatcher { @@ -74,11 +74,7 @@ class GenericRuleClassifier constructor( * Returns whether the validated and extracted answer matches the single validated and extracted * input parameter per the specification of this classifier. */ - fun matches( - answer: AT, - input: IT, - writtenTranslationContext: WrittenTranslationContext - ): Boolean + fun matches(answer: AT, input: IT, classificationContext: ClassificationContext): Boolean } interface MultiTypeDoubleInputMatcher { @@ -90,7 +86,7 @@ class GenericRuleClassifier constructor( answer: AT, firstInput: ITF, secondInput: ITS, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean } @@ -103,7 +99,7 @@ class GenericRuleClassifier constructor( answer: T, firstInput: T, secondInput: T, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean } @@ -112,7 +108,7 @@ class GenericRuleClassifier constructor( abstract fun matches( answer: InteractionObject, inputs: List, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean class NoInputMatcherDelegate( @@ -122,10 +118,10 @@ class GenericRuleClassifier constructor( override fun matches( answer: InteractionObject, inputs: List, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean { check(inputs.isEmpty()) - return matcher.matches(extractObject(answer), writtenTranslationContext) + return matcher.matches(extractObject(answer), classificationContext) } } @@ -136,11 +132,11 @@ class GenericRuleClassifier constructor( override fun matches( answer: InteractionObject, inputs: List, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean { check(inputs.size == 1) return matcher.matches( - extractObject(answer), extractObject(inputs.first()), writtenTranslationContext + extractObject(answer), extractObject(inputs.first()), classificationContext ) } } @@ -153,11 +149,11 @@ class GenericRuleClassifier constructor( override fun matches( answer: InteractionObject, inputs: List, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean { check(inputs.size == 1) return matcher.matches( - extractAnswerObject(answer), extractInputObject(inputs.first()), writtenTranslationContext + extractAnswerObject(answer), extractInputObject(inputs.first()), classificationContext ) } } @@ -169,14 +165,14 @@ class GenericRuleClassifier constructor( override fun matches( answer: InteractionObject, inputs: List, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean { check(inputs.size == 2) return matcher.matches( extractObject(answer), extractObject(inputs[0]), extractObject(inputs[1]), - writtenTranslationContext + classificationContext ) } } @@ -190,14 +186,14 @@ class GenericRuleClassifier constructor( override fun matches( answer: InteractionObject, inputs: List, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean { check(inputs.size == 2) return matcher.matches( extractAnswerObject(answer), extractFirstParamObject(inputs[0]), extractSecondParamObject(inputs[1]), - writtenTranslationContext + classificationContext ) } } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputHasElementXAtPositionYClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputHasElementXAtPositionYClassifierProvider.kt index 72affc7d851..b6388896d97 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputHasElementXAtPositionYClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputHasElementXAtPositionYClassifierProvider.kt @@ -5,7 +5,7 @@ import org.oppia.android.app.model.InteractionObject.ObjectTypeCase.NON_NEGATIVE import org.oppia.android.app.model.InteractionObject.ObjectTypeCase.TRANSLATABLE_HTML_CONTENT_ID import org.oppia.android.app.model.ListOfSetsOfTranslatableHtmlContentIds import org.oppia.android.app.model.TranslatableHtmlContentId -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -45,7 +45,7 @@ class DragDropSortInputHasElementXAtPositionYClassifierProvider @Inject construc answer: ListOfContentIdSets1, firstInput: ContentId1, secondInput: Int, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean { // Note that the '1' returned here is to have consistency with the web platform: matched indexes // start at 1 rather than 0 to make the indexes more human friendly. diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputHasElementXBeforeElementYClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputHasElementXBeforeElementYClassifierProvider.kt index 41fa56e9bcd..2c79702d5c9 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputHasElementXBeforeElementYClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputHasElementXBeforeElementYClassifierProvider.kt @@ -4,7 +4,7 @@ import org.oppia.android.app.model.InteractionObject.ObjectTypeCase.LIST_OF_SETS import org.oppia.android.app.model.InteractionObject.ObjectTypeCase.TRANSLATABLE_HTML_CONTENT_ID import org.oppia.android.app.model.ListOfSetsOfTranslatableHtmlContentIds import org.oppia.android.app.model.TranslatableHtmlContentId -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -44,7 +44,7 @@ class DragDropSortInputHasElementXBeforeElementYClassifierProvider @Inject const answer: ListOfContentIdSets2, firstInput: ContentId2, secondInput: ContentId2, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean { val answerSets = answer.contentIdListsList.map { it.getContentIdSet() } return answerSets.indexOfFirst { diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputIsEqualToOrderingClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputIsEqualToOrderingClassifierProvider.kt index 18b8335d51c..60fdbd5d66d 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputIsEqualToOrderingClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputIsEqualToOrderingClassifierProvider.kt @@ -3,7 +3,7 @@ package org.oppia.android.domain.classify.rules.dragAndDropSortInput import org.oppia.android.app.model.InteractionObject.ObjectTypeCase.LIST_OF_SETS_OF_TRANSLATABLE_HTML_CONTENT_IDS import org.oppia.android.app.model.ListOfSetsOfTranslatableHtmlContentIds import org.oppia.android.app.model.SetOfTranslatableHtmlContentIds -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -34,7 +34,7 @@ class DragDropSortInputIsEqualToOrderingClassifierProvider @Inject constructor( override fun matches( answer: ListOfSetsOfTranslatableHtmlContentIds, input: ListOfSetsOfTranslatableHtmlContentIds, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean = areListOfSetsOfHtmlStringsEqual(answer, input) /** diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputIsEqualToOrderingWithOneItemAtIncorrectPositionClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputIsEqualToOrderingWithOneItemAtIncorrectPositionClassifierProvider.kt index acf28424dfe..64dcae58082 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputIsEqualToOrderingWithOneItemAtIncorrectPositionClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputIsEqualToOrderingWithOneItemAtIncorrectPositionClassifierProvider.kt @@ -2,7 +2,7 @@ package org.oppia.android.domain.classify.rules.dragAndDropSortInput import org.oppia.android.app.model.InteractionObject.ObjectTypeCase.LIST_OF_SETS_OF_TRANSLATABLE_HTML_CONTENT_IDS import org.oppia.android.app.model.ListOfSetsOfTranslatableHtmlContentIds -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -33,7 +33,7 @@ class DragDropSortInputIsEqualToOrderingWithOneItemAtIncorrectPositionClassifier override fun matches( answer: ListOfSetsOfTranslatableHtmlContentIds, input: ListOfSetsOfTranslatableHtmlContentIds, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean { val answerStringSets = answer.contentIdListsList val inputStringSets = input.contentIdListsList diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasDenominatorEqualToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasDenominatorEqualToRuleClassifierProvider.kt index 7467770aaee..1331c2d000c 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasDenominatorEqualToRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasDenominatorEqualToRuleClassifierProvider.kt @@ -2,7 +2,7 @@ package org.oppia.android.domain.classify.rules.fractioninput import org.oppia.android.app.model.Fraction import org.oppia.android.app.model.InteractionObject -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -31,7 +31,7 @@ class FractionInputHasDenominatorEqualToRuleClassifierProvider @Inject construct override fun matches( answer: Fraction, input: Int, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean { return answer.denominator == input } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasFractionalPartExactlyEqualToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasFractionalPartExactlyEqualToRuleClassifierProvider.kt index 5daf835e119..e8e75bce46d 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasFractionalPartExactlyEqualToRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasFractionalPartExactlyEqualToRuleClassifierProvider.kt @@ -2,7 +2,7 @@ package org.oppia.android.domain.classify.rules.fractioninput import org.oppia.android.app.model.Fraction import org.oppia.android.app.model.InteractionObject -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -31,7 +31,7 @@ class FractionInputHasFractionalPartExactlyEqualToRuleClassifierProvider override fun matches( answer: Fraction, input: Fraction, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean { return answer.numerator == input.numerator && answer.denominator == input.denominator } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasIntegerPartEqualToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasIntegerPartEqualToRuleClassifierProvider.kt index cdf8425ec19..81b953e27ca 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasIntegerPartEqualToRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasIntegerPartEqualToRuleClassifierProvider.kt @@ -2,7 +2,7 @@ package org.oppia.android.domain.classify.rules.fractioninput import org.oppia.android.app.model.Fraction import org.oppia.android.app.model.InteractionObject -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -31,7 +31,7 @@ class FractionInputHasIntegerPartEqualToRuleClassifierProvider @Inject construct override fun matches( answer: Fraction, input: Int, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean { return answer.wholeNumber == input } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasNoFractionalPartRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasNoFractionalPartRuleClassifierProvider.kt index 30b63ee8020..d47a4ea43cd 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasNoFractionalPartRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasNoFractionalPartRuleClassifierProvider.kt @@ -2,7 +2,7 @@ package org.oppia.android.domain.classify.rules.fractioninput import org.oppia.android.app.model.Fraction import org.oppia.android.app.model.InteractionObject -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -28,7 +28,7 @@ class FractionInputHasNoFractionalPartRuleClassifierProvider @Inject constructor override fun matches( answer: Fraction, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean { return answer.numerator == 0 } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasNumeratorEqualToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasNumeratorEqualToRuleClassifierProvider.kt index ac3da4c4d15..fa56ec057b5 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasNumeratorEqualToRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasNumeratorEqualToRuleClassifierProvider.kt @@ -2,7 +2,7 @@ package org.oppia.android.domain.classify.rules.fractioninput import org.oppia.android.app.model.Fraction import org.oppia.android.app.model.InteractionObject -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -31,7 +31,7 @@ class FractionInputHasNumeratorEqualToRuleClassifierProvider @Inject constructor override fun matches( answer: Fraction, input: Int, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean { return answer.numerator == input } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProvider.kt index f9498f7d965..9cb2dca6aac 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProvider.kt @@ -2,7 +2,7 @@ package org.oppia.android.domain.classify.rules.fractioninput import org.oppia.android.app.model.Fraction import org.oppia.android.app.model.InteractionObject -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -34,7 +34,7 @@ class FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProvider override fun matches( answer: Fraction, input: Fraction, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean { return answer.toDouble().approximatelyEquals(input.toDouble()) && answer == input.toSimplestForm() diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToRuleClassifierProvider.kt index e2c42f7ec67..8d745d5e3df 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToRuleClassifierProvider.kt @@ -2,7 +2,7 @@ package org.oppia.android.domain.classify.rules.fractioninput import org.oppia.android.app.model.Fraction import org.oppia.android.app.model.InteractionObject -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -32,7 +32,7 @@ class FractionInputIsEquivalentToRuleClassifierProvider @Inject constructor( override fun matches( answer: Fraction, input: Fraction, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean { return answer.toDouble().approximatelyEquals(input.toDouble()) } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsExactlyEqualToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsExactlyEqualToRuleClassifierProvider.kt index 628824681c2..eaf41d88008 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsExactlyEqualToRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsExactlyEqualToRuleClassifierProvider.kt @@ -2,7 +2,7 @@ package org.oppia.android.domain.classify.rules.fractioninput import org.oppia.android.app.model.Fraction import org.oppia.android.app.model.InteractionObject -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -30,7 +30,7 @@ class FractionInputIsExactlyEqualToRuleClassifierProvider @Inject constructor( override fun matches( answer: Fraction, input: Fraction, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean { return answer == input } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsGreaterThanRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsGreaterThanRuleClassifierProvider.kt index 89d83f1e3d6..05d4936965c 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsGreaterThanRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsGreaterThanRuleClassifierProvider.kt @@ -2,7 +2,7 @@ package org.oppia.android.domain.classify.rules.fractioninput import org.oppia.android.app.model.Fraction import org.oppia.android.app.model.InteractionObject -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -31,7 +31,7 @@ class FractionInputIsGreaterThanRuleClassifierProvider @Inject constructor( override fun matches( answer: Fraction, input: Fraction, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean { return answer.toDouble() > input.toDouble() } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsLessThanRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsLessThanRuleClassifierProvider.kt index 02d4b9766c9..b4e0fde2e20 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsLessThanRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsLessThanRuleClassifierProvider.kt @@ -2,7 +2,7 @@ package org.oppia.android.domain.classify.rules.fractioninput import org.oppia.android.app.model.Fraction import org.oppia.android.app.model.InteractionObject -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -31,7 +31,7 @@ class FractionInputIsLessThanRuleClassifierProvider @Inject constructor( override fun matches( answer: Fraction, input: Fraction, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean { return answer.toDouble() < input.toDouble() } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/imageClickInput/ImageClickInputIsInRegionRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/imageClickInput/ImageClickInputIsInRegionRuleClassifierProvider.kt index 3504fdaa68e..4f13484722f 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/imageClickInput/ImageClickInputIsInRegionRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/imageClickInput/ImageClickInputIsInRegionRuleClassifierProvider.kt @@ -2,7 +2,7 @@ package org.oppia.android.domain.classify.rules.imageClickInput import org.oppia.android.app.model.ClickOnImage import org.oppia.android.app.model.InteractionObject -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -31,7 +31,7 @@ class ImageClickInputIsInRegionRuleClassifierProvider @Inject constructor( override fun matches( answer: ClickOnImage, input: String, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean { return answer.clickedRegionsList.indexOf(input) != -1 } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputContainsAtLeastOneOfRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputContainsAtLeastOneOfRuleClassifierProvider.kt index f434bdd263c..f84e30b9b32 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputContainsAtLeastOneOfRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputContainsAtLeastOneOfRuleClassifierProvider.kt @@ -2,7 +2,7 @@ package org.oppia.android.domain.classify.rules.itemselectioninput import org.oppia.android.app.model.InteractionObject import org.oppia.android.app.model.SetOfTranslatableHtmlContentIds -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -32,6 +32,6 @@ class ItemSelectionInputContainsAtLeastOneOfRuleClassifierProvider @Inject const override fun matches( answer: SetOfTranslatableHtmlContentIds, input: SetOfTranslatableHtmlContentIds, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean = answer.getContentIdSet().intersect(input.getContentIdSet()).isNotEmpty() } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputDoesNotContainAtLeastOneOfRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputDoesNotContainAtLeastOneOfRuleClassifierProvider.kt index 5dbe1330c19..e3d306c9afc 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputDoesNotContainAtLeastOneOfRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputDoesNotContainAtLeastOneOfRuleClassifierProvider.kt @@ -2,7 +2,7 @@ package org.oppia.android.domain.classify.rules.itemselectioninput import org.oppia.android.app.model.InteractionObject import org.oppia.android.app.model.SetOfTranslatableHtmlContentIds -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -33,6 +33,6 @@ class ItemSelectionInputDoesNotContainAtLeastOneOfRuleClassifierProvider override fun matches( answer: SetOfTranslatableHtmlContentIds, input: SetOfTranslatableHtmlContentIds, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean = answer.getContentIdSet().intersect(input.getContentIdSet()).isEmpty() } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputEqualsRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputEqualsRuleClassifierProvider.kt index c447df24546..4227906dbaf 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputEqualsRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputEqualsRuleClassifierProvider.kt @@ -2,7 +2,7 @@ package org.oppia.android.domain.classify.rules.itemselectioninput import org.oppia.android.app.model.InteractionObject import org.oppia.android.app.model.SetOfTranslatableHtmlContentIds -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -32,6 +32,6 @@ class ItemSelectionInputEqualsRuleClassifierProvider @Inject constructor( override fun matches( answer: SetOfTranslatableHtmlContentIds, input: SetOfTranslatableHtmlContentIds, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean = answer.getContentIdSet() == input.getContentIdSet() } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputIsProperSubsetOfRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputIsProperSubsetOfRuleClassifierProvider.kt index ba14381872a..c7a35286733 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputIsProperSubsetOfRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputIsProperSubsetOfRuleClassifierProvider.kt @@ -2,7 +2,7 @@ package org.oppia.android.domain.classify.rules.itemselectioninput import org.oppia.android.app.model.InteractionObject import org.oppia.android.app.model.SetOfTranslatableHtmlContentIds -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -32,7 +32,7 @@ class ItemSelectionInputIsProperSubsetOfRuleClassifierProvider @Inject construct override fun matches( answer: SetOfTranslatableHtmlContentIds, input: SetOfTranslatableHtmlContentIds, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean { val answerSet = answer.getContentIdSet() val inputSet = input.getContentIdSet() diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/multiplechoiceinput/MultipleChoiceInputEqualsRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/multiplechoiceinput/MultipleChoiceInputEqualsRuleClassifierProvider.kt index 8814f50fee4..09254a65821 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/multiplechoiceinput/MultipleChoiceInputEqualsRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/multiplechoiceinput/MultipleChoiceInputEqualsRuleClassifierProvider.kt @@ -1,7 +1,7 @@ package org.oppia.android.domain.classify.rules.multiplechoiceinput import org.oppia.android.app.model.InteractionObject -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -29,6 +29,6 @@ class MultipleChoiceInputEqualsRuleClassifierProvider @Inject constructor( override fun matches( answer: Int, input: Int, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean = answer == input } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEqualToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEqualToRuleClassifierProvider.kt index 9a225cc41ca..004168c1d1c 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEqualToRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEqualToRuleClassifierProvider.kt @@ -3,7 +3,7 @@ package org.oppia.android.domain.classify.rules.numberwithunits import org.oppia.android.app.model.Fraction import org.oppia.android.app.model.InteractionObject import org.oppia.android.app.model.NumberWithUnits -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -33,7 +33,7 @@ class NumberWithUnitsIsEqualToRuleClassifierProvider @Inject constructor( override fun matches( answer: NumberWithUnits, input: NumberWithUnits, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean { // The number types must match. if (answer.numberTypeCase != input.numberTypeCase) { diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEquivalentToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEquivalentToRuleClassifierProvider.kt index e94fc9191e7..d3a8bfae04a 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEquivalentToRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEquivalentToRuleClassifierProvider.kt @@ -2,7 +2,7 @@ package org.oppia.android.domain.classify.rules.numberwithunits import org.oppia.android.app.model.InteractionObject import org.oppia.android.app.model.NumberWithUnits -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -33,7 +33,7 @@ class NumberWithUnitsIsEquivalentToRuleClassifierProvider @Inject constructor( override fun matches( answer: NumberWithUnits, input: NumberWithUnits, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean { // Units must match, but in different orders is fine. if (answer.unitList.toSet() != input.unitList.toSet()) { diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProvider.kt index ecf89663802..6af1e922742 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProvider.kt @@ -2,7 +2,7 @@ package org.oppia.android.domain.classify.rules.numericexpressioninput import org.oppia.android.app.model.InteractionObject import org.oppia.android.app.model.Polynomial -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -27,7 +27,7 @@ class NumericExpressionInputIsEquivalentToRuleClassifierProvider @Inject constru override fun matches( answer: String, input: String, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean { val answerExpression = parsePolynomial(answer) ?: return false val inputExpression = parsePolynomial(input) ?: return false diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProvider.kt index 9ce52a59519..4769660a921 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProvider.kt @@ -2,7 +2,7 @@ package org.oppia.android.domain.classify.rules.numericexpressioninput import org.oppia.android.app.model.InteractionObject import org.oppia.android.app.model.MathExpression -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -26,7 +26,7 @@ class NumericExpressionInputMatchesExactlyWithRuleClassifierProvider @Inject con override fun matches( answer: String, input: String, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean { val answerExpression = parseNumericExpression(answer) ?: return false val inputExpression = parseNumericExpression(input) ?: return false diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt index d1ea260e948..60793efa071 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt @@ -2,7 +2,7 @@ package org.oppia.android.domain.classify.rules.numericexpressioninput import org.oppia.android.app.model.ComparableOperationList import org.oppia.android.app.model.InteractionObject -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -28,7 +28,7 @@ class NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvide override fun matches( answer: String, input: String, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean { val answerExpression = parseComparableOperationList(answer) ?: return false val inputExpression = parseComparableOperationList(input) ?: return false diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputEqualsRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputEqualsRuleClassifierProvider.kt index 2c7a6dc5212..7d0f45b793c 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputEqualsRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputEqualsRuleClassifierProvider.kt @@ -1,7 +1,7 @@ package org.oppia.android.domain.classify.rules.numericinput import org.oppia.android.app.model.InteractionObject -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -29,6 +29,6 @@ class NumericInputEqualsRuleClassifierProvider @Inject constructor( override fun matches( answer: Double, input: Double, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean = input.approximatelyEquals(answer) } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsGreaterThanOrEqualToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsGreaterThanOrEqualToRuleClassifierProvider.kt index 09021d836e7..dabdb9c762e 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsGreaterThanOrEqualToRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsGreaterThanOrEqualToRuleClassifierProvider.kt @@ -1,7 +1,7 @@ package org.oppia.android.domain.classify.rules.numericinput import org.oppia.android.app.model.InteractionObject -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -28,6 +28,6 @@ class NumericInputIsGreaterThanOrEqualToRuleClassifierProvider @Inject construct override fun matches( answer: Double, input: Double, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean = answer >= input } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsGreaterThanRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsGreaterThanRuleClassifierProvider.kt index e62a916aa48..41b5532ca13 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsGreaterThanRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsGreaterThanRuleClassifierProvider.kt @@ -1,7 +1,7 @@ package org.oppia.android.domain.classify.rules.numericinput import org.oppia.android.app.model.InteractionObject -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -28,6 +28,6 @@ class NumericInputIsGreaterThanRuleClassifierProvider @Inject constructor( override fun matches( answer: Double, input: Double, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean = answer > input } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsInclusivelyBetweenRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsInclusivelyBetweenRuleClassifierProvider.kt index 052e5ebc60d..2c8e664402a 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsInclusivelyBetweenRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsInclusivelyBetweenRuleClassifierProvider.kt @@ -1,7 +1,7 @@ package org.oppia.android.domain.classify.rules.numericinput import org.oppia.android.app.model.InteractionObject -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -31,6 +31,6 @@ class NumericInputIsInclusivelyBetweenRuleClassifierProvider @Inject constructor answer: Double, firstInput: Double, secondInput: Double, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean = answer in firstInput..secondInput } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsLessThanOrEqualToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsLessThanOrEqualToRuleClassifierProvider.kt index 034b8f61f40..3dc1ddce52c 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsLessThanOrEqualToRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsLessThanOrEqualToRuleClassifierProvider.kt @@ -1,7 +1,7 @@ package org.oppia.android.domain.classify.rules.numericinput import org.oppia.android.app.model.InteractionObject -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -28,6 +28,6 @@ class NumericInputIsLessThanOrEqualToRuleClassifierProvider @Inject constructor( override fun matches( answer: Double, input: Double, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean = answer <= input } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsLessThanRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsLessThanRuleClassifierProvider.kt index 26d56bcaa86..3d5e9cf53ff 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsLessThanRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsLessThanRuleClassifierProvider.kt @@ -1,7 +1,7 @@ package org.oppia.android.domain.classify.rules.numericinput import org.oppia.android.app.model.InteractionObject -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -28,6 +28,6 @@ class NumericInputIsLessThanRuleClassifierProvider @Inject constructor( override fun matches( answer: Double, input: Double, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean = answer < input } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsWithinToleranceRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsWithinToleranceRuleClassifierProvider.kt index ab830d2e3ff..3b3980587a4 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsWithinToleranceRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsWithinToleranceRuleClassifierProvider.kt @@ -1,7 +1,7 @@ package org.oppia.android.domain.classify.rules.numericinput import org.oppia.android.app.model.InteractionObject -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -31,6 +31,6 @@ class NumericInputIsWithinToleranceRuleClassifierProvider @Inject constructor( answer: Double, firstInput: Double, secondInput: Double, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean = answer in (firstInput - secondInput)..(firstInput + secondInput) } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputEqualsRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputEqualsRuleClassifierProvider.kt index c8fbe57a790..5165c781c02 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputEqualsRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputEqualsRuleClassifierProvider.kt @@ -2,7 +2,7 @@ package org.oppia.android.domain.classify.rules.ratioinput import org.oppia.android.app.model.InteractionObject import org.oppia.android.app.model.RatioExpression -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -28,6 +28,6 @@ class RatioInputEqualsRuleClassifierProvider @Inject constructor( override fun matches( answer: RatioExpression, input: RatioExpression, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean = answer.ratioComponentList == input.ratioComponentList } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputHasNumberOfTermsEqualToClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputHasNumberOfTermsEqualToClassifierProvider.kt index 0506de932c3..bdf58bd8d1f 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputHasNumberOfTermsEqualToClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputHasNumberOfTermsEqualToClassifierProvider.kt @@ -2,7 +2,7 @@ package org.oppia.android.domain.classify.rules.ratioinput import org.oppia.android.app.model.InteractionObject import org.oppia.android.app.model.RatioExpression -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -29,6 +29,6 @@ class RatioInputHasNumberOfTermsEqualToClassifierProvider @Inject constructor( override fun matches( answer: RatioExpression, input: Int, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean = answer.ratioComponentCount == input } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputHasSpecificTermEqualToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputHasSpecificTermEqualToRuleClassifierProvider.kt index 749e8bef6a4..75ecb50355f 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputHasSpecificTermEqualToRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputHasSpecificTermEqualToRuleClassifierProvider.kt @@ -2,7 +2,7 @@ package org.oppia.android.domain.classify.rules.ratioinput import org.oppia.android.app.model.InteractionObject import org.oppia.android.app.model.RatioExpression -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -33,6 +33,6 @@ class RatioInputHasSpecificTermEqualToRuleClassifierProvider @Inject constructor answer: RatioExpression, firstInput: Int, secondInput: Int, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean = answer.ratioComponentList.getOrNull(firstInput - 1) == secondInput } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputIsEquivalentRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputIsEquivalentRuleClassifierProvider.kt index f9b9b2e9df8..c21fcc0944c 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputIsEquivalentRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputIsEquivalentRuleClassifierProvider.kt @@ -2,7 +2,7 @@ package org.oppia.android.domain.classify.rules.ratioinput import org.oppia.android.app.model.InteractionObject import org.oppia.android.app.model.RatioExpression -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -29,6 +29,6 @@ class RatioInputIsEquivalentRuleClassifierProvider @Inject constructor( override fun matches( answer: RatioExpression, input: RatioExpression, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean = answer.toSimplestForm() == input.toSimplestForm() } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/TextInputContainsRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/TextInputContainsRuleClassifierProvider.kt index 76b65756b5b..853b513da31 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/TextInputContainsRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/TextInputContainsRuleClassifierProvider.kt @@ -2,7 +2,7 @@ package org.oppia.android.domain.classify.rules.textinput import org.oppia.android.app.model.InteractionObject import org.oppia.android.app.model.TranslatableSetOfNormalizedString -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -37,10 +37,13 @@ class TextInputContainsRuleClassifierProvider @Inject constructor( override fun matches( answer: String, input: TranslatableSetOfNormalizedString, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean { val normalizedAnswer = machineLocale.run { answer.normalizeWhitespace().toMachineLowerCase() } - val inputStringList = translationController.extractStringList(input, writtenTranslationContext) + val inputStringList = + translationController.extractStringList( + input, classificationContext.writtenTranslationContext + ) return inputStringList.any { normalizedAnswer.contains(machineLocale.run { it.normalizeWhitespace().toMachineLowerCase() }) } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/TextInputEqualsRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/TextInputEqualsRuleClassifierProvider.kt index a4b705f05b8..a0c3034d6e7 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/TextInputEqualsRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/TextInputEqualsRuleClassifierProvider.kt @@ -2,7 +2,7 @@ package org.oppia.android.domain.classify.rules.textinput import org.oppia.android.app.model.InteractionObject import org.oppia.android.app.model.TranslatableSetOfNormalizedString -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -37,10 +37,13 @@ class TextInputEqualsRuleClassifierProvider @Inject constructor( override fun matches( answer: String, input: TranslatableSetOfNormalizedString, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean { val normalizedAnswer = answer.normalizeWhitespace() - val inputStringList = translationController.extractStringList(input, writtenTranslationContext) + val inputStringList = + translationController.extractStringList( + input, classificationContext.writtenTranslationContext + ) return inputStringList.any { machineLocale.run { it.normalizeWhitespace().equalsIgnoreCase(normalizedAnswer) } } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/TextInputFuzzyEqualsRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/TextInputFuzzyEqualsRuleClassifierProvider.kt index 3c69d469dec..c660a01f133 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/TextInputFuzzyEqualsRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/TextInputFuzzyEqualsRuleClassifierProvider.kt @@ -2,7 +2,7 @@ package org.oppia.android.domain.classify.rules.textinput import org.oppia.android.app.model.InteractionObject import org.oppia.android.app.model.TranslatableSetOfNormalizedString -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -37,9 +37,12 @@ class TextInputFuzzyEqualsRuleClassifierProvider @Inject constructor( override fun matches( answer: String, input: TranslatableSetOfNormalizedString, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean { - val inputStringList = translationController.extractStringList(input, writtenTranslationContext) + val inputStringList = + translationController.extractStringList( + input, classificationContext.writtenTranslationContext + ) return inputStringList.any { hasEditDistanceEqualToOne(it, answer) } } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/TextInputStartsWithRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/TextInputStartsWithRuleClassifierProvider.kt index a216eabce4c..39c6c7b83d5 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/TextInputStartsWithRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/TextInputStartsWithRuleClassifierProvider.kt @@ -2,7 +2,7 @@ package org.oppia.android.domain.classify.rules.textinput import org.oppia.android.app.model.InteractionObject import org.oppia.android.app.model.TranslatableSetOfNormalizedString -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider @@ -37,10 +37,13 @@ class TextInputStartsWithRuleClassifierProvider @Inject constructor( override fun matches( answer: String, input: TranslatableSetOfNormalizedString, - writtenTranslationContext: WrittenTranslationContext + classificationContext: ClassificationContext ): Boolean { val normalizedAnswer = machineLocale.run { answer.normalizeWhitespace().toMachineLowerCase() } - val inputStringList = translationController.extractStringList(input, writtenTranslationContext) + val inputStringList = + translationController.extractStringList( + input, classificationContext.writtenTranslationContext + ) return inputStringList.any { normalizedAnswer.startsWith( machineLocale.run { it.normalizeWhitespace().toMachineLowerCase() } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputHasElementXAtPositionYRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputHasElementXAtPositionYRuleClassifierProviderTest.kt index d73f1a71934..53be477a285 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputHasElementXAtPositionYRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputHasElementXAtPositionYRuleClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createListOfSetsOfTranslatableHtmlContentIds import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createNonNegativeInt import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createTranslatableHtmlContentId @@ -59,7 +59,7 @@ class DragDropSortInputHasElementXAtPositionYRuleClassifierProviderTest { hasElementXAtPositionYRuleClassifier.matches( answer = LIST_OF_SETS_OF_CONTENT_IDS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -79,7 +79,7 @@ class DragDropSortInputHasElementXAtPositionYRuleClassifierProviderTest { hasElementXAtPositionYRuleClassifier.matches( answer = LIST_OF_SETS_OF_CONTENT_IDS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -96,7 +96,7 @@ class DragDropSortInputHasElementXAtPositionYRuleClassifierProviderTest { hasElementXAtPositionYRuleClassifier.matches( answer = LIST_OF_SETS_OF_CONTENT_IDS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -113,7 +113,7 @@ class DragDropSortInputHasElementXAtPositionYRuleClassifierProviderTest { hasElementXAtPositionYRuleClassifier.matches( answer = LIST_OF_SETS_OF_CONTENT_IDS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -130,7 +130,7 @@ class DragDropSortInputHasElementXAtPositionYRuleClassifierProviderTest { hasElementXAtPositionYRuleClassifier.matches( answer = LIST_OF_SETS_OF_CONTENT_IDS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -147,7 +147,7 @@ class DragDropSortInputHasElementXAtPositionYRuleClassifierProviderTest { hasElementXAtPositionYRuleClassifier.matches( answer = LIST_OF_SETS_OF_CONTENT_IDS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -164,7 +164,7 @@ class DragDropSortInputHasElementXAtPositionYRuleClassifierProviderTest { hasElementXAtPositionYRuleClassifier.matches( answer = LIST_OF_SETS_OF_CONTENT_IDS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -178,7 +178,7 @@ class DragDropSortInputHasElementXAtPositionYRuleClassifierProviderTest { hasElementXAtPositionYRuleClassifier.matches( answer = LIST_OF_SETS_OF_CONTENT_IDS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -192,7 +192,7 @@ class DragDropSortInputHasElementXAtPositionYRuleClassifierProviderTest { hasElementXAtPositionYRuleClassifier.matches( answer = LIST_OF_SETS_OF_CONTENT_IDS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -206,7 +206,7 @@ class DragDropSortInputHasElementXAtPositionYRuleClassifierProviderTest { hasElementXAtPositionYRuleClassifier.matches( answer = LIST_OF_SETS_OF_CONTENT_IDS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputHasElementXBeforeElementYRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputHasElementXBeforeElementYRuleClassifierProviderTest.kt index 06ea75a0cf3..c706db71f64 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputHasElementXBeforeElementYRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputHasElementXBeforeElementYRuleClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createListOfSetsOfTranslatableHtmlContentIds import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createNonNegativeInt import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createTranslatableHtmlContentId @@ -60,7 +60,7 @@ class DragDropSortInputHasElementXBeforeElementYRuleClassifierProviderTest { hasElementXBeforeElementYRuleClassifier.matches( answer = LIST_OF_SETS_OF_CONTENT_IDS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -77,7 +77,7 @@ class DragDropSortInputHasElementXBeforeElementYRuleClassifierProviderTest { hasElementXBeforeElementYRuleClassifier.matches( answer = LIST_OF_SETS_OF_CONTENT_IDS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -94,7 +94,7 @@ class DragDropSortInputHasElementXBeforeElementYRuleClassifierProviderTest { hasElementXBeforeElementYRuleClassifier.matches( answer = LIST_OF_SETS_OF_CONTENT_IDS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -111,7 +111,7 @@ class DragDropSortInputHasElementXBeforeElementYRuleClassifierProviderTest { hasElementXBeforeElementYRuleClassifier.matches( answer = LIST_OF_SETS_OF_CONTENT_IDS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -128,7 +128,7 @@ class DragDropSortInputHasElementXBeforeElementYRuleClassifierProviderTest { hasElementXBeforeElementYRuleClassifier.matches( answer = LIST_OF_SETS_OF_CONTENT_IDS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -145,7 +145,7 @@ class DragDropSortInputHasElementXBeforeElementYRuleClassifierProviderTest { hasElementXBeforeElementYRuleClassifier.matches( answer = LIST_OF_SETS_OF_CONTENT_IDS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -162,7 +162,7 @@ class DragDropSortInputHasElementXBeforeElementYRuleClassifierProviderTest { hasElementXBeforeElementYRuleClassifier.matches( answer = LIST_OF_SETS_OF_CONTENT_IDS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -176,7 +176,7 @@ class DragDropSortInputHasElementXBeforeElementYRuleClassifierProviderTest { hasElementXBeforeElementYRuleClassifier.matches( answer = LIST_OF_SETS_OF_CONTENT_IDS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -190,7 +190,7 @@ class DragDropSortInputHasElementXBeforeElementYRuleClassifierProviderTest { hasElementXBeforeElementYRuleClassifier.matches( answer = LIST_OF_SETS_OF_CONTENT_IDS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputIsEqualToOrderingClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputIsEqualToOrderingClassifierProviderTest.kt index f6efcddc2f6..2c65122884d 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputIsEqualToOrderingClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputIsEqualToOrderingClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createListOfSetsOfTranslatableHtmlContentIds import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.testing.assertThrows @@ -71,7 +71,7 @@ class DragDropSortInputIsEqualToOrderingClassifierProviderTest { isEqualToOrderingClassifierProvider.matches( answer = LIST_OF_SETS_12_3_4, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -85,7 +85,7 @@ class DragDropSortInputIsEqualToOrderingClassifierProviderTest { isEqualToOrderingClassifierProvider.matches( answer = LIST_OF_SETS_12_3_4, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -99,7 +99,7 @@ class DragDropSortInputIsEqualToOrderingClassifierProviderTest { isEqualToOrderingClassifierProvider.matches( answer = LIST_OF_SETS_12_3_4, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -113,7 +113,7 @@ class DragDropSortInputIsEqualToOrderingClassifierProviderTest { isEqualToOrderingClassifierProvider.matches( answer = LIST_OF_SETS_12_3_4, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -127,7 +127,7 @@ class DragDropSortInputIsEqualToOrderingClassifierProviderTest { isEqualToOrderingClassifierProvider.matches( answer = LIST_OF_SETS_12_3_4, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -141,7 +141,7 @@ class DragDropSortInputIsEqualToOrderingClassifierProviderTest { isEqualToOrderingClassifierProvider.matches( answer = LIST_OF_SETS_12_3_4, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputIsEqualToOrderingWithOneItemAtIncorrectPositionClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputIsEqualToOrderingWithOneItemAtIncorrectPositionClassifierProviderTest.kt index af473238da2..d62690db395 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputIsEqualToOrderingWithOneItemAtIncorrectPositionClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/DragDropSortInputIsEqualToOrderingWithOneItemAtIncorrectPositionClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createListOfSetsOfTranslatableHtmlContentIds import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createNonNegativeInt import org.oppia.android.domain.classify.RuleClassifier @@ -73,7 +73,7 @@ class DragDropSortInputIsEqualToOrderingWithOneItemAtIncorrectPositionClassifier isEqualToOrderingWithOneItemIncorrectClassifier.matches( answer = LIST_OF_SETS_12_4_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -92,7 +92,7 @@ class DragDropSortInputIsEqualToOrderingWithOneItemAtIncorrectPositionClassifier isEqualToOrderingWithOneItemIncorrectClassifier.matches( answer = LIST_OF_SETS_12_4_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -106,7 +106,7 @@ class DragDropSortInputIsEqualToOrderingWithOneItemAtIncorrectPositionClassifier isEqualToOrderingWithOneItemIncorrectClassifier.matches( answer = LIST_OF_SETS_1_4_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -120,7 +120,7 @@ class DragDropSortInputIsEqualToOrderingWithOneItemAtIncorrectPositionClassifier isEqualToOrderingWithOneItemIncorrectClassifier.matches( answer = LIST_OF_SETS_1_4_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -134,7 +134,7 @@ class DragDropSortInputIsEqualToOrderingWithOneItemAtIncorrectPositionClassifier isEqualToOrderingWithOneItemIncorrectClassifier.matches( answer = LIST_OF_SETS_1_4_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -148,7 +148,7 @@ class DragDropSortInputIsEqualToOrderingWithOneItemAtIncorrectPositionClassifier isEqualToOrderingWithOneItemIncorrectClassifier.matches( answer = LIST_OF_SETS_12_4_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasDenominatorEqualToRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasDenominatorEqualToRuleClassifierProviderTest.kt index 3fa9315aaa0..a4641cc64ad 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasDenominatorEqualToRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasDenominatorEqualToRuleClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.testing.assertThrows @@ -71,7 +71,7 @@ class FractionInputHasDenominatorEqualToRuleClassifierProviderTest { denominatorIsEqualClassifierProvider.matches( answer = WHOLE_NUMBER_VALUE_TEST_123, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) // This should match because whole numbers have a denominator of 1 by default @@ -86,7 +86,7 @@ class FractionInputHasDenominatorEqualToRuleClassifierProviderTest { denominatorIsEqualClassifierProvider.matches( answer = WHOLE_NUMBER_VALUE_TEST_123, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -100,7 +100,7 @@ class FractionInputHasDenominatorEqualToRuleClassifierProviderTest { denominatorIsEqualClassifierProvider.matches( answer = FRACTION_VALUE_TEST_2_OVER_4, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -114,7 +114,7 @@ class FractionInputHasDenominatorEqualToRuleClassifierProviderTest { denominatorIsEqualClassifierProvider.matches( answer = FRACTION_VALUE_TEST_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -128,7 +128,7 @@ class FractionInputHasDenominatorEqualToRuleClassifierProviderTest { denominatorIsEqualClassifierProvider.matches( answer = FRACTION_VALUE_TEST_2_OVER_4, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -147,7 +147,7 @@ class FractionInputHasDenominatorEqualToRuleClassifierProviderTest { denominatorIsEqualClassifierProvider.matches( answer = FRACTION_VALUE_TEST_2_OVER_4, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasFractionalPartExactlyEqualToRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasFractionalPartExactlyEqualToRuleClassifierProviderTest.kt index af55a78b926..56212d736d9 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasFractionalPartExactlyEqualToRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasFractionalPartExactlyEqualToRuleClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.testing.assertThrows @@ -99,7 +99,7 @@ class FractionInputHasFractionalPartExactlyEqualToRuleClassifierProviderTest { fractionalPartIsExactlyEqualClassifierProvider.matches( answer = MIXED_NUMBER_VALUE_TEST_NEGATIVE_123_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -113,7 +113,7 @@ class FractionInputHasFractionalPartExactlyEqualToRuleClassifierProviderTest { fractionalPartIsExactlyEqualClassifierProvider.matches( answer = FRACTION_VALUE_TEST_NEGATIVE_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -127,7 +127,7 @@ class FractionInputHasFractionalPartExactlyEqualToRuleClassifierProviderTest { fractionalPartIsExactlyEqualClassifierProvider.matches( answer = WHOLE_NUMBER_VALUE_TEST_123, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -141,7 +141,7 @@ class FractionInputHasFractionalPartExactlyEqualToRuleClassifierProviderTest { fractionalPartIsExactlyEqualClassifierProvider.matches( answer = WHOLE_NUMBER_VALUE_TEST_321, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) // 123 and 321 match because they have the same fractional parts: 0/1. @@ -156,7 +156,7 @@ class FractionInputHasFractionalPartExactlyEqualToRuleClassifierProviderTest { fractionalPartIsExactlyEqualClassifierProvider.matches( answer = FRACTION_VALUE_TEST_2_OVER_4, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -170,7 +170,7 @@ class FractionInputHasFractionalPartExactlyEqualToRuleClassifierProviderTest { fractionalPartIsExactlyEqualClassifierProvider.matches( answer = MIXED_NUMBER_VALUE_TEST_123_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -184,7 +184,7 @@ class FractionInputHasFractionalPartExactlyEqualToRuleClassifierProviderTest { fractionalPartIsExactlyEqualClassifierProvider.matches( answer = MIXED_NUMBER_VALUE_TEST_123_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -198,7 +198,7 @@ class FractionInputHasFractionalPartExactlyEqualToRuleClassifierProviderTest { fractionalPartIsExactlyEqualClassifierProvider.matches( answer = WHOLE_NUMBER_VALUE_TEST_123, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -212,7 +212,7 @@ class FractionInputHasFractionalPartExactlyEqualToRuleClassifierProviderTest { fractionalPartIsExactlyEqualClassifierProvider.matches( answer = FRACTION_VALUE_TEST_2_OVER_4, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -231,7 +231,7 @@ class FractionInputHasFractionalPartExactlyEqualToRuleClassifierProviderTest { fractionalPartIsExactlyEqualClassifierProvider.matches( answer = FRACTION_VALUE_TEST_2_OVER_4, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasIntegerPartEqualToRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasIntegerPartEqualToRuleClassifierProviderTest.kt index a9e2bf4eda8..1ce8cbc6e94 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasIntegerPartEqualToRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasIntegerPartEqualToRuleClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder import org.oppia.android.testing.assertThrows import org.robolectric.annotation.Config @@ -146,7 +146,7 @@ class FractionInputHasIntegerPartEqualToRuleClassifierProviderTest { val matches = inputHasIntegerPartEqualToRuleClassifier.matches( answer = FRACTION_VALUE_TEST_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -159,7 +159,7 @@ class FractionInputHasIntegerPartEqualToRuleClassifierProviderTest { val matches = inputHasIntegerPartEqualToRuleClassifier.matches( answer = FRACTION_VALUE_TEST_5_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -172,7 +172,7 @@ class FractionInputHasIntegerPartEqualToRuleClassifierProviderTest { val matches = inputHasIntegerPartEqualToRuleClassifier.matches( answer = FRACTION_VALUE_TEST_5_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -185,7 +185,7 @@ class FractionInputHasIntegerPartEqualToRuleClassifierProviderTest { val matches = inputHasIntegerPartEqualToRuleClassifier.matches( answer = FRACTION_VALUE_TEST_3_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -198,7 +198,7 @@ class FractionInputHasIntegerPartEqualToRuleClassifierProviderTest { val matches = inputHasIntegerPartEqualToRuleClassifier.matches( answer = FRACTION_VALUE_TEST_3_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -211,7 +211,7 @@ class FractionInputHasIntegerPartEqualToRuleClassifierProviderTest { val matches = inputHasIntegerPartEqualToRuleClassifier.matches( answer = FRACTION_VALUE_TEST_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -224,7 +224,7 @@ class FractionInputHasIntegerPartEqualToRuleClassifierProviderTest { val matches = inputHasIntegerPartEqualToRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_123_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -237,7 +237,7 @@ class FractionInputHasIntegerPartEqualToRuleClassifierProviderTest { val matches = inputHasIntegerPartEqualToRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_0_2_OVER_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -250,7 +250,7 @@ class FractionInputHasIntegerPartEqualToRuleClassifierProviderTest { val matches = inputHasIntegerPartEqualToRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_1_2_OVER_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -263,7 +263,7 @@ class FractionInputHasIntegerPartEqualToRuleClassifierProviderTest { val matches = inputHasIntegerPartEqualToRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_1_2_OVER_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -276,7 +276,7 @@ class FractionInputHasIntegerPartEqualToRuleClassifierProviderTest { val matches = inputHasIntegerPartEqualToRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_NEGATIVE_123_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -289,7 +289,7 @@ class FractionInputHasIntegerPartEqualToRuleClassifierProviderTest { val matches = inputHasIntegerPartEqualToRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_NEGATIVE_0_2_OVER_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -302,7 +302,7 @@ class FractionInputHasIntegerPartEqualToRuleClassifierProviderTest { val matches = inputHasIntegerPartEqualToRuleClassifier.matches( answer = FRACTION_VALUE_TEST_NEGATIVE_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -315,7 +315,7 @@ class FractionInputHasIntegerPartEqualToRuleClassifierProviderTest { val matches = inputHasIntegerPartEqualToRuleClassifier.matches( answer = FRACTION_VALUE_TEST_NEGATIVE_5_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -328,7 +328,7 @@ class FractionInputHasIntegerPartEqualToRuleClassifierProviderTest { val matches = inputHasIntegerPartEqualToRuleClassifier.matches( answer = FRACTION_VALUE_TEST_NEGATIVE_5_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -341,7 +341,7 @@ class FractionInputHasIntegerPartEqualToRuleClassifierProviderTest { val matches = inputHasIntegerPartEqualToRuleClassifier.matches( answer = FRACTION_VALUE_TEST_NEGATIVE_3_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -354,7 +354,7 @@ class FractionInputHasIntegerPartEqualToRuleClassifierProviderTest { val matches = inputHasIntegerPartEqualToRuleClassifier.matches( answer = FRACTION_VALUE_TEST_NEGATIVE_3_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -366,7 +366,7 @@ class FractionInputHasIntegerPartEqualToRuleClassifierProviderTest { val matches = inputHasIntegerPartEqualToRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_NEGATIVE_1_2_OVER_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -379,7 +379,7 @@ class FractionInputHasIntegerPartEqualToRuleClassifierProviderTest { val matches = inputHasIntegerPartEqualToRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_NEGATIVE_1_2_OVER_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -394,7 +394,7 @@ class FractionInputHasIntegerPartEqualToRuleClassifierProviderTest { .matches( answer = FRACTION_VALUE_TEST_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -412,7 +412,7 @@ class FractionInputHasIntegerPartEqualToRuleClassifierProviderTest { .matches( answer = FRACTION_VALUE_TEST_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasNoFractionalPartRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasNoFractionalPartRuleClassifierProviderTest.kt index 6d8a25460f8..cb72a6d9b50 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasNoFractionalPartRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasNoFractionalPartRuleClassifierProviderTest.kt @@ -10,7 +10,7 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.oppia.android.app.model.InteractionObject -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder import org.oppia.android.domain.classify.RuleClassifier import org.robolectric.annotation.Config @@ -97,7 +97,7 @@ class FractionInputHasNoFractionalPartRuleClassifierProviderTest { hasNoFractionalPartClassifierProvider.matches( answer = WHOLE_NUMBER_VALUE_TEST_123, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -111,7 +111,7 @@ class FractionInputHasNoFractionalPartRuleClassifierProviderTest { hasNoFractionalPartClassifierProvider.matches( answer = FRACTION_VALUE_TEST_2_OVER_4, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -125,7 +125,7 @@ class FractionInputHasNoFractionalPartRuleClassifierProviderTest { hasNoFractionalPartClassifierProvider.matches( answer = MIXED_NUMBER_VALUE_TEST_123_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -139,7 +139,7 @@ class FractionInputHasNoFractionalPartRuleClassifierProviderTest { hasNoFractionalPartClassifierProvider.matches( answer = MIXED_NUMBER_VALUE_TEST_123_0_OVER_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -153,7 +153,7 @@ class FractionInputHasNoFractionalPartRuleClassifierProviderTest { hasNoFractionalPartClassifierProvider.matches( answer = MIXED_NUMBER_VALUE_TEST_NEGATIVE_123_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -167,7 +167,7 @@ class FractionInputHasNoFractionalPartRuleClassifierProviderTest { hasNoFractionalPartClassifierProvider.matches( answer = MIXED_NUMBER_VALUE_TEST_NEGATIVE_123_0_OVER_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -181,7 +181,7 @@ class FractionInputHasNoFractionalPartRuleClassifierProviderTest { hasNoFractionalPartClassifierProvider.matches( answer = FRACTION_VALUE_TEST_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -195,7 +195,7 @@ class FractionInputHasNoFractionalPartRuleClassifierProviderTest { hasNoFractionalPartClassifierProvider.matches( answer = FRACTION_VALUE_TEST_20_OVER_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasNumeratorEqualToRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasNumeratorEqualToRuleClassifierProviderTest.kt index 8b8352c78db..647d37cbdd3 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasNumeratorEqualToRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputHasNumeratorEqualToRuleClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.testing.assertThrows @@ -79,7 +79,7 @@ class FractionInputHasNumeratorEqualToRuleClassifierProviderTest { numeratorIsEqualClassifierProvider.matches( answer = FRACTION_VALUE_TEST_NEGATIVE_2_OVER_4, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -93,7 +93,7 @@ class FractionInputHasNumeratorEqualToRuleClassifierProviderTest { numeratorIsEqualClassifierProvider.matches( answer = WHOLE_NUMBER_VALUE_TEST_123, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -107,7 +107,7 @@ class FractionInputHasNumeratorEqualToRuleClassifierProviderTest { numeratorIsEqualClassifierProvider.matches( answer = FRACTION_VALUE_TEST_2_OVER_4, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -121,7 +121,7 @@ class FractionInputHasNumeratorEqualToRuleClassifierProviderTest { numeratorIsEqualClassifierProvider.matches( answer = FRACTION_VALUE_TEST_2_OVER_4, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -135,7 +135,7 @@ class FractionInputHasNumeratorEqualToRuleClassifierProviderTest { numeratorIsEqualClassifierProvider.matches( answer = FRACTION_VALUE_TEST_2_OVER_4, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -149,7 +149,7 @@ class FractionInputHasNumeratorEqualToRuleClassifierProviderTest { numeratorIsEqualClassifierProvider.matches( answer = FRACTION_VALUE_TEST_2_OVER_4, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -168,7 +168,7 @@ class FractionInputHasNumeratorEqualToRuleClassifierProviderTest { numeratorIsEqualClassifierProvider.matches( answer = FRACTION_VALUE_TEST_2_OVER_4, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProviderTest.kt index 5210a2162a1..1afbb516319 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder import org.oppia.android.testing.assertThrows import org.robolectric.annotation.Config @@ -111,7 +111,7 @@ class FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProviderTest { inputIsEquivalentToAndInSimplestFormRuleClassifier.matches( answer = answer, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -126,7 +126,7 @@ class FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProviderTest { inputIsEquivalentToAndInSimplestFormRuleClassifier.matches( answer = answer, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -141,7 +141,7 @@ class FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProviderTest { inputIsEquivalentToAndInSimplestFormRuleClassifier.matches( answer = answer, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -156,7 +156,7 @@ class FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProviderTest { inputIsEquivalentToAndInSimplestFormRuleClassifier.matches( answer = answer, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -171,7 +171,7 @@ class FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProviderTest { inputIsEquivalentToAndInSimplestFormRuleClassifier.matches( answer = answer, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -186,7 +186,7 @@ class FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProviderTest { inputIsEquivalentToAndInSimplestFormRuleClassifier.matches( answer = answer, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) // Even if creator does not input simplest form, learner's answer must still be in simplest form @@ -202,7 +202,7 @@ class FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProviderTest { inputIsEquivalentToAndInSimplestFormRuleClassifier.matches( answer = answer, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -217,7 +217,7 @@ class FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProviderTest { inputIsEquivalentToAndInSimplestFormRuleClassifier.matches( answer = answer, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -232,7 +232,7 @@ class FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProviderTest { inputIsEquivalentToAndInSimplestFormRuleClassifier.matches( answer = answer, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -247,7 +247,7 @@ class FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProviderTest { inputIsEquivalentToAndInSimplestFormRuleClassifier.matches( answer = answer, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -262,7 +262,7 @@ class FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProviderTest { inputIsEquivalentToAndInSimplestFormRuleClassifier.matches( answer = answer, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -277,7 +277,7 @@ class FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProviderTest { inputIsEquivalentToAndInSimplestFormRuleClassifier.matches( answer = answer, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -292,7 +292,7 @@ class FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProviderTest { inputIsEquivalentToAndInSimplestFormRuleClassifier.matches( answer = answer, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -310,7 +310,7 @@ class FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProviderTest { inputIsEquivalentToAndInSimplestFormRuleClassifier.matches( answer = answer, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -328,7 +328,7 @@ class FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProviderTest { inputIsEquivalentToAndInSimplestFormRuleClassifier.matches( answer = answer, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToRuleClassifierProviderTest.kt index 5453058e53b..a61d6a03b41 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToRuleClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder import org.oppia.android.testing.assertThrows import org.robolectric.annotation.Config @@ -100,7 +100,7 @@ class FractionInputIsEquivalentToRuleClassifierProviderTest { inputIsEquivalentToRuleClassifier.matches( answer = answer, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -114,7 +114,7 @@ class FractionInputIsEquivalentToRuleClassifierProviderTest { inputIsEquivalentToRuleClassifier.matches( answer = WHOLE_NUMBER_VALUE_TEST_123, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -128,7 +128,7 @@ class FractionInputIsEquivalentToRuleClassifierProviderTest { inputIsEquivalentToRuleClassifier.matches( answer = FRACTION_VALUE_TEST_2_OVER_8, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -142,7 +142,7 @@ class FractionInputIsEquivalentToRuleClassifierProviderTest { inputIsEquivalentToRuleClassifier.matches( answer = FRACTION_VALUE_TEST_2_OVER_8, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -156,7 +156,7 @@ class FractionInputIsEquivalentToRuleClassifierProviderTest { inputIsEquivalentToRuleClassifier.matches( answer = FRACTION_VALUE_TEST_1_OVER_4, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -170,7 +170,7 @@ class FractionInputIsEquivalentToRuleClassifierProviderTest { inputIsEquivalentToRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_6_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -184,7 +184,7 @@ class FractionInputIsEquivalentToRuleClassifierProviderTest { inputIsEquivalentToRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_55_1_OVER_4, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -198,7 +198,7 @@ class FractionInputIsEquivalentToRuleClassifierProviderTest { inputIsEquivalentToRuleClassifier.matches( answer = FRACTION_VALUE_TEST_13_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -212,7 +212,7 @@ class FractionInputIsEquivalentToRuleClassifierProviderTest { inputIsEquivalentToRuleClassifier.matches( answer = WHOLE_NUMBER_VALUE_TEST_254, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -226,7 +226,7 @@ class FractionInputIsEquivalentToRuleClassifierProviderTest { inputIsEquivalentToRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_55_1_OVER_4, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -240,7 +240,7 @@ class FractionInputIsEquivalentToRuleClassifierProviderTest { inputIsEquivalentToRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_6_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -254,7 +254,7 @@ class FractionInputIsEquivalentToRuleClassifierProviderTest { inputIsEquivalentToRuleClassifier.matches( answer = FRACTION_VALUE_TEST_2_OVER_8, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsExactlyEqualToRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsExactlyEqualToRuleClassifierProviderTest.kt index 083294f9ce5..5280da48f8a 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsExactlyEqualToRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsExactlyEqualToRuleClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.testing.assertThrows @@ -99,7 +99,7 @@ class FractionInputIsExactlyEqualToRuleClassifierProviderTest { isExactlyEqualClassifierProvider.matches( answer = WHOLE_NUMBER_VALUE_TEST_123, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -113,7 +113,7 @@ class FractionInputIsExactlyEqualToRuleClassifierProviderTest { isExactlyEqualClassifierProvider.matches( answer = WHOLE_NUMBER_VALUE_TEST_321, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -127,7 +127,7 @@ class FractionInputIsExactlyEqualToRuleClassifierProviderTest { isExactlyEqualClassifierProvider.matches( answer = FRACTION_VALUE_TEST_2_OVER_4, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -141,7 +141,7 @@ class FractionInputIsExactlyEqualToRuleClassifierProviderTest { isExactlyEqualClassifierProvider.matches( answer = FRACTION_VALUE_TEST_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -155,7 +155,7 @@ class FractionInputIsExactlyEqualToRuleClassifierProviderTest { isExactlyEqualClassifierProvider.matches( answer = MIXED_NUMBER_VALUE_TEST_123_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -169,7 +169,7 @@ class FractionInputIsExactlyEqualToRuleClassifierProviderTest { isExactlyEqualClassifierProvider.matches( answer = MIXED_NUMBER_VALUE_TEST_123_1_OVER_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -183,7 +183,7 @@ class FractionInputIsExactlyEqualToRuleClassifierProviderTest { isExactlyEqualClassifierProvider.matches( answer = MIXED_NUMBER_VALUE_TEST_NEGATIVE_123_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -197,7 +197,7 @@ class FractionInputIsExactlyEqualToRuleClassifierProviderTest { isExactlyEqualClassifierProvider.matches( answer = WHOLE_NUMBER_VALUE_TEST_123, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -211,7 +211,7 @@ class FractionInputIsExactlyEqualToRuleClassifierProviderTest { isExactlyEqualClassifierProvider.matches( answer = WHOLE_NUMBER_VALUE_TEST_123, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -225,7 +225,7 @@ class FractionInputIsExactlyEqualToRuleClassifierProviderTest { isExactlyEqualClassifierProvider.matches( answer = MIXED_NUMBER_VALUE_TEST_123_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -239,7 +239,7 @@ class FractionInputIsExactlyEqualToRuleClassifierProviderTest { isExactlyEqualClassifierProvider.matches( answer = FRACTION_VALUE_TEST_2_OVER_4, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -258,7 +258,7 @@ class FractionInputIsExactlyEqualToRuleClassifierProviderTest { isExactlyEqualClassifierProvider.matches( answer = FRACTION_VALUE_TEST_2_OVER_4, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsGreaterThanRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsGreaterThanRuleClassifierProviderTest.kt index 63b132c8441..b14d9993e06 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsGreaterThanRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsGreaterThanRuleClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder import org.oppia.android.testing.assertThrows import org.robolectric.annotation.Config @@ -100,7 +100,7 @@ class FractionInputIsGreaterThanRuleClassifierProviderTest { val matches = inputGreaterThanRuleClassifier.matches( answer = FRACTION_VALUE_TEST_1_OVER_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -113,7 +113,7 @@ class FractionInputIsGreaterThanRuleClassifierProviderTest { val matches = inputGreaterThanRuleClassifier.matches( answer = FRACTION_VALUE_TEST_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -126,7 +126,7 @@ class FractionInputIsGreaterThanRuleClassifierProviderTest { val matches = inputGreaterThanRuleClassifier.matches( answer = FRACTION_VALUE_TEST_1_OVER_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -139,7 +139,7 @@ class FractionInputIsGreaterThanRuleClassifierProviderTest { val matches = inputGreaterThanRuleClassifier.matches( answer = FRACTION_VALUE_TEST_1_OVER_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -152,7 +152,7 @@ class FractionInputIsGreaterThanRuleClassifierProviderTest { val matches = inputGreaterThanRuleClassifier.matches( answer = FRACTION_VALUE_TEST_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -165,7 +165,7 @@ class FractionInputIsGreaterThanRuleClassifierProviderTest { val matches = inputGreaterThanRuleClassifier.matches( answer = FRACTION_VALUE_TEST_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -178,7 +178,7 @@ class FractionInputIsGreaterThanRuleClassifierProviderTest { val matches = inputGreaterThanRuleClassifier.matches( answer = FRACTION_VALUE_TEST_NEGATIVE_1_OVER_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -191,7 +191,7 @@ class FractionInputIsGreaterThanRuleClassifierProviderTest { val matches = inputGreaterThanRuleClassifier.matches( answer = FRACTION_VALUE_TEST_NEGATIVE_1_OVER_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -204,7 +204,7 @@ class FractionInputIsGreaterThanRuleClassifierProviderTest { val matches = inputGreaterThanRuleClassifier.matches( answer = FRACTION_VALUE_TEST_NEGATIVE_1_OVER_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -217,7 +217,7 @@ class FractionInputIsGreaterThanRuleClassifierProviderTest { val matches = inputGreaterThanRuleClassifier.matches( answer = FRACTION_VALUE_TEST_NEGATIVE_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -230,7 +230,7 @@ class FractionInputIsGreaterThanRuleClassifierProviderTest { val matches = inputGreaterThanRuleClassifier.matches( answer = FRACTION_VALUE_TEST_NEGATIVE_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -243,7 +243,7 @@ class FractionInputIsGreaterThanRuleClassifierProviderTest { val matches = inputGreaterThanRuleClassifier.matches( answer = FRACTION_VALUE_TEST_NEGATIVE_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -256,7 +256,7 @@ class FractionInputIsGreaterThanRuleClassifierProviderTest { val matches = inputGreaterThanRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_123_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -269,7 +269,7 @@ class FractionInputIsGreaterThanRuleClassifierProviderTest { val matches = inputGreaterThanRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_123_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -282,7 +282,7 @@ class FractionInputIsGreaterThanRuleClassifierProviderTest { val matches = inputGreaterThanRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_123_1_OVER_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -295,7 +295,7 @@ class FractionInputIsGreaterThanRuleClassifierProviderTest { val matches = inputGreaterThanRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_123_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -308,7 +308,7 @@ class FractionInputIsGreaterThanRuleClassifierProviderTest { val matches = inputGreaterThanRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_123_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -321,7 +321,7 @@ class FractionInputIsGreaterThanRuleClassifierProviderTest { val matches = inputGreaterThanRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_123_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -334,7 +334,7 @@ class FractionInputIsGreaterThanRuleClassifierProviderTest { val matches = inputGreaterThanRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_NEGATIVE_123_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -347,7 +347,7 @@ class FractionInputIsGreaterThanRuleClassifierProviderTest { val matches = inputGreaterThanRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_NEGATIVE_123_1_OVER_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -360,7 +360,7 @@ class FractionInputIsGreaterThanRuleClassifierProviderTest { val matches = inputGreaterThanRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_NEGATIVE_123_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -373,7 +373,7 @@ class FractionInputIsGreaterThanRuleClassifierProviderTest { val matches = inputGreaterThanRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_NEGATIVE_123_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -386,7 +386,7 @@ class FractionInputIsGreaterThanRuleClassifierProviderTest { val matches = inputGreaterThanRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_NEGATIVE_123_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -399,7 +399,7 @@ class FractionInputIsGreaterThanRuleClassifierProviderTest { val matches = inputGreaterThanRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_NEGATIVE_123_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -414,7 +414,7 @@ class FractionInputIsGreaterThanRuleClassifierProviderTest { .matches( answer = FRACTION_VALUE_TEST_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -432,7 +432,7 @@ class FractionInputIsGreaterThanRuleClassifierProviderTest { .matches( answer = FRACTION_VALUE_TEST_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsLessThanRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsLessThanRuleClassifierProviderTest.kt index 8318da73310..1dda20b3d7c 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsLessThanRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsLessThanRuleClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder import org.oppia.android.testing.assertThrows import org.robolectric.annotation.Config @@ -100,7 +100,7 @@ class FractionInputIsLessThanRuleClassifierProviderTest { val matches = inputLessThanRuleClassifier.matches( answer = FRACTION_VALUE_TEST_1_OVER_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -113,7 +113,7 @@ class FractionInputIsLessThanRuleClassifierProviderTest { val matches = inputLessThanRuleClassifier.matches( answer = FRACTION_VALUE_TEST_1_OVER_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -126,7 +126,7 @@ class FractionInputIsLessThanRuleClassifierProviderTest { val matches = inputLessThanRuleClassifier.matches( answer = FRACTION_VALUE_TEST_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -139,7 +139,7 @@ class FractionInputIsLessThanRuleClassifierProviderTest { val matches = inputLessThanRuleClassifier.matches( answer = FRACTION_VALUE_TEST_1_OVER_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -152,7 +152,7 @@ class FractionInputIsLessThanRuleClassifierProviderTest { val matches = inputLessThanRuleClassifier.matches( answer = FRACTION_VALUE_TEST_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -165,7 +165,7 @@ class FractionInputIsLessThanRuleClassifierProviderTest { val matches = inputLessThanRuleClassifier.matches( answer = FRACTION_VALUE_TEST_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -178,7 +178,7 @@ class FractionInputIsLessThanRuleClassifierProviderTest { val matches = inputLessThanRuleClassifier.matches( answer = FRACTION_VALUE_TEST_NEGATIVE_1_OVER_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -191,7 +191,7 @@ class FractionInputIsLessThanRuleClassifierProviderTest { val matches = inputLessThanRuleClassifier.matches( answer = FRACTION_VALUE_TEST_NEGATIVE_1_OVER_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -204,7 +204,7 @@ class FractionInputIsLessThanRuleClassifierProviderTest { val matches = inputLessThanRuleClassifier.matches( answer = FRACTION_VALUE_TEST_NEGATIVE_1_OVER_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -217,7 +217,7 @@ class FractionInputIsLessThanRuleClassifierProviderTest { val matches = inputLessThanRuleClassifier.matches( answer = FRACTION_VALUE_TEST_NEGATIVE_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -230,7 +230,7 @@ class FractionInputIsLessThanRuleClassifierProviderTest { val matches = inputLessThanRuleClassifier.matches( answer = FRACTION_VALUE_TEST_NEGATIVE_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -243,7 +243,7 @@ class FractionInputIsLessThanRuleClassifierProviderTest { val matches = inputLessThanRuleClassifier.matches( answer = FRACTION_VALUE_TEST_NEGATIVE_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -256,7 +256,7 @@ class FractionInputIsLessThanRuleClassifierProviderTest { val matches = inputLessThanRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_123_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -269,7 +269,7 @@ class FractionInputIsLessThanRuleClassifierProviderTest { val matches = inputLessThanRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_123_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -282,7 +282,7 @@ class FractionInputIsLessThanRuleClassifierProviderTest { val matches = inputLessThanRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_123_1_OVER_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -295,7 +295,7 @@ class FractionInputIsLessThanRuleClassifierProviderTest { val matches = inputLessThanRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_123_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -308,7 +308,7 @@ class FractionInputIsLessThanRuleClassifierProviderTest { val matches = inputLessThanRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_123_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -321,7 +321,7 @@ class FractionInputIsLessThanRuleClassifierProviderTest { val matches = inputLessThanRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_NEGATIVE_123_1_OVER_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -334,7 +334,7 @@ class FractionInputIsLessThanRuleClassifierProviderTest { val matches = inputLessThanRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_123_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -347,7 +347,7 @@ class FractionInputIsLessThanRuleClassifierProviderTest { val matches = inputLessThanRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_NEGATIVE_123_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -360,7 +360,7 @@ class FractionInputIsLessThanRuleClassifierProviderTest { val matches = inputLessThanRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_NEGATIVE_123_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -373,7 +373,7 @@ class FractionInputIsLessThanRuleClassifierProviderTest { val matches = inputLessThanRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_NEGATIVE_123_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -386,7 +386,7 @@ class FractionInputIsLessThanRuleClassifierProviderTest { val matches = inputLessThanRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_NEGATIVE_123_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -399,7 +399,7 @@ class FractionInputIsLessThanRuleClassifierProviderTest { val matches = inputLessThanRuleClassifier.matches( answer = MIXED_NUMBER_VALUE_TEST_NEGATIVE_123_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -414,7 +414,7 @@ class FractionInputIsLessThanRuleClassifierProviderTest { .matches( answer = FRACTION_VALUE_TEST_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -432,7 +432,7 @@ class FractionInputIsLessThanRuleClassifierProviderTest { .matches( answer = FRACTION_VALUE_TEST_1_OVER_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/imageClickInput/ImageClickInputIsInRegionRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/imageClickInput/ImageClickInputIsInRegionRuleClassifierProviderTest.kt index 7acae52a13b..0694388d0c9 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/imageClickInput/ImageClickInputIsInRegionRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/imageClickInput/ImageClickInputIsInRegionRuleClassifierProviderTest.kt @@ -12,7 +12,7 @@ import org.junit.runner.RunWith import org.oppia.android.app.model.ClickOnImage import org.oppia.android.app.model.InteractionObject import org.oppia.android.app.model.Point2d -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.testing.assertThrows import org.robolectric.annotation.Config @@ -51,7 +51,7 @@ class ImageClickInputIsInRegionRuleClassifierProviderTest { isInRegionClassifierProvider.matches( answer = IMAGE_REGION_ABC_POSITION_1, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -65,7 +65,7 @@ class ImageClickInputIsInRegionRuleClassifierProviderTest { isInRegionClassifierProvider.matches( answer = IMAGE_REGION_ABC_POSITION_1, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -79,7 +79,7 @@ class ImageClickInputIsInRegionRuleClassifierProviderTest { isInRegionClassifierProvider.matches( answer = IMAGE_REGION_ABC_POSITION_1, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -93,7 +93,7 @@ class ImageClickInputIsInRegionRuleClassifierProviderTest { isInRegionClassifierProvider.matches( answer = IMAGE_REGION_ABC_POSITION_1, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -112,7 +112,7 @@ class ImageClickInputIsInRegionRuleClassifierProviderTest { isInRegionClassifierProvider.matches( answer = IMAGE_REGION_ABC_POSITION_1, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputContainsAtLeastOneOfRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputContainsAtLeastOneOfRuleClassifierProviderTest.kt index e17ba337f4a..e182692d80b 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputContainsAtLeastOneOfRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputContainsAtLeastOneOfRuleClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createSetOfTranslatableHtmlContentIds import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode @@ -52,7 +52,7 @@ class ItemSelectionInputContainsAtLeastOneOfRuleClassifierProviderTest { val matches = inputContainsAtLeastOneOfRuleClassifier.matches( answer = ITEM_SELECTION_12345, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -65,7 +65,7 @@ class ItemSelectionInputContainsAtLeastOneOfRuleClassifierProviderTest { val matches = inputContainsAtLeastOneOfRuleClassifier.matches( answer = ITEM_SELECTION_12345, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -78,7 +78,7 @@ class ItemSelectionInputContainsAtLeastOneOfRuleClassifierProviderTest { val matches = inputContainsAtLeastOneOfRuleClassifier.matches( answer = ITEM_SELECTION_12345, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -91,7 +91,7 @@ class ItemSelectionInputContainsAtLeastOneOfRuleClassifierProviderTest { val matches = inputContainsAtLeastOneOfRuleClassifier.matches( answer = ITEM_SELECTION_12345, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -104,7 +104,7 @@ class ItemSelectionInputContainsAtLeastOneOfRuleClassifierProviderTest { val matches = inputContainsAtLeastOneOfRuleClassifier.matches( answer = ITEM_SELECTION_12345, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -117,7 +117,7 @@ class ItemSelectionInputContainsAtLeastOneOfRuleClassifierProviderTest { val matches = inputContainsAtLeastOneOfRuleClassifier.matches( answer = ITEM_SELECTION_12345, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputDoesNotContainAtLeastOneOfRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputDoesNotContainAtLeastOneOfRuleClassifierProviderTest.kt index f31abfa389c..99be995d0bc 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputDoesNotContainAtLeastOneOfRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputDoesNotContainAtLeastOneOfRuleClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createInt import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createSetOfTranslatableHtmlContentIds import org.oppia.android.testing.assertThrows @@ -55,7 +55,7 @@ class ItemSelectionInputDoesNotContainAtLeastOneOfRuleClassifierProviderTest { val matches = inputDoesNotContainAtLeastOneOfRuleClassifier.matches( answer = ITEM_SET_12345, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -68,7 +68,7 @@ class ItemSelectionInputDoesNotContainAtLeastOneOfRuleClassifierProviderTest { val matches = inputDoesNotContainAtLeastOneOfRuleClassifier.matches( answer = ITEM_SET_12345, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -81,7 +81,7 @@ class ItemSelectionInputDoesNotContainAtLeastOneOfRuleClassifierProviderTest { val matches = inputDoesNotContainAtLeastOneOfRuleClassifier.matches( answer = ITEM_SET_12345, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -94,7 +94,7 @@ class ItemSelectionInputDoesNotContainAtLeastOneOfRuleClassifierProviderTest { val matches = inputDoesNotContainAtLeastOneOfRuleClassifier.matches( answer = ITEM_SET_12345, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -107,7 +107,7 @@ class ItemSelectionInputDoesNotContainAtLeastOneOfRuleClassifierProviderTest { val matches = inputDoesNotContainAtLeastOneOfRuleClassifier.matches( answer = ITEM_SET_12345, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -120,7 +120,7 @@ class ItemSelectionInputDoesNotContainAtLeastOneOfRuleClassifierProviderTest { val matches = inputDoesNotContainAtLeastOneOfRuleClassifier.matches( answer = ITEM_SET_12345, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -133,7 +133,7 @@ class ItemSelectionInputDoesNotContainAtLeastOneOfRuleClassifierProviderTest { val matches = inputDoesNotContainAtLeastOneOfRuleClassifier.matches( answer = ITEM_SET_EMPTY, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -146,7 +146,7 @@ class ItemSelectionInputDoesNotContainAtLeastOneOfRuleClassifierProviderTest { val matches = inputDoesNotContainAtLeastOneOfRuleClassifier.matches( answer = ITEM_SET_12, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -160,7 +160,7 @@ class ItemSelectionInputDoesNotContainAtLeastOneOfRuleClassifierProviderTest { inputDoesNotContainAtLeastOneOfRuleClassifier.matches( answer = ITEM_SET_12345, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -177,7 +177,7 @@ class ItemSelectionInputDoesNotContainAtLeastOneOfRuleClassifierProviderTest { inputDoesNotContainAtLeastOneOfRuleClassifier.matches( answer = ITEM_SET_12345, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -194,7 +194,7 @@ class ItemSelectionInputDoesNotContainAtLeastOneOfRuleClassifierProviderTest { inputDoesNotContainAtLeastOneOfRuleClassifier.matches( answer = DIFFERENT_INTERACTION_OBJECT_TYPE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputEqualsRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputEqualsRuleClassifierProviderTest.kt index 748a43c7a12..927dcdc5f3f 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputEqualsRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputEqualsRuleClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createSetOfTranslatableHtmlContentIds import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.testing.assertThrows @@ -54,7 +54,7 @@ class ItemSelectionInputEqualsRuleClassifierProviderTest { inputEqualsRuleClassifierProvider.matches( answer = TEST_HTML_STRING_SET_LOWERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -68,7 +68,7 @@ class ItemSelectionInputEqualsRuleClassifierProviderTest { inputEqualsRuleClassifierProvider.matches( answer = TEST_HTML_STRING_SET_LOWERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -82,7 +82,7 @@ class ItemSelectionInputEqualsRuleClassifierProviderTest { inputEqualsRuleClassifierProvider.matches( answer = TEST_HTML_STRING_SET_LOWERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -96,7 +96,7 @@ class ItemSelectionInputEqualsRuleClassifierProviderTest { inputEqualsRuleClassifierProvider.matches( answer = TEST_HTML_STRING_SET_LOWERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -110,7 +110,7 @@ class ItemSelectionInputEqualsRuleClassifierProviderTest { inputEqualsRuleClassifierProvider.matches( answer = TEST_HTML_STRING_SET_UPPERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -124,7 +124,7 @@ class ItemSelectionInputEqualsRuleClassifierProviderTest { inputEqualsRuleClassifierProvider.matches( answer = TEST_HTML_STRING_SET_MIXED_UPPERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -138,7 +138,7 @@ class ItemSelectionInputEqualsRuleClassifierProviderTest { inputEqualsRuleClassifierProvider.matches( answer = TEST_HTML_STRING_SET_UPPERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -152,7 +152,7 @@ class ItemSelectionInputEqualsRuleClassifierProviderTest { inputEqualsRuleClassifierProvider.matches( answer = TEST_HTML_STRING_SET_LOWERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -166,7 +166,7 @@ class ItemSelectionInputEqualsRuleClassifierProviderTest { inputEqualsRuleClassifierProvider.matches( answer = TEST_HTML_STRING_SET_LOWERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputIsProperSubsetOfRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputIsProperSubsetOfRuleClassifierProviderTest.kt index 5e22f731b95..8997d85c434 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputIsProperSubsetOfRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/itemselectioninput/ItemSelectionInputIsProperSubsetOfRuleClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createSetOfTranslatableHtmlContentIds import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createString import org.oppia.android.testing.assertThrows @@ -55,7 +55,7 @@ class ItemSelectionInputIsProperSubsetOfRuleClassifierProviderTest() { val matches = inputContainsAtLeastOneOfRuleClassifier.matches( answer = ITEM_SELECTION_12345, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -68,7 +68,7 @@ class ItemSelectionInputIsProperSubsetOfRuleClassifierProviderTest() { val matches = inputContainsAtLeastOneOfRuleClassifier.matches( answer = ITEM_SELECTION_12345, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -81,7 +81,7 @@ class ItemSelectionInputIsProperSubsetOfRuleClassifierProviderTest() { val matches = inputContainsAtLeastOneOfRuleClassifier.matches( answer = ITEM_SELECTION_12, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -94,7 +94,7 @@ class ItemSelectionInputIsProperSubsetOfRuleClassifierProviderTest() { val matches = inputContainsAtLeastOneOfRuleClassifier.matches( answer = ITEM_SELECTION_126, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -107,7 +107,7 @@ class ItemSelectionInputIsProperSubsetOfRuleClassifierProviderTest() { val matches = inputContainsAtLeastOneOfRuleClassifier.matches( answer = ITEM_SELECTION_16, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -120,7 +120,7 @@ class ItemSelectionInputIsProperSubsetOfRuleClassifierProviderTest() { val matches = inputContainsAtLeastOneOfRuleClassifier.matches( answer = ITEM_SELECTION_NONE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -133,7 +133,7 @@ class ItemSelectionInputIsProperSubsetOfRuleClassifierProviderTest() { val matches = inputContainsAtLeastOneOfRuleClassifier.matches( answer = ITEM_SELECTION_NONE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -146,7 +146,7 @@ class ItemSelectionInputIsProperSubsetOfRuleClassifierProviderTest() { val matches = inputContainsAtLeastOneOfRuleClassifier.matches( answer = ITEM_SELECTION_6, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -159,7 +159,7 @@ class ItemSelectionInputIsProperSubsetOfRuleClassifierProviderTest() { val matches = inputContainsAtLeastOneOfRuleClassifier.matches( answer = ITEM_SELECTION_12345, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -173,7 +173,7 @@ class ItemSelectionInputIsProperSubsetOfRuleClassifierProviderTest() { inputContainsAtLeastOneOfRuleClassifier.matches( answer = ITEM_SELECTION_12345, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -190,7 +190,7 @@ class ItemSelectionInputIsProperSubsetOfRuleClassifierProviderTest() { inputContainsAtLeastOneOfRuleClassifier.matches( answer = ITEM_SELECTION_INVAILD, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -207,7 +207,7 @@ class ItemSelectionInputIsProperSubsetOfRuleClassifierProviderTest() { inputContainsAtLeastOneOfRuleClassifier.matches( answer = ITEM_SELECTION_12345, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/multiplechoiceinput/MultipleChoiceInputEqualsRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/multiplechoiceinput/MultipleChoiceInputEqualsRuleClassifierProviderTest.kt index 691b84e99a2..d193f4c795b 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/multiplechoiceinput/MultipleChoiceInputEqualsRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/multiplechoiceinput/MultipleChoiceInputEqualsRuleClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder import org.oppia.android.testing.assertThrows import org.robolectric.annotation.Config @@ -49,7 +49,7 @@ class MultipleChoiceInputEqualsRuleClassifierProviderTest { val matches = inputEqualsRuleClassifier.matches( answer = NON_NEGATIVE_VALUE_TEST_0, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -62,7 +62,7 @@ class MultipleChoiceInputEqualsRuleClassifierProviderTest { val matches = inputEqualsRuleClassifier.matches( answer = NON_NEGATIVE_VALUE_TEST_1, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -76,7 +76,7 @@ class MultipleChoiceInputEqualsRuleClassifierProviderTest { inputEqualsRuleClassifier.matches( answer = NON_NEGATIVE_VALUE_TEST_0, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -93,7 +93,7 @@ class MultipleChoiceInputEqualsRuleClassifierProviderTest { inputEqualsRuleClassifier.matches( answer = STRING_VALUE_TEST, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -110,7 +110,7 @@ class MultipleChoiceInputEqualsRuleClassifierProviderTest { inputEqualsRuleClassifier.matches( answer = NON_NEGATIVE_VALUE_TEST_0, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEqualToRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEqualToRuleClassifierProviderTest.kt index 4557720a234..048e5574066 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEqualToRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEqualToRuleClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder import org.oppia.android.testing.assertThrows import org.robolectric.annotation.Config @@ -105,7 +105,7 @@ class NumberWithUnitsIsEqualToRuleClassifierProviderTest { unitsIsEqualsRuleClassifier.matches( answer = ANSWER_TEST_NUMBER_WITH_UNITS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -119,7 +119,7 @@ class NumberWithUnitsIsEqualToRuleClassifierProviderTest { unitsIsEqualsRuleClassifier.matches( answer = TEST_REAL_INPUT_NUMBER_WITH_UNITS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -133,7 +133,7 @@ class NumberWithUnitsIsEqualToRuleClassifierProviderTest { unitsIsEqualsRuleClassifier.matches( answer = INPUT_TEST_NUMBER_WITH_UNITS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -147,7 +147,7 @@ class NumberWithUnitsIsEqualToRuleClassifierProviderTest { unitsIsEqualsRuleClassifier.matches( answer = ANSWER_TEST_NUMBER_WITH_UNITS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -161,7 +161,7 @@ class NumberWithUnitsIsEqualToRuleClassifierProviderTest { unitsIsEqualsRuleClassifier.matches( answer = ANSWER_TEST_NUMBER_WITH_UNITS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -175,7 +175,7 @@ class NumberWithUnitsIsEqualToRuleClassifierProviderTest { unitsIsEqualsRuleClassifier.matches( answer = TEST_REAL_ANSWER_NUMBER_WITH_UNITS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -190,7 +190,7 @@ class NumberWithUnitsIsEqualToRuleClassifierProviderTest { unitsIsEqualsRuleClassifier.matches( answer = DOUBLE_VALUE_TEST_DIFFERENT_TYPE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEquivalentToRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEquivalentToRuleClassifierProviderTest.kt index 2176ab2837c..d53553b9d79 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEquivalentToRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEquivalentToRuleClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder import org.oppia.android.testing.assertThrows import org.robolectric.annotation.Config @@ -99,7 +99,7 @@ class NumberWithUnitsIsEquivalentToRuleClassifierProviderTest { unitIsEquivalentRuleClassifier.matches( answer = DIFF_TEST_NUMBER_WITH_UNITS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -113,7 +113,7 @@ class NumberWithUnitsIsEquivalentToRuleClassifierProviderTest { unitIsEquivalentRuleClassifier.matches( answer = ANSWER_TEST_NUMBER_WITH_UNITS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -127,7 +127,7 @@ class NumberWithUnitsIsEquivalentToRuleClassifierProviderTest { unitIsEquivalentRuleClassifier.matches( answer = ANSWER_TEST_NUMBER_WITH_UNITS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -141,7 +141,7 @@ class NumberWithUnitsIsEquivalentToRuleClassifierProviderTest { unitIsEquivalentRuleClassifier.matches( answer = DIFF_TEST_NUMBER_WITH_UNITS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -155,7 +155,7 @@ class NumberWithUnitsIsEquivalentToRuleClassifierProviderTest { unitIsEquivalentRuleClassifier.matches( answer = ANSWER_TEST_REAL_NUMBER_WITH_UNITS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -169,7 +169,7 @@ class NumberWithUnitsIsEquivalentToRuleClassifierProviderTest { unitIsEquivalentRuleClassifier.matches( answer = ANSWER_TEST_REAL_NUMBER_WITH_UNITS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -183,7 +183,7 @@ class NumberWithUnitsIsEquivalentToRuleClassifierProviderTest { unitIsEquivalentRuleClassifier.matches( answer = DOUBLE_VALUE_TEST_DIFFERENT_TYPE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -202,7 +202,7 @@ class NumberWithUnitsIsEquivalentToRuleClassifierProviderTest { unitIsEquivalentRuleClassifier.matches( answer = ANSWER_TEST_NUMBER_WITH_UNITS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputEqualsRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputEqualsRuleClassifierProviderTest.kt index 1c7f3c2eb96..b8f7e8a0f9f 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputEqualsRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputEqualsRuleClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder import org.oppia.android.domain.util.FLOAT_EQUALITY_INTERVAL import org.oppia.android.testing.assertThrows @@ -65,7 +65,7 @@ class NumericInputEqualsRuleClassifierProviderTest { inputEqualsRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -79,7 +79,7 @@ class NumericInputEqualsRuleClassifierProviderTest { inputEqualsRuleClassifier.matches( answer = NEGATIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -94,7 +94,7 @@ class NumericInputEqualsRuleClassifierProviderTest { val matches = inputEqualsRuleClassifier.matches( answer = FIVE_POINT_ONE_TIMES_FLOAT_EQUALITY_INTERVAL, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -108,7 +108,7 @@ class NumericInputEqualsRuleClassifierProviderTest { inputEqualsRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_3_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -122,7 +122,7 @@ class NumericInputEqualsRuleClassifierProviderTest { inputEqualsRuleClassifier.matches( answer = NEGATIVE_REAL_VALUE_3_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -136,7 +136,7 @@ class NumericInputEqualsRuleClassifierProviderTest { inputEqualsRuleClassifier.matches( answer = NEGATIVE_REAL_VALUE_3_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -151,7 +151,7 @@ class NumericInputEqualsRuleClassifierProviderTest { val matches = inputEqualsRuleClassifier.matches( answer = SIX_TIMES_FLOAT_EQUALITY_INTERVAL, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -165,7 +165,7 @@ class NumericInputEqualsRuleClassifierProviderTest { inputEqualsRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -182,7 +182,7 @@ class NumericInputEqualsRuleClassifierProviderTest { inputEqualsRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsGreaterThanOrEqualToRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsGreaterThanOrEqualToRuleClassifierProviderTest.kt index 53aacbb513a..b7298602e46 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsGreaterThanOrEqualToRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsGreaterThanOrEqualToRuleClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder import org.oppia.android.testing.assertThrows import org.robolectric.annotation.Config @@ -63,7 +63,7 @@ class NumericInputIsGreaterThanOrEqualToRuleClassifierProviderTest { inputIsGreaterThanOrEqualToRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -77,7 +77,7 @@ class NumericInputIsGreaterThanOrEqualToRuleClassifierProviderTest { inputIsGreaterThanOrEqualToRuleClassifier.matches( answer = NEGATIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -91,7 +91,7 @@ class NumericInputIsGreaterThanOrEqualToRuleClassifierProviderTest { inputIsGreaterThanOrEqualToRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_3_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -105,7 +105,7 @@ class NumericInputIsGreaterThanOrEqualToRuleClassifierProviderTest { inputIsGreaterThanOrEqualToRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -119,7 +119,7 @@ class NumericInputIsGreaterThanOrEqualToRuleClassifierProviderTest { inputIsGreaterThanOrEqualToRuleClassifier.matches( answer = NEGATIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -133,7 +133,7 @@ class NumericInputIsGreaterThanOrEqualToRuleClassifierProviderTest { inputIsGreaterThanOrEqualToRuleClassifier.matches( answer = NEGATIVE_REAL_VALUE_3_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -147,7 +147,7 @@ class NumericInputIsGreaterThanOrEqualToRuleClassifierProviderTest { inputIsGreaterThanOrEqualToRuleClassifier.matches( answer = NEGATIVE_REAL_VALUE_3_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -161,7 +161,7 @@ class NumericInputIsGreaterThanOrEqualToRuleClassifierProviderTest { inputIsGreaterThanOrEqualToRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -175,7 +175,7 @@ class NumericInputIsGreaterThanOrEqualToRuleClassifierProviderTest { inputIsGreaterThanOrEqualToRuleClassifier.matches( answer = POSITIVE_INT_VALUE_1, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -189,7 +189,7 @@ class NumericInputIsGreaterThanOrEqualToRuleClassifierProviderTest { inputIsGreaterThanOrEqualToRuleClassifier.matches( answer = NEGATIVE_INT_VALUE_1, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -203,7 +203,7 @@ class NumericInputIsGreaterThanOrEqualToRuleClassifierProviderTest { inputIsGreaterThanOrEqualToRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -220,7 +220,7 @@ class NumericInputIsGreaterThanOrEqualToRuleClassifierProviderTest { inputIsGreaterThanOrEqualToRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsGreaterThanRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsGreaterThanRuleClassifierProviderTest.kt index 26e38c83922..aa4f78a021d 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsGreaterThanRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsGreaterThanRuleClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder import org.oppia.android.testing.assertThrows import org.robolectric.annotation.Config @@ -64,7 +64,7 @@ class NumericInputIsGreaterThanRuleClassifierProviderTest { .matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -78,7 +78,7 @@ class NumericInputIsGreaterThanRuleClassifierProviderTest { inputIsGreaterThanRuleClassifier.matches( answer = NEGATIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -92,7 +92,7 @@ class NumericInputIsGreaterThanRuleClassifierProviderTest { inputIsGreaterThanRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_3_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -106,7 +106,7 @@ class NumericInputIsGreaterThanRuleClassifierProviderTest { inputIsGreaterThanRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -120,7 +120,7 @@ class NumericInputIsGreaterThanRuleClassifierProviderTest { inputIsGreaterThanRuleClassifier.matches( answer = NEGATIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -134,7 +134,7 @@ class NumericInputIsGreaterThanRuleClassifierProviderTest { inputIsGreaterThanRuleClassifier.matches( answer = NEGATIVE_REAL_VALUE_3_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -148,7 +148,7 @@ class NumericInputIsGreaterThanRuleClassifierProviderTest { inputIsGreaterThanRuleClassifier.matches( answer = NEGATIVE_REAL_VALUE_3_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -162,7 +162,7 @@ class NumericInputIsGreaterThanRuleClassifierProviderTest { inputIsGreaterThanRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -176,7 +176,7 @@ class NumericInputIsGreaterThanRuleClassifierProviderTest { inputIsGreaterThanRuleClassifier.matches( answer = POSITIVE_INT_VALUE_1, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -190,7 +190,7 @@ class NumericInputIsGreaterThanRuleClassifierProviderTest { inputIsGreaterThanRuleClassifier.matches( answer = NEGATIVE_INT_VALUE_1, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -204,7 +204,7 @@ class NumericInputIsGreaterThanRuleClassifierProviderTest { inputIsGreaterThanRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -221,7 +221,7 @@ class NumericInputIsGreaterThanRuleClassifierProviderTest { inputIsGreaterThanRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsInclusivelyBetweenRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsInclusivelyBetweenRuleClassifierProviderTest.kt index fbe82358b66..504117c488e 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsInclusivelyBetweenRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsInclusivelyBetweenRuleClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder import org.oppia.android.testing.assertThrows import org.robolectric.annotation.Config @@ -75,7 +75,7 @@ class NumericInputIsInclusivelyBetweenRuleClassifierProviderTest { inputIsInclusivelyBetweenRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -91,7 +91,7 @@ class NumericInputIsInclusivelyBetweenRuleClassifierProviderTest { inputIsInclusivelyBetweenRuleClassifier.matches( answer = NEGATIVE_REAL_VALUE_3_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -107,7 +107,7 @@ class NumericInputIsInclusivelyBetweenRuleClassifierProviderTest { inputIsInclusivelyBetweenRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_2_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -123,7 +123,7 @@ class NumericInputIsInclusivelyBetweenRuleClassifierProviderTest { inputIsInclusivelyBetweenRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_2_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -139,7 +139,7 @@ class NumericInputIsInclusivelyBetweenRuleClassifierProviderTest { inputIsInclusivelyBetweenRuleClassifier.matches( answer = NEGATIVE_REAL_VALUE_2_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -155,7 +155,7 @@ class NumericInputIsInclusivelyBetweenRuleClassifierProviderTest { inputIsInclusivelyBetweenRuleClassifier.matches( answer = NEGATIVE_REAL_VALUE_2_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -171,7 +171,7 @@ class NumericInputIsInclusivelyBetweenRuleClassifierProviderTest { inputIsInclusivelyBetweenRuleClassifier.matches( answer = POSITIVE_INT_VALUE_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -187,7 +187,7 @@ class NumericInputIsInclusivelyBetweenRuleClassifierProviderTest { inputIsInclusivelyBetweenRuleClassifier.matches( answer = POSITIVE_INT_VALUE_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -203,7 +203,7 @@ class NumericInputIsInclusivelyBetweenRuleClassifierProviderTest { inputIsInclusivelyBetweenRuleClassifier.matches( answer = NEGATIVE_INT_VALUE_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -219,7 +219,7 @@ class NumericInputIsInclusivelyBetweenRuleClassifierProviderTest { inputIsInclusivelyBetweenRuleClassifier.matches( answer = NEGATIVE_REAL_VALUE_2_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -235,7 +235,7 @@ class NumericInputIsInclusivelyBetweenRuleClassifierProviderTest { inputIsInclusivelyBetweenRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -254,7 +254,7 @@ class NumericInputIsInclusivelyBetweenRuleClassifierProviderTest { inputIsInclusivelyBetweenRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -273,7 +273,7 @@ class NumericInputIsInclusivelyBetweenRuleClassifierProviderTest { inputIsInclusivelyBetweenRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -292,7 +292,7 @@ class NumericInputIsInclusivelyBetweenRuleClassifierProviderTest { inputIsInclusivelyBetweenRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -311,7 +311,7 @@ class NumericInputIsInclusivelyBetweenRuleClassifierProviderTest { inputIsInclusivelyBetweenRuleClassifier.matches( answer = POSITIVE_INT_VALUE_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -330,7 +330,7 @@ class NumericInputIsInclusivelyBetweenRuleClassifierProviderTest { inputIsInclusivelyBetweenRuleClassifier.matches( answer = POSITIVE_INT_VALUE_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -349,7 +349,7 @@ class NumericInputIsInclusivelyBetweenRuleClassifierProviderTest { inputIsInclusivelyBetweenRuleClassifier.matches( answer = POSITIVE_INT_VALUE_1, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -368,7 +368,7 @@ class NumericInputIsInclusivelyBetweenRuleClassifierProviderTest { inputIsInclusivelyBetweenRuleClassifier.matches( answer = POSITIVE_INT_VALUE_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsLessThanOrEqualToRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsLessThanOrEqualToRuleClassifierProviderTest.kt index c3894d36ccb..1bcc688f8c3 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsLessThanOrEqualToRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsLessThanOrEqualToRuleClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder import org.oppia.android.testing.assertThrows import org.robolectric.annotation.Config @@ -63,7 +63,7 @@ class NumericInputIsLessThanOrEqualToRuleClassifierProviderTest { inputIsLessThanOrEqualToRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -77,7 +77,7 @@ class NumericInputIsLessThanOrEqualToRuleClassifierProviderTest { inputIsLessThanOrEqualToRuleClassifier.matches( answer = NEGATIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -91,7 +91,7 @@ class NumericInputIsLessThanOrEqualToRuleClassifierProviderTest { inputIsLessThanOrEqualToRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -105,7 +105,7 @@ class NumericInputIsLessThanOrEqualToRuleClassifierProviderTest { inputIsLessThanOrEqualToRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_3_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -119,7 +119,7 @@ class NumericInputIsLessThanOrEqualToRuleClassifierProviderTest { inputIsLessThanOrEqualToRuleClassifier.matches( answer = NEGATIVE_REAL_VALUE_3_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -133,7 +133,7 @@ class NumericInputIsLessThanOrEqualToRuleClassifierProviderTest { inputIsLessThanOrEqualToRuleClassifier.matches( answer = NEGATIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -147,7 +147,7 @@ class NumericInputIsLessThanOrEqualToRuleClassifierProviderTest { inputIsLessThanOrEqualToRuleClassifier.matches( answer = NEGATIVE_REAL_VALUE_3_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -161,7 +161,7 @@ class NumericInputIsLessThanOrEqualToRuleClassifierProviderTest { inputIsLessThanOrEqualToRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -175,7 +175,7 @@ class NumericInputIsLessThanOrEqualToRuleClassifierProviderTest { inputIsLessThanOrEqualToRuleClassifier.matches( answer = POSITIVE_INT_VALUE_1, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -189,7 +189,7 @@ class NumericInputIsLessThanOrEqualToRuleClassifierProviderTest { inputIsLessThanOrEqualToRuleClassifier.matches( answer = NEGATIVE_INT_VALUE_1, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -203,7 +203,7 @@ class NumericInputIsLessThanOrEqualToRuleClassifierProviderTest { inputIsLessThanOrEqualToRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -220,7 +220,7 @@ class NumericInputIsLessThanOrEqualToRuleClassifierProviderTest { inputIsLessThanOrEqualToRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -237,7 +237,7 @@ class NumericInputIsLessThanOrEqualToRuleClassifierProviderTest { inputIsLessThanOrEqualToRuleClassifier.matches( answer = POSITIVE_INT_VALUE_1, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -254,7 +254,7 @@ class NumericInputIsLessThanOrEqualToRuleClassifierProviderTest { inputIsLessThanOrEqualToRuleClassifier.matches( answer = POSITIVE_INT_VALUE_1, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsLessThanRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsLessThanRuleClassifierProviderTest.kt index 50f1e00fbe7..97d152ce3e4 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsLessThanRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsLessThanRuleClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder import org.oppia.android.testing.assertThrows import org.robolectric.annotation.Config @@ -63,7 +63,7 @@ class NumericInputIsLessThanRuleClassifierProviderTest { inputIsLessThanRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -77,7 +77,7 @@ class NumericInputIsLessThanRuleClassifierProviderTest { inputIsLessThanRuleClassifier.matches( answer = NEGATIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -91,7 +91,7 @@ class NumericInputIsLessThanRuleClassifierProviderTest { inputIsLessThanRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -105,7 +105,7 @@ class NumericInputIsLessThanRuleClassifierProviderTest { inputIsLessThanRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_3_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -119,7 +119,7 @@ class NumericInputIsLessThanRuleClassifierProviderTest { inputIsLessThanRuleClassifier.matches( answer = NEGATIVE_REAL_VALUE_3_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -133,7 +133,7 @@ class NumericInputIsLessThanRuleClassifierProviderTest { inputIsLessThanRuleClassifier.matches( answer = NEGATIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -147,7 +147,7 @@ class NumericInputIsLessThanRuleClassifierProviderTest { inputIsLessThanRuleClassifier.matches( answer = NEGATIVE_REAL_VALUE_3_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -161,7 +161,7 @@ class NumericInputIsLessThanRuleClassifierProviderTest { inputIsLessThanRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -175,7 +175,7 @@ class NumericInputIsLessThanRuleClassifierProviderTest { inputIsLessThanRuleClassifier.matches( answer = POSITIVE_INT_VALUE_1, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -189,7 +189,7 @@ class NumericInputIsLessThanRuleClassifierProviderTest { inputIsLessThanRuleClassifier.matches( answer = NEGATIVE_INT_VALUE_1, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -203,7 +203,7 @@ class NumericInputIsLessThanRuleClassifierProviderTest { inputIsLessThanRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -220,7 +220,7 @@ class NumericInputIsLessThanRuleClassifierProviderTest { inputIsLessThanRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -237,7 +237,7 @@ class NumericInputIsLessThanRuleClassifierProviderTest { inputIsLessThanRuleClassifier.matches( answer = POSITIVE_INT_VALUE_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -254,7 +254,7 @@ class NumericInputIsLessThanRuleClassifierProviderTest { inputIsLessThanRuleClassifier.matches( answer = NEGATIVE_INT_VALUE_1, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsWithinToleranceRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsWithinToleranceRuleClassifierProviderTest.kt index b0a6148d256..d5481c0ac89 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsWithinToleranceRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputIsWithinToleranceRuleClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder import org.oppia.android.testing.assertThrows import org.robolectric.annotation.Config @@ -78,7 +78,7 @@ class NumericInputIsWithinToleranceRuleClassifierProviderTest { answer = POSITIVE_REAL_VALUE_2_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -94,7 +94,7 @@ class NumericInputIsWithinToleranceRuleClassifierProviderTest { inputIsWithinToleranceRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -110,7 +110,7 @@ class NumericInputIsWithinToleranceRuleClassifierProviderTest { inputIsWithinToleranceRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_2_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -126,7 +126,7 @@ class NumericInputIsWithinToleranceRuleClassifierProviderTest { inputIsWithinToleranceRuleClassifier.matches( answer = NEGATIVE_REAL_VALUE_2_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -142,7 +142,7 @@ class NumericInputIsWithinToleranceRuleClassifierProviderTest { inputIsWithinToleranceRuleClassifier.matches( answer = NEGATIVE_REAL_VALUE_2_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -158,7 +158,7 @@ class NumericInputIsWithinToleranceRuleClassifierProviderTest { inputIsWithinToleranceRuleClassifier.matches( answer = NEGATIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -174,7 +174,7 @@ class NumericInputIsWithinToleranceRuleClassifierProviderTest { inputIsWithinToleranceRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -190,7 +190,7 @@ class NumericInputIsWithinToleranceRuleClassifierProviderTest { inputIsWithinToleranceRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_2_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -206,7 +206,7 @@ class NumericInputIsWithinToleranceRuleClassifierProviderTest { inputIsWithinToleranceRuleClassifier.matches( answer = NEGATIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -222,7 +222,7 @@ class NumericInputIsWithinToleranceRuleClassifierProviderTest { inputIsWithinToleranceRuleClassifier.matches( answer = NEGATIVE_REAL_VALUE_2_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -238,7 +238,7 @@ class NumericInputIsWithinToleranceRuleClassifierProviderTest { inputIsWithinToleranceRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_2_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -254,7 +254,7 @@ class NumericInputIsWithinToleranceRuleClassifierProviderTest { inputIsWithinToleranceRuleClassifier.matches( answer = POSITIVE_INT_VALUE_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -270,7 +270,7 @@ class NumericInputIsWithinToleranceRuleClassifierProviderTest { inputIsWithinToleranceRuleClassifier.matches( answer = POSITIVE_INT_VALUE_1, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -286,7 +286,7 @@ class NumericInputIsWithinToleranceRuleClassifierProviderTest { inputIsWithinToleranceRuleClassifier.matches( answer = NEGATIVE_INT_VALUE_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -302,7 +302,7 @@ class NumericInputIsWithinToleranceRuleClassifierProviderTest { inputIsWithinToleranceRuleClassifier.matches( answer = NEGATIVE_INT_VALUE_1, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -318,7 +318,7 @@ class NumericInputIsWithinToleranceRuleClassifierProviderTest { inputIsWithinToleranceRuleClassifier.matches( answer = POSITIVE_INT_VALUE_1, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -334,7 +334,7 @@ class NumericInputIsWithinToleranceRuleClassifierProviderTest { inputIsWithinToleranceRuleClassifier.matches( answer = POSITIVE_INT_VALUE_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -350,7 +350,7 @@ class NumericInputIsWithinToleranceRuleClassifierProviderTest { inputIsWithinToleranceRuleClassifier.matches( answer = NEGATIVE_INT_VALUE_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -366,7 +366,7 @@ class NumericInputIsWithinToleranceRuleClassifierProviderTest { inputIsWithinToleranceRuleClassifier.matches( answer = NEGATIVE_INT_VALUE_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -382,7 +382,7 @@ class NumericInputIsWithinToleranceRuleClassifierProviderTest { inputIsWithinToleranceRuleClassifier.matches( answer = POSITIVE_INT_VALUE_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -397,7 +397,7 @@ class NumericInputIsWithinToleranceRuleClassifierProviderTest { inputIsWithinToleranceRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -415,7 +415,7 @@ class NumericInputIsWithinToleranceRuleClassifierProviderTest { inputIsWithinToleranceRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -434,7 +434,7 @@ class NumericInputIsWithinToleranceRuleClassifierProviderTest { inputIsWithinToleranceRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -453,7 +453,7 @@ class NumericInputIsWithinToleranceRuleClassifierProviderTest { inputIsWithinToleranceRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -472,7 +472,7 @@ class NumericInputIsWithinToleranceRuleClassifierProviderTest { inputIsWithinToleranceRuleClassifier.matches( answer = POSITIVE_REAL_VALUE_1_5, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -490,7 +490,7 @@ class NumericInputIsWithinToleranceRuleClassifierProviderTest { inputIsWithinToleranceRuleClassifier.matches( answer = POSITIVE_INT_VALUE_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -508,7 +508,7 @@ class NumericInputIsWithinToleranceRuleClassifierProviderTest { inputIsWithinToleranceRuleClassifier.matches( answer = POSITIVE_INT_VALUE_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -527,7 +527,7 @@ class NumericInputIsWithinToleranceRuleClassifierProviderTest { inputIsWithinToleranceRuleClassifier.matches( answer = POSITIVE_INT_VALUE_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -546,7 +546,7 @@ class NumericInputIsWithinToleranceRuleClassifierProviderTest { inputIsWithinToleranceRuleClassifier.matches( answer = POSITIVE_INT_VALUE_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -565,7 +565,7 @@ class NumericInputIsWithinToleranceRuleClassifierProviderTest { inputIsWithinToleranceRuleClassifier.matches( answer = POSITIVE_INT_VALUE_1, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputEqualsRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputEqualsRuleClassifierProviderTest.kt index 6e221e3a790..244cb981571 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputEqualsRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputEqualsRuleClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.testing.assertThrows @@ -62,7 +62,7 @@ class RatioInputEqualsRuleClassifierProviderTest { equalsClassifierProvider.matches( answer = RATIO_VALUE_TEST_1_2_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -76,7 +76,7 @@ class RatioInputEqualsRuleClassifierProviderTest { equalsClassifierProvider.matches( answer = RATIO_VALUE_TEST_1_2_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -90,7 +90,7 @@ class RatioInputEqualsRuleClassifierProviderTest { equalsClassifierProvider.matches( answer = RATIO_VALUE_TEST_1_2_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -104,7 +104,7 @@ class RatioInputEqualsRuleClassifierProviderTest { equalsClassifierProvider.matches( answer = RATIO_VALUE_TEST_1_2_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -123,7 +123,7 @@ class RatioInputEqualsRuleClassifierProviderTest { equalsClassifierProvider.matches( answer = RATIO_VALUE_TEST_1_2_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputHasNumberOfTermsEqualToClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputHasNumberOfTermsEqualToClassifierProviderTest.kt index 9a38307e9d5..2cbb3b324a4 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputHasNumberOfTermsEqualToClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputHasNumberOfTermsEqualToClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.testing.assertThrows @@ -58,7 +58,7 @@ class RatioInputHasNumberOfTermsEqualToClassifierProviderTest { hasNumberOfTermsEqualToClassifierProvider.matches( answer = RATIO_VALUE_TEST_1_2_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -72,7 +72,7 @@ class RatioInputHasNumberOfTermsEqualToClassifierProviderTest { hasNumberOfTermsEqualToClassifierProvider.matches( answer = RATIO_VALUE_TEST_1_2_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -86,7 +86,7 @@ class RatioInputHasNumberOfTermsEqualToClassifierProviderTest { hasNumberOfTermsEqualToClassifierProvider.matches( answer = RATIO_VALUE_TEST_1_2_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -105,7 +105,7 @@ class RatioInputHasNumberOfTermsEqualToClassifierProviderTest { hasNumberOfTermsEqualToClassifierProvider.matches( answer = RATIO_VALUE_TEST_1_2_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputHasSpecificTermEqualToRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputHasSpecificTermEqualToRuleClassifierProviderTest.kt index dbddb7e5483..81968647149 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputHasSpecificTermEqualToRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputHasSpecificTermEqualToRuleClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.testing.assertThrows @@ -49,7 +49,7 @@ class RatioInputHasSpecificTermEqualToRuleClassifierProviderTest { hasSpecificTermEqualToClassifierProvider.matches( answer = RATIO_VALUE_TEST_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -66,7 +66,7 @@ class RatioInputHasSpecificTermEqualToRuleClassifierProviderTest { hasSpecificTermEqualToClassifierProvider.matches( answer = RATIO_VALUE_TEST_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) // The ratio has value 3, but the value 2 was expected. @@ -84,7 +84,7 @@ class RatioInputHasSpecificTermEqualToRuleClassifierProviderTest { hasSpecificTermEqualToClassifierProvider.matches( answer = RATIO_VALUE_TEST_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) // A value was expected at index 2, but the ratio doesn't have that. @@ -99,7 +99,7 @@ class RatioInputHasSpecificTermEqualToRuleClassifierProviderTest { hasSpecificTermEqualToClassifierProvider.matches( answer = RATIO_VALUE_TEST_3_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -113,7 +113,7 @@ class RatioInputHasSpecificTermEqualToRuleClassifierProviderTest { hasSpecificTermEqualToClassifierProvider.matches( answer = RATIO_VALUE_TEST_3_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) // The ratio has value 3 at index 1, but the value 2 was expected. @@ -128,7 +128,7 @@ class RatioInputHasSpecificTermEqualToRuleClassifierProviderTest { hasSpecificTermEqualToClassifierProvider.matches( answer = RATIO_VALUE_TEST_3_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -142,7 +142,7 @@ class RatioInputHasSpecificTermEqualToRuleClassifierProviderTest { hasSpecificTermEqualToClassifierProvider.matches( answer = RATIO_VALUE_TEST_3_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) // The ratio has value 2 at index 2, but the value 3 was expected. @@ -157,7 +157,7 @@ class RatioInputHasSpecificTermEqualToRuleClassifierProviderTest { hasSpecificTermEqualToClassifierProvider.matches( answer = RATIO_VALUE_TEST_3_2, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) // A value was expected at index 3, but the ratio doesn't have that. @@ -172,7 +172,7 @@ class RatioInputHasSpecificTermEqualToRuleClassifierProviderTest { hasSpecificTermEqualToClassifierProvider.matches( answer = RATIO_VALUE_TEST_1_2_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) // A value was expected at index 0, but the ratio doesn't have that. @@ -187,7 +187,7 @@ class RatioInputHasSpecificTermEqualToRuleClassifierProviderTest { hasSpecificTermEqualToClassifierProvider.matches( answer = RATIO_VALUE_TEST_1_2_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) // A value was expected at index 4, but the ratio doesn't have that. @@ -202,7 +202,7 @@ class RatioInputHasSpecificTermEqualToRuleClassifierProviderTest { hasSpecificTermEqualToClassifierProvider.matches( answer = RATIO_VALUE_TEST_1_2_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -219,7 +219,7 @@ class RatioInputHasSpecificTermEqualToRuleClassifierProviderTest { hasSpecificTermEqualToClassifierProvider.matches( answer = RATIO_VALUE_TEST_1_2_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -236,7 +236,7 @@ class RatioInputHasSpecificTermEqualToRuleClassifierProviderTest { hasSpecificTermEqualToClassifierProvider.matches( answer = RATIO_VALUE_TEST_1_2_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -253,7 +253,7 @@ class RatioInputHasSpecificTermEqualToRuleClassifierProviderTest { hasSpecificTermEqualToClassifierProvider.matches( answer = RATIO_VALUE_TEST_1_2_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -268,7 +268,7 @@ class RatioInputHasSpecificTermEqualToRuleClassifierProviderTest { hasSpecificTermEqualToClassifierProvider.matches( answer = RATIO_VALUE_TEST_1_2_3, inputs = mapOf(), - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputIsEquivalentRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputIsEquivalentRuleClassifierProviderTest.kt index 9b1e17c3c42..c93b44f8dc2 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputIsEquivalentRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/ratioinput/RatioInputIsEquivalentRuleClassifierProviderTest.kt @@ -9,7 +9,7 @@ import dagger.Component import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.testing.assertThrows @@ -66,7 +66,7 @@ class RatioInputIsEquivalentRuleClassifierProviderTest { isEquivalentClassifierProvider.matches( answer = RATIO_VALUE_TEST_2_4_6, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -80,7 +80,7 @@ class RatioInputIsEquivalentRuleClassifierProviderTest { isEquivalentClassifierProvider.matches( answer = RATIO_VALUE_TEST_1_2_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -94,7 +94,7 @@ class RatioInputIsEquivalentRuleClassifierProviderTest { isEquivalentClassifierProvider.matches( answer = RATIO_VALUE_TEST_1_2_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -108,7 +108,7 @@ class RatioInputIsEquivalentRuleClassifierProviderTest { isEquivalentClassifierProvider.matches( answer = RATIO_VALUE_TEST_1_2_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -122,7 +122,7 @@ class RatioInputIsEquivalentRuleClassifierProviderTest { isEquivalentClassifierProvider.matches( answer = RATIO_VALUE_TEST_1_2_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -136,7 +136,7 @@ class RatioInputIsEquivalentRuleClassifierProviderTest { isEquivalentClassifierProvider.matches( answer = RATIO_VALUE_TEST_1_2_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -150,7 +150,7 @@ class RatioInputIsEquivalentRuleClassifierProviderTest { isEquivalentClassifierProvider.matches( answer = RATIO_VALUE_TEST_1_2_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -169,7 +169,7 @@ class RatioInputIsEquivalentRuleClassifierProviderTest { isEquivalentClassifierProvider.matches( answer = RATIO_VALUE_TEST_1_2_3, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/textinput/TextInputContainsRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/textinput/TextInputContainsRuleClassifierProviderTest.kt index 8c931bc5009..11c16049edc 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/textinput/TextInputContainsRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/textinput/TextInputContainsRuleClassifierProviderTest.kt @@ -13,7 +13,7 @@ import dagger.Provides import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createNonNegativeInt import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createString import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createTranslatableSetOfNormalizedString @@ -95,7 +95,7 @@ class TextInputContainsRuleClassifierProviderTest { val matches = inputContainsRuleClassifier.matches( answer = STRING_VALUE_TEST_ANSWER, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -109,7 +109,7 @@ class TextInputContainsRuleClassifierProviderTest { inputContainsRuleClassifier.matches( answer = STRING_VALUE_TEST_ANSWER_NULL, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -122,7 +122,7 @@ class TextInputContainsRuleClassifierProviderTest { val matches = inputContainsRuleClassifier.matches( answer = STRING_VALUE_TEST_ANSWER, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -135,7 +135,7 @@ class TextInputContainsRuleClassifierProviderTest { val matches = inputContainsRuleClassifier.matches( answer = STRING_VALUE_IS_ANSWER, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -148,7 +148,7 @@ class TextInputContainsRuleClassifierProviderTest { val matches = inputContainsRuleClassifier.matches( answer = STRING_VALUE_NOT_ANSWER, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -161,7 +161,7 @@ class TextInputContainsRuleClassifierProviderTest { val matches = inputContainsRuleClassifier.matches( answer = STRING_VALUE_TEST_ANSWER, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -174,7 +174,7 @@ class TextInputContainsRuleClassifierProviderTest { val matches = inputContainsRuleClassifier.matches( answer = STRING_VALUE_TEST_ANSWER, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -187,7 +187,7 @@ class TextInputContainsRuleClassifierProviderTest { val matches = inputContainsRuleClassifier.matches( answer = STRING_VALUE_TEST_ANSWER, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -200,7 +200,7 @@ class TextInputContainsRuleClassifierProviderTest { val matches = inputContainsRuleClassifier.matches( answer = STRING_VALUE_TEST_ANSWER, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -213,7 +213,7 @@ class TextInputContainsRuleClassifierProviderTest { val matches = inputContainsRuleClassifier.matches( answer = STRING_VALUE_TEST_ANSWER, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -227,7 +227,7 @@ class TextInputContainsRuleClassifierProviderTest { inputContainsRuleClassifier.matches( answer = STRING_VALUE_TEST_ANSWER_NULL, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -240,7 +240,7 @@ class TextInputContainsRuleClassifierProviderTest { val matches = inputContainsRuleClassifier.matches( answer = STRING_VALUE_TEST_ANSWER, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -253,7 +253,7 @@ class TextInputContainsRuleClassifierProviderTest { val matches = inputContainsRuleClassifier.matches( answer = STRING_VALUE_TEST_ANSWER, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -267,7 +267,7 @@ class TextInputContainsRuleClassifierProviderTest { inputContainsRuleClassifier.matches( answer = STRING_VALUE_TEST_ANSWER, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -284,7 +284,7 @@ class TextInputContainsRuleClassifierProviderTest { inputContainsRuleClassifier.matches( answer = STRING_VALUE_TEST_ANSWER, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -302,7 +302,7 @@ class TextInputContainsRuleClassifierProviderTest { val matches = inputContainsRuleClassifier.matches( answer = createString("an answer among many"), inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -315,7 +315,7 @@ class TextInputContainsRuleClassifierProviderTest { val matches = inputContainsRuleClassifier.matches( answer = createString("uma resposta entre muitas"), inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) // A Portuguese answer isn't reocgnized with this translation context. @@ -329,7 +329,9 @@ class TextInputContainsRuleClassifierProviderTest { val matches = inputContainsRuleClassifier.matches( answer = createString("an answer among many"), inputs = inputs, - writtenTranslationContext = createTranslationContext(TEST_STRING_CONTENT_ID, "uma resposta") + classificationContext = ClassificationContext( + createTranslationContext(TEST_STRING_CONTENT_ID, "uma resposta") + ) ) // Even though the English string matches, the presence of the Portuguese context should trigger @@ -344,7 +346,9 @@ class TextInputContainsRuleClassifierProviderTest { val matches = inputContainsRuleClassifier.matches( answer = createString("uma resposta entre muitas"), inputs = inputs, - writtenTranslationContext = createTranslationContext(TEST_STRING_CONTENT_ID, "uma resposta") + classificationContext = ClassificationContext( + createTranslationContext(TEST_STRING_CONTENT_ID, "uma resposta") + ) ) // The translation context provides a bridge between Portuguese & English. @@ -358,7 +362,9 @@ class TextInputContainsRuleClassifierProviderTest { val matches = inputContainsRuleClassifier.matches( answer = createString("de outros"), inputs = inputs, - writtenTranslationContext = createTranslationContext(TEST_STRING_CONTENT_ID, "uma resposta") + classificationContext = ClassificationContext( + createTranslationContext(TEST_STRING_CONTENT_ID, "uma resposta") + ) ) // The Portuguese answer doesn't match. diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/textinput/TextInputEqualsRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/textinput/TextInputEqualsRuleClassifierProviderTest.kt index 59dc509ca7f..6506df027fd 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/textinput/TextInputEqualsRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/textinput/TextInputEqualsRuleClassifierProviderTest.kt @@ -13,7 +13,7 @@ import dagger.Provides import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createNonNegativeInt import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createString import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createTranslatableSetOfNormalizedString @@ -87,7 +87,7 @@ class TextInputEqualsRuleClassifierProviderTest { inputEqualsRuleClassifier.matches( answer = STRING_VALUE_TEST_UPPERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -101,7 +101,7 @@ class TextInputEqualsRuleClassifierProviderTest { inputEqualsRuleClassifier.matches( answer = STRING_VALUE_TEST_UPPERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -115,7 +115,7 @@ class TextInputEqualsRuleClassifierProviderTest { inputEqualsRuleClassifier.matches( answer = STRING_VALUE_TEST_LOWERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -129,7 +129,7 @@ class TextInputEqualsRuleClassifierProviderTest { inputEqualsRuleClassifier.matches( answer = STRING_VALUE_TEST_SINGLE_SPACES, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -142,7 +142,7 @@ class TextInputEqualsRuleClassifierProviderTest { val matches = inputEqualsRuleClassifier.matches( answer = STRING_VALUE_TEST_DIFFERENT_VALUE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -156,7 +156,7 @@ class TextInputEqualsRuleClassifierProviderTest { inputEqualsRuleClassifier.matches( answer = STRING_VALUE_TEST_SINGLE_SPACES, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -169,7 +169,7 @@ class TextInputEqualsRuleClassifierProviderTest { val matches = inputEqualsRuleClassifier.matches( answer = STRING_VALUE_THIS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -182,7 +182,7 @@ class TextInputEqualsRuleClassifierProviderTest { val matches = inputEqualsRuleClassifier.matches( answer = STRING_VALUE_A_TEST, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -195,7 +195,7 @@ class TextInputEqualsRuleClassifierProviderTest { val matches = inputEqualsRuleClassifier.matches( answer = STRING_VALUE_NOT_A_TEST, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -209,7 +209,7 @@ class TextInputEqualsRuleClassifierProviderTest { inputEqualsRuleClassifier.matches( answer = STRING_VALUE_TEST_LOWERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -226,7 +226,7 @@ class TextInputEqualsRuleClassifierProviderTest { inputEqualsRuleClassifier.matches( answer = STRING_VALUE_TEST_UPPERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -244,7 +244,7 @@ class TextInputEqualsRuleClassifierProviderTest { val matches = inputEqualsRuleClassifier.matches( answer = createString("an answer"), inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -257,7 +257,7 @@ class TextInputEqualsRuleClassifierProviderTest { val matches = inputEqualsRuleClassifier.matches( answer = createString("uma resposta"), inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) // A Portuguese answer isn't reocgnized with this translation context. @@ -271,7 +271,9 @@ class TextInputEqualsRuleClassifierProviderTest { val matches = inputEqualsRuleClassifier.matches( answer = createString("an answer"), inputs = inputs, - writtenTranslationContext = createTranslationContext(TEST_STRING_CONTENT_ID, "uma resposta") + classificationContext = ClassificationContext( + createTranslationContext(TEST_STRING_CONTENT_ID, "uma resposta") + ) ) // Even though the English string matches, the presence of the Portuguese context should trigger @@ -286,7 +288,9 @@ class TextInputEqualsRuleClassifierProviderTest { val matches = inputEqualsRuleClassifier.matches( answer = createString("uma resposta"), inputs = inputs, - writtenTranslationContext = createTranslationContext(TEST_STRING_CONTENT_ID, "uma resposta") + classificationContext = ClassificationContext( + createTranslationContext(TEST_STRING_CONTENT_ID, "uma resposta") + ) ) // The translation context provides a bridge between Portuguese & English. @@ -300,7 +304,9 @@ class TextInputEqualsRuleClassifierProviderTest { val matches = inputEqualsRuleClassifier.matches( answer = createString("uma resposta diferente"), inputs = inputs, - writtenTranslationContext = createTranslationContext(TEST_STRING_CONTENT_ID, "uma resposta") + classificationContext = ClassificationContext( + createTranslationContext(TEST_STRING_CONTENT_ID, "uma resposta") + ) ) // The Portuguese answer doesn't match. diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/textinput/TextInputFuzzyEqualsRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/textinput/TextInputFuzzyEqualsRuleClassifierProviderTest.kt index 0642bc8879c..53617305bd3 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/textinput/TextInputFuzzyEqualsRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/textinput/TextInputFuzzyEqualsRuleClassifierProviderTest.kt @@ -13,7 +13,7 @@ import dagger.Provides import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createNonNegativeInt import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createString import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createTranslatableSetOfNormalizedString @@ -89,7 +89,7 @@ class TextInputFuzzyEqualsRuleClassifierProviderTest { inputFuzzyEqualsRuleClassifier.matches( answer = STRING_VALUE_TEST_ANSWER_UPPERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -103,7 +103,7 @@ class TextInputFuzzyEqualsRuleClassifierProviderTest { inputFuzzyEqualsRuleClassifier.matches( answer = STRING_VALUE_TEST_ANSWER_UPPERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -117,7 +117,7 @@ class TextInputFuzzyEqualsRuleClassifierProviderTest { inputFuzzyEqualsRuleClassifier.matches( answer = STRING_VALUE_TEST_ANSWER_UPPERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -131,7 +131,7 @@ class TextInputFuzzyEqualsRuleClassifierProviderTest { inputFuzzyEqualsRuleClassifier.matches( answer = STRING_VALUE_TEST_DIFF_UPPERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -145,7 +145,7 @@ class TextInputFuzzyEqualsRuleClassifierProviderTest { inputFuzzyEqualsRuleClassifier.matches( answer = STRING_VALUE_TEST_ANSWER_LOWERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -159,7 +159,7 @@ class TextInputFuzzyEqualsRuleClassifierProviderTest { inputFuzzyEqualsRuleClassifier.matches( answer = STRING_VALUE_TEST_ANSWER_LOWERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -173,7 +173,7 @@ class TextInputFuzzyEqualsRuleClassifierProviderTest { inputFuzzyEqualsRuleClassifier.matches( answer = STRING_VALUE_TEST_ANSWER_DIFF_LOWERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -187,7 +187,7 @@ class TextInputFuzzyEqualsRuleClassifierProviderTest { inputFuzzyEqualsRuleClassifier.matches( answer = STRING_VALUE_TEST_ANSWER_DIFF_LOWERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -200,7 +200,7 @@ class TextInputFuzzyEqualsRuleClassifierProviderTest { val matches = inputFuzzyEqualsRuleClassifier.matches( answer = STRING_VALUE_TEST_ANSWER, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -213,7 +213,7 @@ class TextInputFuzzyEqualsRuleClassifierProviderTest { val matches = inputFuzzyEqualsRuleClassifier.matches( answer = STRING_VALUE_TEST_WITH_WHITESPACE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -226,7 +226,7 @@ class TextInputFuzzyEqualsRuleClassifierProviderTest { val matches = inputFuzzyEqualsRuleClassifier.matches( answer = STRING_VALUE_THIS, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -239,7 +239,7 @@ class TextInputFuzzyEqualsRuleClassifierProviderTest { val matches = inputFuzzyEqualsRuleClassifier.matches( answer = STRING_VALUE_TEST, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -252,7 +252,7 @@ class TextInputFuzzyEqualsRuleClassifierProviderTest { val matches = inputFuzzyEqualsRuleClassifier.matches( answer = STRING_VALUE_TESTING, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -266,7 +266,7 @@ class TextInputFuzzyEqualsRuleClassifierProviderTest { inputFuzzyEqualsRuleClassifier.matches( answer = STRING_VALUE_TEST_ANSWER_UPPERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -284,7 +284,7 @@ class TextInputFuzzyEqualsRuleClassifierProviderTest { val matches = inputFuzzyEqualsRuleClassifier.matches( answer = createString("an answer"), inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -297,7 +297,7 @@ class TextInputFuzzyEqualsRuleClassifierProviderTest { val matches = inputFuzzyEqualsRuleClassifier.matches( answer = createString("uma resposta"), inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) // A Portuguese answer isn't reocgnized with this translation context. @@ -311,7 +311,9 @@ class TextInputFuzzyEqualsRuleClassifierProviderTest { val matches = inputFuzzyEqualsRuleClassifier.matches( answer = createString("an answer"), inputs = inputs, - writtenTranslationContext = createTranslationContext(TEST_STRING_CONTENT_ID, "uma resposta") + classificationContext = ClassificationContext( + createTranslationContext(TEST_STRING_CONTENT_ID, "uma resposta") + ) ) // Even though the English string matches, the presence of the Portuguese context should trigger @@ -326,7 +328,9 @@ class TextInputFuzzyEqualsRuleClassifierProviderTest { val matches = inputFuzzyEqualsRuleClassifier.matches( answer = createString("uma resposta"), inputs = inputs, - writtenTranslationContext = createTranslationContext(TEST_STRING_CONTENT_ID, "uma resposta") + classificationContext = ClassificationContext( + createTranslationContext(TEST_STRING_CONTENT_ID, "uma resposta") + ) ) // The translation context provides a bridge between Portuguese & English. @@ -340,7 +344,9 @@ class TextInputFuzzyEqualsRuleClassifierProviderTest { val matches = inputFuzzyEqualsRuleClassifier.matches( answer = createString("uma reposta"), inputs = inputs, - writtenTranslationContext = createTranslationContext(TEST_STRING_CONTENT_ID, "uma resposta") + classificationContext = ClassificationContext( + createTranslationContext(TEST_STRING_CONTENT_ID, "uma resposta") + ) ) // A single misspelled letter should still result in a match in the same way as English. @@ -354,7 +360,9 @@ class TextInputFuzzyEqualsRuleClassifierProviderTest { val matches = inputFuzzyEqualsRuleClassifier.matches( answer = createString("reposta"), inputs = inputs, - writtenTranslationContext = createTranslationContext(TEST_STRING_CONTENT_ID, "uma resposta") + classificationContext = ClassificationContext( + createTranslationContext(TEST_STRING_CONTENT_ID, "uma resposta") + ) ) // A missing word & a misspelled word should result in no match. @@ -368,7 +376,9 @@ class TextInputFuzzyEqualsRuleClassifierProviderTest { val matches = inputFuzzyEqualsRuleClassifier.matches( answer = createString("إجاب"), inputs = inputs, - writtenTranslationContext = createTranslationContext(TEST_STRING_CONTENT_ID, "إجابة") + classificationContext = ClassificationContext( + createTranslationContext(TEST_STRING_CONTENT_ID, "إجابة") + ) ) // A single misspelled letter should still result in a match in the same way as English. @@ -382,7 +392,9 @@ class TextInputFuzzyEqualsRuleClassifierProviderTest { val matches = inputFuzzyEqualsRuleClassifier.matches( answer = createString("إجا"), inputs = inputs, - writtenTranslationContext = createTranslationContext(TEST_STRING_CONTENT_ID, "uma resposta") + classificationContext = ClassificationContext( + createTranslationContext(TEST_STRING_CONTENT_ID, "uma resposta") + ) ) // Multiple missing letters should result in no match. diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/textinput/TextInputStartsWithRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/textinput/TextInputStartsWithRuleClassifierProviderTest.kt index 1d0cd08ece0..1999f7ca984 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/textinput/TextInputStartsWithRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/textinput/TextInputStartsWithRuleClassifierProviderTest.kt @@ -13,7 +13,7 @@ import dagger.Provides import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createNonNegativeInt import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createString import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createTranslatableSetOfNormalizedString @@ -93,7 +93,7 @@ class TextInputStartsWithRuleClassifierProviderTest { val matches = inputStartsWithRuleClassifier.matches( answer = STRING_VALUE_TEST_STRING_LOWERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -106,7 +106,7 @@ class TextInputStartsWithRuleClassifierProviderTest { val matches = inputStartsWithRuleClassifier.matches( answer = STRING_VALUE_TEST_STRING_LOWERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -119,7 +119,7 @@ class TextInputStartsWithRuleClassifierProviderTest { val matches = inputStartsWithRuleClassifier.matches( answer = STRING_VALUE_TEST_LOWERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -132,7 +132,7 @@ class TextInputStartsWithRuleClassifierProviderTest { val matches = inputStartsWithRuleClassifier.matches( answer = STRING_VALUE_TEST_STRING_LOWERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) // The check should be case-insensitive. @@ -146,7 +146,7 @@ class TextInputStartsWithRuleClassifierProviderTest { val matches = inputStartsWithRuleClassifier.matches( answer = STRING_VALUE_TEST_STRING_LOWERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -159,7 +159,7 @@ class TextInputStartsWithRuleClassifierProviderTest { val matches = inputStartsWithRuleClassifier.matches( answer = STRING_VALUE_TEST_STRING_LOWERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) // The check should be case-insensitive. @@ -173,7 +173,7 @@ class TextInputStartsWithRuleClassifierProviderTest { val matches = inputStartsWithRuleClassifier.matches( answer = STRING_VALUE_TEST_STRING_LOWERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -186,7 +186,7 @@ class TextInputStartsWithRuleClassifierProviderTest { val matches = inputStartsWithRuleClassifier.matches( answer = STRING_VALUE_TEST_UPPERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -199,7 +199,7 @@ class TextInputStartsWithRuleClassifierProviderTest { val matches = inputStartsWithRuleClassifier.matches( answer = STRING_VALUE_TEST_UPPERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -212,7 +212,7 @@ class TextInputStartsWithRuleClassifierProviderTest { val matches = inputStartsWithRuleClassifier.matches( answer = STRING_VALUE_TEST_STRING_UPPERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -225,7 +225,7 @@ class TextInputStartsWithRuleClassifierProviderTest { val matches = inputStartsWithRuleClassifier.matches( answer = STRING_VALUE_TEST_NULL, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -238,7 +238,7 @@ class TextInputStartsWithRuleClassifierProviderTest { val matches = inputStartsWithRuleClassifier.matches( answer = STRING_VALUE_TEST_NULL, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -251,7 +251,7 @@ class TextInputStartsWithRuleClassifierProviderTest { val matches = inputStartsWithRuleClassifier.matches( answer = STRING_VALUE_ANTIDERIVATIVE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -264,7 +264,7 @@ class TextInputStartsWithRuleClassifierProviderTest { val matches = inputStartsWithRuleClassifier.matches( answer = STRING_VALUE_PREFIX, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -277,7 +277,7 @@ class TextInputStartsWithRuleClassifierProviderTest { val matches = inputStartsWithRuleClassifier.matches( answer = STRING_VALUE_SOMETHING_ELSE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isFalse() @@ -291,7 +291,7 @@ class TextInputStartsWithRuleClassifierProviderTest { inputStartsWithRuleClassifier.matches( answer = STRING_VALUE_TEST_STRING_LOWERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -308,7 +308,7 @@ class TextInputStartsWithRuleClassifierProviderTest { inputStartsWithRuleClassifier.matches( answer = STRING_VALUE_TEST_STRING_LOWERCASE, inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } @@ -326,7 +326,7 @@ class TextInputStartsWithRuleClassifierProviderTest { val matches = inputStartsWithRuleClassifier.matches( answer = createString("an answer is my choice"), inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) assertThat(matches).isTrue() @@ -339,7 +339,7 @@ class TextInputStartsWithRuleClassifierProviderTest { val matches = inputStartsWithRuleClassifier.matches( answer = createString("uma resposta é minha escolha"), inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) // A Portuguese answer isn't reocgnized with this translation context. @@ -353,7 +353,9 @@ class TextInputStartsWithRuleClassifierProviderTest { val matches = inputStartsWithRuleClassifier.matches( answer = createString("an answer is my choice"), inputs = inputs, - writtenTranslationContext = createTranslationContext(TEST_STRING_CONTENT_ID, "uma resposta") + classificationContext = ClassificationContext( + createTranslationContext(TEST_STRING_CONTENT_ID, "uma resposta") + ) ) // Even though the English string matches, the presence of the Portuguese context should trigger @@ -368,7 +370,9 @@ class TextInputStartsWithRuleClassifierProviderTest { val matches = inputStartsWithRuleClassifier.matches( answer = createString("uma resposta é minha escolha"), inputs = inputs, - writtenTranslationContext = createTranslationContext(TEST_STRING_CONTENT_ID, "uma resposta") + classificationContext = ClassificationContext( + createTranslationContext(TEST_STRING_CONTENT_ID, "uma resposta") + ) ) // The translation context provides a bridge between Portuguese & English. @@ -382,7 +386,9 @@ class TextInputStartsWithRuleClassifierProviderTest { val matches = inputStartsWithRuleClassifier.matches( answer = createString("diferente"), inputs = inputs, - writtenTranslationContext = createTranslationContext(TEST_STRING_CONTENT_ID, "uma resposta") + classificationContext = ClassificationContext( + createTranslationContext(TEST_STRING_CONTENT_ID, "uma resposta") + ) ) // The Portuguese answer doesn't match. From fef2d976ec89ad02c198c7d25b743ba762424812 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 15 Dec 2021 17:47:07 -0800 Subject: [PATCH 019/162] Add classifiers for AlgebraicExpressionInput. --- ...putIsEquivalentToRuleClassifierProvider.kt | 69 +++++++++++++++++++ ...atchesExactlyWithRuleClassifierProvider.kt | 62 +++++++++++++++++ ...vialManipulationsRuleClassifierProvider.kt | 64 +++++++++++++++++ .../AlgebraicExpressionInputModule.kt | 36 ++++++++++ 4 files changed, 231 insertions(+) create mode 100644 domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputIsEquivalentToRuleClassifierProvider.kt create mode 100644 domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider.kt create mode 100644 domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt create mode 100644 domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModule.kt diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputIsEquivalentToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputIsEquivalentToRuleClassifierProvider.kt new file mode 100644 index 00000000000..899a14682d6 --- /dev/null +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputIsEquivalentToRuleClassifierProvider.kt @@ -0,0 +1,69 @@ +package org.oppia.android.domain.classify.rules.algebraicexpressioninput + +import org.oppia.android.app.model.InteractionObject +import org.oppia.android.app.model.Polynomial +import org.oppia.android.domain.classify.ClassificationContext +import org.oppia.android.domain.classify.RuleClassifier +import org.oppia.android.domain.classify.rules.GenericRuleClassifier +import org.oppia.android.domain.classify.rules.RuleClassifierProvider +import org.oppia.android.util.logging.ConsoleLogger +import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult +import org.oppia.android.util.math.toPolynomial +import javax.inject.Inject +import org.oppia.android.util.math.MathExpressionParser.Companion.parseAlgebraicExpression + +class AlgebraicExpressionInputIsEquivalentToRuleClassifierProvider @Inject constructor( + private val classifierFactory: GenericRuleClassifier.Factory, + private val consoleLogger: ConsoleLogger +) : RuleClassifierProvider, GenericRuleClassifier.SingleInputMatcher { + override fun createRuleClassifier(): RuleClassifier { + return classifierFactory.createSingleInputClassifier( + expectedObjectType = InteractionObject.ObjectTypeCase.MATH_EXPRESSION, + inputParameterName = "x", + matcher = this + ) + } + + override fun matches( + answer: String, + input: String, + classificationContext: ClassificationContext + ): Boolean { + val allowedVariables = classificationContext.extractAllowedVariables() + val answerExpression = parsePolynomial(answer, allowedVariables) ?: return false + val inputExpression = parsePolynomial(input, allowedVariables) ?: return false + return answerExpression == inputExpression + } + + private fun parsePolynomial(rawExpression: String, allowedVariables: List): Polynomial? { + return when (val expResult = parseAlgebraicExpression(rawExpression, allowedVariables)) { + is MathParsingResult.Success -> { + expResult.result.toPolynomial().also { + if (it == null) { + consoleLogger.w( + "AlgebraExpEquivalent", "Expression is not a supported polynomial: $rawExpression." + ) + } + } + } + is MathParsingResult.Failure -> { + consoleLogger.e( + "AlgebraExpEquivalent", + "Encountered expression that failed parsing. Expression: $rawExpression." + + " Failure: ${expResult.error}." + ) + null + } + } + } + + private companion object { + private fun ClassificationContext.extractAllowedVariables(): List { + return customizationArgs["customOskLetters"] + ?.schemaObjectList + ?.schemaObjectList + ?.map { it.normalizedString } + ?: listOf() + } + } +} diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider.kt new file mode 100644 index 00000000000..fb521383d4c --- /dev/null +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider.kt @@ -0,0 +1,62 @@ +package org.oppia.android.domain.classify.rules.algebraicexpressioninput + +import org.oppia.android.app.model.InteractionObject +import org.oppia.android.app.model.MathExpression +import org.oppia.android.domain.classify.ClassificationContext +import org.oppia.android.domain.classify.RuleClassifier +import org.oppia.android.domain.classify.rules.GenericRuleClassifier +import org.oppia.android.domain.classify.rules.RuleClassifierProvider +import org.oppia.android.util.logging.ConsoleLogger +import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult +import javax.inject.Inject +import org.oppia.android.util.math.MathExpressionParser.Companion.parseAlgebraicExpression + +class AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider @Inject constructor( + private val classifierFactory: GenericRuleClassifier.Factory, + private val consoleLogger: ConsoleLogger +) : RuleClassifierProvider, GenericRuleClassifier.SingleInputMatcher { + override fun createRuleClassifier(): RuleClassifier { + return classifierFactory.createSingleInputClassifier( + expectedObjectType = InteractionObject.ObjectTypeCase.MATH_EXPRESSION, + inputParameterName = "x", + matcher = this + ) + } + + override fun matches( + answer: String, + input: String, + classificationContext: ClassificationContext + ): Boolean { + val allowedVariables = classificationContext.extractAllowedVariables() + val answerExpression = parseExpression(answer, allowedVariables) ?: return false + val inputExpression = parseExpression(input, allowedVariables) ?: return false + return answerExpression == inputExpression + } + + private fun parseExpression( + rawExpression: String, allowedVariables: List + ): MathExpression? { + return when (val expResult = parseAlgebraicExpression(rawExpression, allowedVariables)) { + is MathParsingResult.Success -> expResult.result + is MathParsingResult.Failure -> { + consoleLogger.e( + "AlgebraExpMatchesExact", + "Encountered expression that failed parsing. Expression: $rawExpression." + + " Failure: ${expResult.error}." + ) + null + } + } + } + + private companion object { + private fun ClassificationContext.extractAllowedVariables(): List { + return customizationArgs["customOskLetters"] + ?.schemaObjectList + ?.schemaObjectList + ?.map { it.normalizedString } + ?: listOf() + } + } +} diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt new file mode 100644 index 00000000000..b56fe353d39 --- /dev/null +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt @@ -0,0 +1,64 @@ +package org.oppia.android.domain.classify.rules.algebraicexpressioninput + +import org.oppia.android.app.model.ComparableOperationList +import org.oppia.android.app.model.InteractionObject +import org.oppia.android.domain.classify.ClassificationContext +import org.oppia.android.domain.classify.RuleClassifier +import org.oppia.android.domain.classify.rules.GenericRuleClassifier +import org.oppia.android.domain.classify.rules.RuleClassifierProvider +import org.oppia.android.util.logging.ConsoleLogger +import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult +import org.oppia.android.util.math.toComparableOperationList +import javax.inject.Inject +import org.oppia.android.util.math.MathExpressionParser.Companion.parseAlgebraicExpression + +class AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider +@Inject constructor( + private val classifierFactory: GenericRuleClassifier.Factory, + private val consoleLogger: ConsoleLogger +) : RuleClassifierProvider, GenericRuleClassifier.SingleInputMatcher { + override fun createRuleClassifier(): RuleClassifier { + return classifierFactory.createSingleInputClassifier( + expectedObjectType = InteractionObject.ObjectTypeCase.MATH_EXPRESSION, + inputParameterName = "x", + matcher = this + ) + } + + override fun matches( + answer: String, + input: String, + classificationContext: ClassificationContext + ): Boolean { + val allowedVariables = classificationContext.extractAllowedVariables() + val answerExpression = parseComparableOperationList(answer, allowedVariables) ?: return false + val inputExpression = parseComparableOperationList(input, allowedVariables) ?: return false + return answerExpression == inputExpression + } + + private fun parseComparableOperationList( + rawExpression: String, allowedVariables: List + ): ComparableOperationList? { + return when (val expResult = parseAlgebraicExpression(rawExpression, allowedVariables)) { + is MathParsingResult.Success -> expResult.result.toComparableOperationList() + is MathParsingResult.Failure -> { + consoleLogger.e( + "AlgebraExpTrivialManips", + "Encountered expression that failed parsing. Expression: $rawExpression." + + " Failure: ${expResult.error}." + ) + null + } + } + } + + private companion object { + private fun ClassificationContext.extractAllowedVariables(): List { + return customizationArgs["customOskLetters"] + ?.schemaObjectList + ?.schemaObjectList + ?.map { it.normalizedString } + ?: listOf() + } + } +} diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModule.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModule.kt new file mode 100644 index 00000000000..091dc12e4e6 --- /dev/null +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModule.kt @@ -0,0 +1,36 @@ +package org.oppia.android.domain.classify.rules.algebraicexpressioninput + +import dagger.Module +import dagger.Provides +import dagger.multibindings.IntoMap +import dagger.multibindings.StringKey +import org.oppia.android.domain.classify.RuleClassifier +import org.oppia.android.domain.classify.rules.FractionInputRules + +/** Module that binds rule classifiers corresponding to the algebraic expression input interaction. */ +@Module +class AlgebraicExpressionInputModule { + @Provides + @IntoMap + @StringKey("MatchesExactlyWith") + @FractionInputRules + internal fun provideAlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider( + classifierProvider: AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider + ): RuleClassifier = classifierProvider.createRuleClassifier() + + @Provides + @IntoMap + @StringKey("MatchesUpToTrivialManipulations") + @FractionInputRules + internal fun provideAlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifier( + classifierProvider: AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider + ): RuleClassifier = classifierProvider.createRuleClassifier() + + @Provides + @IntoMap + @StringKey("IsEquivalentTo") + @FractionInputRules + internal fun provideAlgebraicExpressionInputIsEquivalentToRuleClassifier( + classifierProvider: AlgebraicExpressionInputIsEquivalentToRuleClassifierProvider + ): RuleClassifier = classifierProvider.createRuleClassifier() +} From 783396f6998eddde37ff61d8c3f3662bb44c5e22 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 15 Dec 2021 17:49:16 -0800 Subject: [PATCH 020/162] Add missing annotation for new interaction. --- .../domain/classify/rules/RuleQualifiers.kt | 57 +++++++++++++++---- .../NumericExpressionInputModule.kt | 8 +-- 2 files changed, 51 insertions(+), 14 deletions(-) diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/RuleQualifiers.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/RuleQualifiers.kt index 18b9d9faed7..a8965af13ae 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/RuleQualifiers.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/RuleQualifiers.kt @@ -2,42 +2,79 @@ package org.oppia.android.domain.classify.rules import javax.inject.Qualifier -/** Corresponds to [org.oppia.android.domain.classify.RuleClassifier]s that can be used by the continue interaction. */ +/** + * Corresponds to [org.oppia.android.domain.classify.RuleClassifier]s that can be used by the + * continue interaction. + */ @Qualifier annotation class ContinueRules -/** Corresponds to [org.oppia.android.domain.classify.RuleClassifier]s that can be used by the fraction input interaction. */ +/** + * Corresponds to [org.oppia.android.domain.classify.RuleClassifier]s that can be used by the + * fraction input interaction. + */ @Qualifier annotation class FractionInputRules -/** Corresponds to [org.oppia.android.domain.classify.RuleClassifier]s that can be used by the item selection interaction. */ +/** + * Corresponds to [org.oppia.android.domain.classify.RuleClassifier]s that can be used by the item + * selection interaction. + */ @Qualifier annotation class ItemSelectionInputRules -/** Corresponds to [org.oppia.android.domain.classify.RuleClassifier]s that can be used by the multiple choice interaction. */ +/** + * Corresponds to [org.oppia.android.domain.classify.RuleClassifier]s that can be used by the + * multiple choice interaction. + */ @Qualifier annotation class MultipleChoiceInputRules -/** Corresponds to [org.oppia.android.domain.classify.RuleClassifier]s that can be used by the number with units interaction. */ +/** + * Corresponds to [org.oppia.android.domain.classify.RuleClassifier]s that can be used by the number + * with units interaction. + */ @Qualifier annotation class NumberWithUnitsRules -/** Corresponds to [org.oppia.android.domain.classify.RuleClassifier]s that can be used by the text input interaction. */ +/** + * Corresponds to [org.oppia.android.domain.classify.RuleClassifier]s that can be used by the text + * input interaction. + */ @Qualifier annotation class TextInputRules -/** Corresponds to [org.oppia.android.domain.classify.RuleClassifier]s that can be used by the numeric input interaction. */ +/** + * Corresponds to [org.oppia.android.domain.classify.RuleClassifier]s that can be used by the + * numeric input interaction. + */ @Qualifier annotation class NumericInputRules -/** Corresponds to [org.oppia.android.domain.classify.RuleClassifier]s that can be used by the drag drop sort input interaction. */ +/** + * Corresponds to [org.oppia.android.domain.classify.RuleClassifier]s that can be used by the drag + * drop sort input interaction. + */ @Qualifier annotation class DragDropSortInputRules -/** Corresponds to [org.oppia.android.domain.classify.RuleClassifier]s that can be used by the image click input interaction. */ +/** + * Corresponds to [org.oppia.android.domain.classify.RuleClassifier]s that can be used by the image + * click input interaction. + */ @Qualifier annotation class ImageClickInputRules -/** Corresponds to [org.oppia.android.domain.classify.RuleClassifier]s that can be used by the ratio input interaction. */ +/** + * Corresponds to [org.oppia.android.domain.classify.RuleClassifier]s that can be used by the ratio + * input interaction. + */ @Qualifier annotation class RatioExpressionInputRules + +/** + * Corresponds to [org.oppia.android.domain.classify.RuleClassifier]s that can be used by the + * numeric expression input interaction. + */ +@Qualifier +annotation class NumericExpressionInputRules diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputModule.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputModule.kt index cb010da48de..4a74256a777 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputModule.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputModule.kt @@ -5,7 +5,7 @@ import dagger.Provides import dagger.multibindings.IntoMap import dagger.multibindings.StringKey import org.oppia.android.domain.classify.RuleClassifier -import org.oppia.android.domain.classify.rules.FractionInputRules +import org.oppia.android.domain.classify.rules.NumericExpressionInputRules /** Module that binds rule classifiers corresponding to the numeric expression input interaction. */ @Module @@ -13,7 +13,7 @@ class NumericExpressionInputModule { @Provides @IntoMap @StringKey("MatchesExactlyWith") - @FractionInputRules + @NumericExpressionInputRules internal fun provideNumericExpressionInputMatchesExactlyWithRuleClassifierProvider( classifierProvider: NumericExpressionInputMatchesExactlyWithRuleClassifierProvider ): RuleClassifier = classifierProvider.createRuleClassifier() @@ -21,7 +21,7 @@ class NumericExpressionInputModule { @Provides @IntoMap @StringKey("MatchesUpToTrivialManipulations") - @FractionInputRules + @NumericExpressionInputRules internal fun provideNumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifier( classifierProvider: NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider ): RuleClassifier = classifierProvider.createRuleClassifier() @@ -29,7 +29,7 @@ class NumericExpressionInputModule { @Provides @IntoMap @StringKey("IsEquivalentTo") - @FractionInputRules + @NumericExpressionInputRules internal fun provideNumericExpressionInputIsEquivalentToRuleClassifier( classifierProvider: NumericExpressionInputIsEquivalentToRuleClassifierProvider ): RuleClassifier = classifierProvider.createRuleClassifier() From 69eaf25db8971d0bb8ae0539de09963714e8398e Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 15 Dec 2021 17:50:36 -0800 Subject: [PATCH 021/162] Add missing annotation for new interaction. --- .../oppia/android/domain/classify/rules/RuleQualifiers.kt | 7 +++++++ .../AlgebraicExpressionInputModule.kt | 8 ++++---- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/RuleQualifiers.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/RuleQualifiers.kt index a8965af13ae..9c38cbcef98 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/RuleQualifiers.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/RuleQualifiers.kt @@ -78,3 +78,10 @@ annotation class RatioExpressionInputRules */ @Qualifier annotation class NumericExpressionInputRules + +/** + * Corresponds to [org.oppia.android.domain.classify.RuleClassifier]s that can be used by the + * algebraic expression input interaction. + */ +@Qualifier +annotation class AlgebraicExpressionInputRules diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModule.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModule.kt index 091dc12e4e6..88019af6562 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModule.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModule.kt @@ -5,7 +5,7 @@ import dagger.Provides import dagger.multibindings.IntoMap import dagger.multibindings.StringKey import org.oppia.android.domain.classify.RuleClassifier -import org.oppia.android.domain.classify.rules.FractionInputRules +import org.oppia.android.domain.classify.rules.AlgebraicExpressionInputRules /** Module that binds rule classifiers corresponding to the algebraic expression input interaction. */ @Module @@ -13,7 +13,7 @@ class AlgebraicExpressionInputModule { @Provides @IntoMap @StringKey("MatchesExactlyWith") - @FractionInputRules + @AlgebraicExpressionInputRules internal fun provideAlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider( classifierProvider: AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider ): RuleClassifier = classifierProvider.createRuleClassifier() @@ -21,7 +21,7 @@ class AlgebraicExpressionInputModule { @Provides @IntoMap @StringKey("MatchesUpToTrivialManipulations") - @FractionInputRules + @AlgebraicExpressionInputRules internal fun provideAlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifier( classifierProvider: AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider ): RuleClassifier = classifierProvider.createRuleClassifier() @@ -29,7 +29,7 @@ class AlgebraicExpressionInputModule { @Provides @IntoMap @StringKey("IsEquivalentTo") - @FractionInputRules + @AlgebraicExpressionInputRules internal fun provideAlgebraicExpressionInputIsEquivalentToRuleClassifier( classifierProvider: AlgebraicExpressionInputIsEquivalentToRuleClassifierProvider ): RuleClassifier = classifierProvider.createRuleClassifier() From 86256d948c60be6510022ee76af6983b704a7ddf Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 15 Dec 2021 17:51:38 -0800 Subject: [PATCH 022/162] Lint fixes. --- ...cExpressionInputIsEquivalentToRuleClassifierProvider.kt | 2 +- ...ressionInputMatchesExactlyWithRuleClassifierProvider.kt | 5 +++-- ...atchesUpToTrivialManipulationsRuleClassifierProvider.kt | 5 +++-- .../AlgebraicExpressionInputModule.kt | 7 +++++-- 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputIsEquivalentToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputIsEquivalentToRuleClassifierProvider.kt index 899a14682d6..276e9de2868 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputIsEquivalentToRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputIsEquivalentToRuleClassifierProvider.kt @@ -8,9 +8,9 @@ import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider import org.oppia.android.util.logging.ConsoleLogger import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult +import org.oppia.android.util.math.MathExpressionParser.Companion.parseAlgebraicExpression import org.oppia.android.util.math.toPolynomial import javax.inject.Inject -import org.oppia.android.util.math.MathExpressionParser.Companion.parseAlgebraicExpression class AlgebraicExpressionInputIsEquivalentToRuleClassifierProvider @Inject constructor( private val classifierFactory: GenericRuleClassifier.Factory, diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider.kt index fb521383d4c..b23d434d2d6 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider.kt @@ -8,8 +8,8 @@ import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider import org.oppia.android.util.logging.ConsoleLogger import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult -import javax.inject.Inject import org.oppia.android.util.math.MathExpressionParser.Companion.parseAlgebraicExpression +import javax.inject.Inject class AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider @Inject constructor( private val classifierFactory: GenericRuleClassifier.Factory, @@ -35,7 +35,8 @@ class AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider @Inject c } private fun parseExpression( - rawExpression: String, allowedVariables: List + rawExpression: String, + allowedVariables: List ): MathExpression? { return when (val expResult = parseAlgebraicExpression(rawExpression, allowedVariables)) { is MathParsingResult.Success -> expResult.result diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt index b56fe353d39..a7062556901 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt @@ -8,9 +8,9 @@ import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider import org.oppia.android.util.logging.ConsoleLogger import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult +import org.oppia.android.util.math.MathExpressionParser.Companion.parseAlgebraicExpression import org.oppia.android.util.math.toComparableOperationList import javax.inject.Inject -import org.oppia.android.util.math.MathExpressionParser.Companion.parseAlgebraicExpression class AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider @Inject constructor( @@ -37,7 +37,8 @@ class AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvi } private fun parseComparableOperationList( - rawExpression: String, allowedVariables: List + rawExpression: String, + allowedVariables: List ): ComparableOperationList? { return when (val expResult = parseAlgebraicExpression(rawExpression, allowedVariables)) { is MathParsingResult.Success -> expResult.result.toComparableOperationList() diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModule.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModule.kt index 88019af6562..810f868808a 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModule.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModule.kt @@ -7,7 +7,9 @@ import dagger.multibindings.StringKey import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.AlgebraicExpressionInputRules -/** Module that binds rule classifiers corresponding to the algebraic expression input interaction. */ +/** + * Module that binds rule classifiers corresponding to the algebraic expression input interaction. + */ @Module class AlgebraicExpressionInputModule { @Provides @@ -23,7 +25,8 @@ class AlgebraicExpressionInputModule { @StringKey("MatchesUpToTrivialManipulations") @AlgebraicExpressionInputRules internal fun provideAlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifier( - classifierProvider: AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider + classifierProvider: + AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider ): RuleClassifier = classifierProvider.createRuleClassifier() @Provides From 07dcc94fad4cdb60cade7da79d7a4c78a3620b53 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 15 Dec 2021 18:07:05 -0800 Subject: [PATCH 023/162] Add math equation input classifiers. --- .../domain/classify/rules/RuleQualifiers.kt | 7 ++ .../AlgebraicExpressionInputModule.kt | 9 ++- ...putIsEquivalentToRuleClassifierProvider.kt | 78 +++++++++++++++++++ ...atchesExactlyWithRuleClassifierProvider.kt | 63 +++++++++++++++ ...vialManipulationsRuleClassifierProvider.kt | 71 +++++++++++++++++ .../MathEquationInputModule.kt | 37 +++++++++ 6 files changed, 262 insertions(+), 3 deletions(-) create mode 100644 domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputIsEquivalentToRuleClassifierProvider.kt create mode 100644 domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesExactlyWithRuleClassifierProvider.kt create mode 100644 domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt create mode 100644 domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputModule.kt diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/RuleQualifiers.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/RuleQualifiers.kt index 9c38cbcef98..ec08463b944 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/RuleQualifiers.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/RuleQualifiers.kt @@ -85,3 +85,10 @@ annotation class NumericExpressionInputRules */ @Qualifier annotation class AlgebraicExpressionInputRules + +/** + * Corresponds to [org.oppia.android.domain.classify.RuleClassifier]s that can be used by the + * math equation input interaction. + */ +@Qualifier +annotation class MathEquationInputRules diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModule.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModule.kt index 810f868808a..bdff7180826 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModule.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModule.kt @@ -6,6 +6,9 @@ import dagger.multibindings.IntoMap import dagger.multibindings.StringKey import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.AlgebraicExpressionInputRules +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputIsEquivalentToRuleClassifierProvider +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputMatchesExactlyWithRuleClassifierProvider +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProvider /** * Module that binds rule classifiers corresponding to the algebraic expression input interaction. @@ -17,7 +20,7 @@ class AlgebraicExpressionInputModule { @StringKey("MatchesExactlyWith") @AlgebraicExpressionInputRules internal fun provideAlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider( - classifierProvider: AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider + classifierProvider: MathEquationInputMatchesExactlyWithRuleClassifierProvider ): RuleClassifier = classifierProvider.createRuleClassifier() @Provides @@ -26,7 +29,7 @@ class AlgebraicExpressionInputModule { @AlgebraicExpressionInputRules internal fun provideAlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifier( classifierProvider: - AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider + MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProvider ): RuleClassifier = classifierProvider.createRuleClassifier() @Provides @@ -34,6 +37,6 @@ class AlgebraicExpressionInputModule { @StringKey("IsEquivalentTo") @AlgebraicExpressionInputRules internal fun provideAlgebraicExpressionInputIsEquivalentToRuleClassifier( - classifierProvider: AlgebraicExpressionInputIsEquivalentToRuleClassifierProvider + classifierProvider: MathEquationInputIsEquivalentToRuleClassifierProvider ): RuleClassifier = classifierProvider.createRuleClassifier() } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputIsEquivalentToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputIsEquivalentToRuleClassifierProvider.kt new file mode 100644 index 00000000000..07de228a104 --- /dev/null +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputIsEquivalentToRuleClassifierProvider.kt @@ -0,0 +1,78 @@ +package org.oppia.android.domain.classify.rules.mathequationinput + +import org.oppia.android.app.model.InteractionObject +import org.oppia.android.app.model.Polynomial +import org.oppia.android.domain.classify.ClassificationContext +import org.oppia.android.domain.classify.RuleClassifier +import org.oppia.android.domain.classify.rules.GenericRuleClassifier +import org.oppia.android.domain.classify.rules.RuleClassifierProvider +import org.oppia.android.util.logging.ConsoleLogger +import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult +import org.oppia.android.util.math.MathExpressionParser.Companion.parseAlgebraicEquation +import org.oppia.android.util.math.toPolynomial +import javax.inject.Inject + +class MathEquationInputIsEquivalentToRuleClassifierProvider @Inject constructor( + private val classifierFactory: GenericRuleClassifier.Factory, + private val consoleLogger: ConsoleLogger +) : RuleClassifierProvider, GenericRuleClassifier.SingleInputMatcher { + override fun createRuleClassifier(): RuleClassifier { + return classifierFactory.createSingleInputClassifier( + expectedObjectType = InteractionObject.ObjectTypeCase.MATH_EXPRESSION, + inputParameterName = "x", + matcher = this + ) + } + + override fun matches( + answer: String, + input: String, + classificationContext: ClassificationContext + ): Boolean { + val allowedVariables = classificationContext.extractAllowedVariables() + val (answerLhs, answerRhs) = parsePolynomials(answer, allowedVariables) ?: return false + val (inputLhs, inputRhs) = parsePolynomials(input, allowedVariables) ?: return false + + // Sides may cross-match (i.e. it's fine to reorder around the '='). + return (answerLhs == inputLhs && answerRhs == inputRhs) || + (answerLhs == inputRhs && answerRhs == inputLhs) + } + + private fun parsePolynomials( + rawEquation: String, + allowedVariables: List + ): Pair? { + return when (val eqResult = parseAlgebraicEquation(rawEquation, allowedVariables)) { + is MathParsingResult.Success -> { + val lhsExp = eqResult.result.leftSide.toPolynomial() + val rhsExp = eqResult.result.rightSide.toPolynomial() + if (lhsExp != null && rhsExp != null) { + lhsExp to rhsExp + } else { + consoleLogger.w( + "AlgebraEqEquivalent", "Equation is not a supported polynomial: $rawEquation." + ) + null + } + } + is MathParsingResult.Failure -> { + consoleLogger.e( + "AlgebraEqEquivalent", + "Encountered equation that failed parsing. Equation: $rawEquation." + + " Failure: ${eqResult.error}." + ) + null + } + } + } + + private companion object { + private fun ClassificationContext.extractAllowedVariables(): List { + return customizationArgs["customOskLetters"] + ?.schemaObjectList + ?.schemaObjectList + ?.map { it.normalizedString } + ?: listOf() + } + } +} diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesExactlyWithRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesExactlyWithRuleClassifierProvider.kt new file mode 100644 index 00000000000..3bb7e3d875c --- /dev/null +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesExactlyWithRuleClassifierProvider.kt @@ -0,0 +1,63 @@ +package org.oppia.android.domain.classify.rules.mathequationinput + +import org.oppia.android.app.model.InteractionObject +import org.oppia.android.app.model.MathEquation +import org.oppia.android.domain.classify.ClassificationContext +import org.oppia.android.domain.classify.RuleClassifier +import org.oppia.android.domain.classify.rules.GenericRuleClassifier +import org.oppia.android.domain.classify.rules.RuleClassifierProvider +import org.oppia.android.util.logging.ConsoleLogger +import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult +import org.oppia.android.util.math.MathExpressionParser.Companion.parseAlgebraicEquation +import javax.inject.Inject + +class MathEquationInputMatchesExactlyWithRuleClassifierProvider @Inject constructor( + private val classifierFactory: GenericRuleClassifier.Factory, + private val consoleLogger: ConsoleLogger +) : RuleClassifierProvider, GenericRuleClassifier.SingleInputMatcher { + override fun createRuleClassifier(): RuleClassifier { + return classifierFactory.createSingleInputClassifier( + expectedObjectType = InteractionObject.ObjectTypeCase.MATH_EXPRESSION, + inputParameterName = "x", + matcher = this + ) + } + + override fun matches( + answer: String, + input: String, + classificationContext: ClassificationContext + ): Boolean { + val allowedVariables = classificationContext.extractAllowedVariables() + val answerEquation = parseEquation(answer, allowedVariables) ?: return false + val inputEquation = parseEquation(input, allowedVariables) ?: return false + return answerEquation == inputEquation + } + + private fun parseEquation( + rawEquation: String, + allowedVariables: List + ): MathEquation? { + return when (val eqResult = parseAlgebraicEquation(rawEquation, allowedVariables)) { + is MathParsingResult.Success -> eqResult.result + is MathParsingResult.Failure -> { + consoleLogger.e( + "AlgebraEqMatchesExact", + "Encountered equation that failed parsing. Equation: $rawEquation." + + " Failure: ${eqResult.error}." + ) + null + } + } + } + + private companion object { + private fun ClassificationContext.extractAllowedVariables(): List { + return customizationArgs["customOskLetters"] + ?.schemaObjectList + ?.schemaObjectList + ?.map { it.normalizedString } + ?: listOf() + } + } +} diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt new file mode 100644 index 00000000000..64b5e6215f6 --- /dev/null +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt @@ -0,0 +1,71 @@ +package org.oppia.android.domain.classify.rules.mathequationinput + +import org.oppia.android.app.model.ComparableOperationList +import org.oppia.android.app.model.InteractionObject +import org.oppia.android.domain.classify.ClassificationContext +import org.oppia.android.domain.classify.RuleClassifier +import org.oppia.android.domain.classify.rules.GenericRuleClassifier +import org.oppia.android.domain.classify.rules.RuleClassifierProvider +import org.oppia.android.util.logging.ConsoleLogger +import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult +import org.oppia.android.util.math.MathExpressionParser.Companion.parseAlgebraicEquation +import org.oppia.android.util.math.toComparableOperationList +import javax.inject.Inject + +class MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProvider +@Inject constructor( + private val classifierFactory: GenericRuleClassifier.Factory, + private val consoleLogger: ConsoleLogger +) : RuleClassifierProvider, GenericRuleClassifier.SingleInputMatcher { + override fun createRuleClassifier(): RuleClassifier { + return classifierFactory.createSingleInputClassifier( + expectedObjectType = InteractionObject.ObjectTypeCase.MATH_EXPRESSION, + inputParameterName = "x", + matcher = this + ) + } + + override fun matches( + answer: String, + input: String, + classificationContext: ClassificationContext + ): Boolean { + val allowedVariables = classificationContext.extractAllowedVariables() + val (answerLhs, answerRhs) = parseComparableLists(answer, allowedVariables) ?: return false + val (inputLhs, inputRhs) = parseComparableLists(input, allowedVariables) ?: return false + + // Sides must match (reordering around the '=' is not allowed by this classifier). + return answerLhs == inputLhs && answerRhs == inputRhs + } + + private fun parseComparableLists( + rawEquation: String, + allowedVariables: List + ): Pair? { + return when (val eqResult = parseAlgebraicEquation(rawEquation, allowedVariables)) { + is MathParsingResult.Success -> { + val lhsExp = eqResult.result.leftSide + val rhsExp = eqResult.result.rightSide + lhsExp.toComparableOperationList() to rhsExp.toComparableOperationList() + } + is MathParsingResult.Failure -> { + consoleLogger.e( + "AlgebraEqTrivialManips", + "Encountered equation that failed parsing. Equation: $rawEquation." + + " Failure: ${eqResult.error}." + ) + null + } + } + } + + private companion object { + private fun ClassificationContext.extractAllowedVariables(): List { + return customizationArgs["customOskLetters"] + ?.schemaObjectList + ?.schemaObjectList + ?.map { it.normalizedString } + ?: listOf() + } + } +} diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputModule.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputModule.kt new file mode 100644 index 00000000000..8e8bc03f75d --- /dev/null +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputModule.kt @@ -0,0 +1,37 @@ +package org.oppia.android.domain.classify.rules.mathequationinput + +import dagger.Module +import dagger.Provides +import dagger.multibindings.IntoMap +import dagger.multibindings.StringKey +import org.oppia.android.domain.classify.RuleClassifier +import org.oppia.android.domain.classify.rules.MathEquationInputRules + +/** Module that binds rule classifiers corresponding to the math equation input interaction. */ +@Module +class MathEquationInputModule { + @Provides + @IntoMap + @StringKey("MatchesExactlyWith") + @MathEquationInputRules + internal fun provideMathEquationInputMatchesExactlyWithRuleClassifier( + classifierProvider: MathEquationInputMatchesExactlyWithRuleClassifierProvider + ): RuleClassifier = classifierProvider.createRuleClassifier() + + @Provides + @IntoMap + @StringKey("MatchesUpToTrivialManipulations") + @MathEquationInputRules + internal fun provideMathEquationInputMatchesUpToTrivialManipulationsRuleClassifier( + classifierProvider: + MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProvider + ): RuleClassifier = classifierProvider.createRuleClassifier() + + @Provides + @IntoMap + @StringKey("IsEquivalentTo") + @MathEquationInputRules + internal fun provideMathEquationInputIsEquivalentToRuleClassifier( + classifierProvider: MathEquationInputIsEquivalentToRuleClassifierProvider + ): RuleClassifier = classifierProvider.createRuleClassifier() +} From 42684b9e2250a7a613835cd9cd75e0b89c636a85 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 15 Dec 2021 18:07:36 -0800 Subject: [PATCH 024/162] Fix provider name. --- .../numericexpressioninput/NumericExpressionInputModule.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputModule.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputModule.kt index 4a74256a777..ca42ea19de0 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputModule.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputModule.kt @@ -14,7 +14,7 @@ class NumericExpressionInputModule { @IntoMap @StringKey("MatchesExactlyWith") @NumericExpressionInputRules - internal fun provideNumericExpressionInputMatchesExactlyWithRuleClassifierProvider( + internal fun provideNumericExpressionInputMatchesExactlyWithRuleClassifier( classifierProvider: NumericExpressionInputMatchesExactlyWithRuleClassifierProvider ): RuleClassifier = classifierProvider.createRuleClassifier() From aff5bc69fb64c85c466379b3cfe8580ee7190877 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 15 Dec 2021 18:08:11 -0800 Subject: [PATCH 025/162] Fix provider name. --- .../algebraicexpressioninput/AlgebraicExpressionInputModule.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModule.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModule.kt index 810f868808a..9923355076f 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModule.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModule.kt @@ -16,7 +16,7 @@ class AlgebraicExpressionInputModule { @IntoMap @StringKey("MatchesExactlyWith") @AlgebraicExpressionInputRules - internal fun provideAlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider( + internal fun provideAlgebraicExpressionInputMatchesExactlyWithRuleClassifier( classifierProvider: AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider ): RuleClassifier = classifierProvider.createRuleClassifier() From ebd2073b864a1b0b00eeca5626f57d14d3233f9a Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 15 Dec 2021 18:10:39 -0800 Subject: [PATCH 026/162] Fix module regression from earlier commit. --- .../AlgebraicExpressionInputModule.kt | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModule.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModule.kt index 1fe035f285d..9923355076f 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModule.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModule.kt @@ -6,9 +6,6 @@ import dagger.multibindings.IntoMap import dagger.multibindings.StringKey import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.AlgebraicExpressionInputRules -import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputIsEquivalentToRuleClassifierProvider -import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputMatchesExactlyWithRuleClassifierProvider -import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProvider /** * Module that binds rule classifiers corresponding to the algebraic expression input interaction. @@ -29,7 +26,7 @@ class AlgebraicExpressionInputModule { @AlgebraicExpressionInputRules internal fun provideAlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifier( classifierProvider: - MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProvider + AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider ): RuleClassifier = classifierProvider.createRuleClassifier() @Provides @@ -37,6 +34,6 @@ class AlgebraicExpressionInputModule { @StringKey("IsEquivalentTo") @AlgebraicExpressionInputRules internal fun provideAlgebraicExpressionInputIsEquivalentToRuleClassifier( - classifierProvider: MathEquationInputIsEquivalentToRuleClassifierProvider + classifierProvider: AlgebraicExpressionInputIsEquivalentToRuleClassifierProvider ): RuleClassifier = classifierProvider.createRuleClassifier() } From 46b765567d194cfb0e2c7f616f0ae9bf238ac944 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 15 Dec 2021 22:23:14 -0800 Subject: [PATCH 027/162] Enable new math classifiers. Also, add the classifiers' new modules to all affected tests, plus both the prod and instrumentation application components. --- .../app/application/ApplicationComponent.kt | 5 +++ .../AdministratorControlsActivityTest.kt | 7 ++++- .../AppVersionActivityTest.kt | 7 ++++- .../CompletedStoryListActivityTest.kt | 7 ++++- .../LessonThumbnailImageViewTest.kt | 7 ++++- .../ImageViewBindingAdaptersTest.kt | 7 ++++- .../databinding/MarginBindingAdaptersTest.kt | 7 ++++- ...StateAssemblerMarginBindingAdaptersTest.kt | 7 ++++- ...tateAssemblerPaddingBindingAdaptersTest.kt | 7 ++++- .../databinding/ViewBindingAdaptersTest.kt | 7 ++++- .../DeveloperOptionsActivityTest.kt | 7 ++++- .../DeveloperOptionsFragmentTest.kt | 7 ++++- .../MarkChaptersCompletedActivityTest.kt | 7 ++++- .../MarkChaptersCompletedFragmentTest.kt | 7 ++++- .../MarkStoriesCompletedActivityTest.kt | 7 ++++- .../MarkStoriesCompletedFragmentTest.kt | 7 ++++- .../MarkTopicsCompletedActivityTest.kt | 7 ++++- .../MarkTopicsCompletedFragmentTest.kt | 7 ++++- .../devoptions/ViewEventLogsActivityTest.kt | 7 ++++- .../devoptions/ViewEventLogsFragmentTest.kt | 7 ++++- .../ForceNetworkTypeActivityTest.kt | 7 ++++- .../ForceNetworkTypeFragmentTest.kt | 7 ++++- .../android/app/faq/FAQListFragmentTest.kt | 7 ++++- .../android/app/faq/FAQSingleActivityTest.kt | 7 ++++- .../android/app/faq/FaqListActivityTest.kt | 7 ++++- .../android/app/help/HelpActivityTest.kt | 7 ++++- .../android/app/help/HelpFragmentTest.kt | 7 ++++- .../android/app/home/HomeActivityTest.kt | 7 ++++- .../app/home/RecentlyPlayedFragmentTest.kt | 7 ++++- .../app/home/TopicSummaryViewModelTest.kt | 7 ++++- .../android/app/home/WelcomeViewModelTest.kt | 7 ++++- .../PromotedStoryListViewModelTest.kt | 7 ++++- .../PromotedStoryViewModelTest.kt | 7 ++++- .../mydownloads/MyDownloadsFragmentTest.kt | 7 ++++- .../app/onboarding/OnboardingActivityTest.kt | 7 ++++- .../app/onboarding/OnboardingFragmentTest.kt | 7 ++++- .../OngoingTopicListActivityTest.kt | 7 ++++- .../app/options/AppLanguageActivityTest.kt | 7 ++++- .../app/options/AppLanguageFragmentTest.kt | 7 ++++- .../app/options/AudioLanguageActivityTest.kt | 7 ++++- .../app/options/AudioLanguageFragmentTest.kt | 7 ++++- .../app/options/OptionsActivityTest.kt | 7 ++++- .../app/options/OptionsFragmentTest.kt | 7 ++++- .../options/ReadingTextSizeActivityTest.kt | 7 ++++- .../options/ReadingTextSizeFragmentTest.kt | 7 ++++- .../app/parser/CustomBulletSpanTest.kt | 7 ++++- .../android/app/parser/HtmlParserTest.kt | 7 ++++- .../app/player/audio/AudioFragmentTest.kt | 7 ++++- .../exploration/ExplorationActivityTest.kt | 7 ++++- .../app/player/state/StateFragmentTest.kt | 7 ++++- .../app/profile/AddProfileActivityTest.kt | 7 ++++- .../app/profile/AdminAuthActivityTest.kt | 7 ++++- .../app/profile/AdminPinActivityTest.kt | 7 ++++- .../app/profile/PinPasswordActivityTest.kt | 7 ++++- .../app/profile/ProfileChooserFragmentTest.kt | 7 ++++- .../ProfilePictureActivityTest.kt | 7 ++++- .../ProfileProgressActivityTest.kt | 7 ++++- .../ProfileProgressFragmentTest.kt | 7 ++++- .../app/recyclerview/BindableAdapterTest.kt | 7 ++++- .../resumelesson/ResumeLessonActivityTest.kt | 7 ++++- .../resumelesson/ResumeLessonFragmentTest.kt | 7 ++++- .../profile/ProfileEditActivityTest.kt | 7 ++++- .../profile/ProfileListActivityTest.kt | 7 ++++- .../profile/ProfileListFragmentTest.kt | 7 ++++- .../profile/ProfileRenameActivityTest.kt | 7 ++++- .../profile/ProfileRenameFragmentTest.kt | 7 ++++- .../profile/ProfileResetPinActivityTest.kt | 7 ++++- .../android/app/splash/SplashActivityTest.kt | 7 ++++- .../android/app/story/StoryActivityTest.kt | 7 ++++- .../android/app/story/StoryFragmentTest.kt | 7 ++++- .../app/testing/DragDropTestActivityTest.kt | 7 ++++- ...ImageRegionSelectionInteractionViewTest.kt | 7 ++++- .../InputInteractionViewTestActivityTest.kt | 7 ++++- .../NavigationDrawerActivityDebugTest.kt | 7 ++++- .../NavigationDrawerActivityProdTest.kt | 6 +++- ...tFontScaleConfigurationUtilActivityTest.kt | 7 ++++- .../testing/TopicTestActivityForStoryTest.kt | 7 ++++- .../app/thirdparty/LicenseListActivityTest.kt | 7 ++++- .../app/thirdparty/LicenseListFragmentTest.kt | 7 ++++- .../LicenseTextViewerActivityTest.kt | 7 ++++- .../LicenseTextViewerFragmentTest.kt | 7 ++++- .../ThirdPartyDependencyListActivityTest.kt | 7 ++++- .../ThirdPartyDependencyListFragmentTest.kt | 7 ++++- .../android/app/topic/TopicActivityTest.kt | 7 ++++- .../android/app/topic/TopicFragmentTest.kt | 7 ++++- .../conceptcard/ConceptCardFragmentTest.kt | 7 ++++- .../app/topic/info/TopicInfoFragmentTest.kt | 7 ++++- .../topic/lessons/TopicLessonsFragmentTest.kt | 7 ++++- .../practice/TopicPracticeFragmentTest.kt | 7 ++++- .../QuestionPlayerActivityTest.kt | 7 ++++- .../revision/TopicRevisionFragmentTest.kt | 7 ++++- .../revisioncard/RevisionCardActivityTest.kt | 7 ++++- .../revisioncard/RevisionCardFragmentTest.kt | 7 ++++- .../app/utility/RatioExtensionsTest.kt | 7 ++++- .../walkthrough/WalkthroughActivityTest.kt | 7 ++++- .../WalkthroughFinalFragmentTest.kt | 7 ++++- .../WalkthroughTopicListFragmentTest.kt | 7 ++++- .../WalkthroughWelcomeFragmentTest.kt | 7 ++++- .../activity/ActivityIntentFactoriesTest.kt | 7 ++++- .../android/app/home/HomeActivityLocalTest.kt | 6 +++- .../app/parser/StringToFractionParserTest.kt | 7 ++++- .../app/parser/StringToRatioParserTest.kt | 7 ++++- .../ExplorationActivityLocalTest.kt | 6 +++- .../player/state/StateFragmentLocalTest.kt | 7 ++++- .../ProfileChooserFragmentLocalTest.kt | 7 ++++- .../app/story/StoryActivityLocalTest.kt | 7 ++++- .../app/testing/CompletedStoryListSpanTest.kt | 6 +++- .../oppia/android/app/testing/HomeSpanTest.kt | 6 +++- .../app/testing/OngoingTopicListSpanTest.kt | 6 +++- .../PlatformParameterIntegrationTest.kt | 7 ++++- .../app/testing/ProfileChooserSpanTest.kt | 6 +++- .../app/testing/ProfileProgressSpanCount.kt | 6 +++- .../app/testing/RecentlyPlayedSpanTest.kt | 6 +++- .../app/testing/TopicRevisionSpanTest.kt | 6 +++- .../app/testing/activity/TestActivityTest.kt | 7 ++++- .../AdministratorControlsFragmentTest.kt | 7 ++++- .../testing/options/OptionsFragmentTest.kt | 7 ++++- .../player/split/PlayerSplitScreenTesting.kt | 6 +++- .../state/StateFragmentAccessibilityTest.kt | 6 +++- .../topic/info/TopicInfoFragmentLocalTest.kt | 7 ++++- .../lessons/TopicLessonsFragmentLocalTest.kt | 7 ++++- .../QuestionPlayerActivityLocalTest.kt | 7 ++++- .../RevisionCardActivityLocalTest.kt | 7 ++++- .../AppLanguageResourceHandlerTest.kt | 7 ++++- .../AppLanguageWatcherMixinTest.kt | 7 ++++- .../app/utility/datetime/DateTimeUtilTest.kt | 7 ++++- .../domain/classify/InteractionsModule.kt | 31 +++++++++++++++++++ .../AnswerClassificationControllerTest.kt | 7 ++++- .../ExplorationDataControllerTest.kt | 6 +++- .../ExplorationProgressControllerTest.kt | 6 +++- ...uestionAssessmentProgressControllerTest.kt | 6 +++- .../QuestionTrainingControllerTest.kt | 7 ++++- .../application/TestApplicationComponent.kt | 5 +++ ...alizeDefaultLocaleRuleCustomContextTest.kt | 7 ++++- ...InitializeDefaultLocaleRuleOmissionTest.kt | 7 ++++- .../junit/InitializeDefaultLocaleRuleTest.kt | 7 ++++- 136 files changed, 824 insertions(+), 133 deletions(-) diff --git a/app/src/main/java/org/oppia/android/app/application/ApplicationComponent.kt b/app/src/main/java/org/oppia/android/app/application/ApplicationComponent.kt index dbb6b7dd5d8..0f708fef5f8 100644 --- a/app/src/main/java/org/oppia/android/app/application/ApplicationComponent.kt +++ b/app/src/main/java/org/oppia/android/app/application/ApplicationComponent.kt @@ -14,13 +14,16 @@ import org.oppia.android.app.translation.ActivityRecreatorProdModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -93,6 +96,8 @@ import javax.inject.Singleton DeveloperOptionsModule::class, PlatformParameterSyncUpWorkerModule::class, NetworkConnectionUtilDebugModule::class, NetworkConfigProdModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorProdModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class, // TODO(#59): Remove this module once we completely migrate to Bazel from Gradle as we can then // directly exclude debug files from the build and thus won't be requiring this module. NetworkConnectionDebugUtilModule::class diff --git a/app/src/sharedTest/java/org/oppia/android/app/administratorcontrols/AdministratorControlsActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/administratorcontrols/AdministratorControlsActivityTest.kt index d88d63e1650..159cce60276 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/administratorcontrols/AdministratorControlsActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/administratorcontrols/AdministratorControlsActivityTest.kt @@ -68,13 +68,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -719,7 +722,9 @@ class AdministratorControlsActivityTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/administratorcontrols/AppVersionActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/administratorcontrols/AppVersionActivityTest.kt index 801af1bccff..6922b83fb3d 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/administratorcontrols/AppVersionActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/administratorcontrols/AppVersionActivityTest.kt @@ -48,13 +48,16 @@ import org.oppia.android.app.utility.getVersionName import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -279,7 +282,9 @@ class AppVersionActivityTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/completedstorylist/CompletedStoryListActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/completedstorylist/CompletedStoryListActivityTest.kt index 3ac470b78ad..d5117f9b248 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/completedstorylist/CompletedStoryListActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/completedstorylist/CompletedStoryListActivityTest.kt @@ -49,13 +49,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -502,7 +505,9 @@ class CompletedStoryListActivityTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/customview/LessonThumbnailImageViewTest.kt b/app/src/sharedTest/java/org/oppia/android/app/customview/LessonThumbnailImageViewTest.kt index 8b577dbc71d..01b3c5a1ee2 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/customview/LessonThumbnailImageViewTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/customview/LessonThumbnailImageViewTest.kt @@ -32,13 +32,16 @@ import org.oppia.android.app.utility.EspressoTestsMatchers.withDrawable import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -160,7 +163,9 @@ class LessonThumbnailImageViewTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/databinding/ImageViewBindingAdaptersTest.kt b/app/src/sharedTest/java/org/oppia/android/app/databinding/ImageViewBindingAdaptersTest.kt index d342a5e559d..b15930dedaf 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/databinding/ImageViewBindingAdaptersTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/databinding/ImageViewBindingAdaptersTest.kt @@ -42,13 +42,16 @@ import org.oppia.android.app.utility.EspressoTestsMatchers.withDrawable import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -209,7 +212,9 @@ class ImageViewBindingAdaptersTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) /** Create a TestApplicationComponent. */ diff --git a/app/src/sharedTest/java/org/oppia/android/app/databinding/MarginBindingAdaptersTest.kt b/app/src/sharedTest/java/org/oppia/android/app/databinding/MarginBindingAdaptersTest.kt index c079fd4dda3..bb2a7fe3506 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/databinding/MarginBindingAdaptersTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/databinding/MarginBindingAdaptersTest.kt @@ -43,13 +43,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -297,7 +300,9 @@ class MarginBindingAdaptersTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) /** Create a TestApplicationComponent. */ diff --git a/app/src/sharedTest/java/org/oppia/android/app/databinding/StateAssemblerMarginBindingAdaptersTest.kt b/app/src/sharedTest/java/org/oppia/android/app/databinding/StateAssemblerMarginBindingAdaptersTest.kt index 270c740a1d2..080a013ee66 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/databinding/StateAssemblerMarginBindingAdaptersTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/databinding/StateAssemblerMarginBindingAdaptersTest.kt @@ -45,13 +45,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -485,7 +488,9 @@ class StateAssemblerMarginBindingAdaptersTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) /** Create a TestApplicationComponent. */ diff --git a/app/src/sharedTest/java/org/oppia/android/app/databinding/StateAssemblerPaddingBindingAdaptersTest.kt b/app/src/sharedTest/java/org/oppia/android/app/databinding/StateAssemblerPaddingBindingAdaptersTest.kt index ddcd62043c1..2d1f8204539 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/databinding/StateAssemblerPaddingBindingAdaptersTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/databinding/StateAssemblerPaddingBindingAdaptersTest.kt @@ -43,13 +43,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -483,7 +486,9 @@ class StateAssemblerPaddingBindingAdaptersTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) diff --git a/app/src/sharedTest/java/org/oppia/android/app/databinding/ViewBindingAdaptersTest.kt b/app/src/sharedTest/java/org/oppia/android/app/databinding/ViewBindingAdaptersTest.kt index dcfb063a37d..e01404ce617 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/databinding/ViewBindingAdaptersTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/databinding/ViewBindingAdaptersTest.kt @@ -40,13 +40,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -217,7 +220,9 @@ class ViewBindingAdaptersTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) /** Create a TestApplicationComponent. */ diff --git a/app/src/sharedTest/java/org/oppia/android/app/devoptions/DeveloperOptionsActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/devoptions/DeveloperOptionsActivityTest.kt index fd2c18b6502..ddb40edd5a5 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/devoptions/DeveloperOptionsActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/devoptions/DeveloperOptionsActivityTest.kt @@ -52,13 +52,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -285,7 +288,9 @@ class DeveloperOptionsActivityTest { ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/devoptions/DeveloperOptionsFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/devoptions/DeveloperOptionsFragmentTest.kt index 379f01c84f8..1443f901790 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/devoptions/DeveloperOptionsFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/devoptions/DeveloperOptionsFragmentTest.kt @@ -52,13 +52,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -613,7 +616,9 @@ class DeveloperOptionsFragmentTest { ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkChaptersCompletedActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkChaptersCompletedActivityTest.kt index d5e639ba1d8..ee5330a99a6 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkChaptersCompletedActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkChaptersCompletedActivityTest.kt @@ -35,13 +35,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -175,7 +178,9 @@ class MarkChaptersCompletedActivityTest { ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkChaptersCompletedFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkChaptersCompletedFragmentTest.kt index 7505976ed81..2e966c81b4a 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkChaptersCompletedFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkChaptersCompletedFragmentTest.kt @@ -44,13 +44,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -879,7 +882,9 @@ class MarkChaptersCompletedFragmentTest { ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkStoriesCompletedActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkStoriesCompletedActivityTest.kt index 57b31d34318..fed5ad8a6ba 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkStoriesCompletedActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkStoriesCompletedActivityTest.kt @@ -35,13 +35,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -175,7 +178,9 @@ class MarkStoriesCompletedActivityTest { ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkStoriesCompletedFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkStoriesCompletedFragmentTest.kt index c29d95f046c..2385222bfb9 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkStoriesCompletedFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkStoriesCompletedFragmentTest.kt @@ -44,13 +44,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -580,7 +583,9 @@ class MarkStoriesCompletedFragmentTest { ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkTopicsCompletedActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkTopicsCompletedActivityTest.kt index 9e7bd458c57..4ab74ccc9ef 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkTopicsCompletedActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkTopicsCompletedActivityTest.kt @@ -35,13 +35,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -175,7 +178,9 @@ class MarkTopicsCompletedActivityTest { ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkTopicsCompletedFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkTopicsCompletedFragmentTest.kt index 26c9d35446f..1ba8cf8aa45 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkTopicsCompletedFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/devoptions/MarkTopicsCompletedFragmentTest.kt @@ -44,13 +44,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -550,7 +553,9 @@ class MarkTopicsCompletedFragmentTest { ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/devoptions/ViewEventLogsActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/devoptions/ViewEventLogsActivityTest.kt index 50a7fb1341e..52876fb584b 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/devoptions/ViewEventLogsActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/devoptions/ViewEventLogsActivityTest.kt @@ -36,13 +36,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -163,7 +166,9 @@ class ViewEventLogsActivityTest { ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/devoptions/ViewEventLogsFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/devoptions/ViewEventLogsFragmentTest.kt index cf5fbb8c656..39d046d1bd8 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/devoptions/ViewEventLogsFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/devoptions/ViewEventLogsFragmentTest.kt @@ -42,13 +42,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -619,7 +622,9 @@ class ViewEventLogsFragmentTest { ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) diff --git a/app/src/sharedTest/java/org/oppia/android/app/devoptions/forcenetworktype/ForceNetworkTypeActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/devoptions/forcenetworktype/ForceNetworkTypeActivityTest.kt index 852966ba144..91f7b1aab13 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/devoptions/forcenetworktype/ForceNetworkTypeActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/devoptions/forcenetworktype/ForceNetworkTypeActivityTest.kt @@ -36,13 +36,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -167,7 +170,9 @@ class ForceNetworkTypeActivityTest { ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) /** [ApplicationComponent] for [ForceNetworkTypeActivityTest]. */ diff --git a/app/src/sharedTest/java/org/oppia/android/app/devoptions/forcenetworktype/ForceNetworkTypeFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/devoptions/forcenetworktype/ForceNetworkTypeFragmentTest.kt index 3d42ca09eb8..09d87f436f1 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/devoptions/forcenetworktype/ForceNetworkTypeFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/devoptions/forcenetworktype/ForceNetworkTypeFragmentTest.kt @@ -41,13 +41,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -383,7 +386,9 @@ class ForceNetworkTypeFragmentTest { ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) /** [ApplicationComponent] for [ForceNetworkTypeFragmentTest]. */ diff --git a/app/src/sharedTest/java/org/oppia/android/app/faq/FAQListFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/faq/FAQListFragmentTest.kt index df66cb56cd9..0175df9bc05 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/faq/FAQListFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/faq/FAQListFragmentTest.kt @@ -47,13 +47,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -235,7 +238,9 @@ class FAQListFragmentTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/faq/FAQSingleActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/faq/FAQSingleActivityTest.kt index e7dfe213e43..c6b1bdb48e6 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/faq/FAQSingleActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/faq/FAQSingleActivityTest.kt @@ -41,13 +41,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -207,7 +210,9 @@ class FAQSingleActivityTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/faq/FaqListActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/faq/FaqListActivityTest.kt index 28f0cb11667..d217168b930 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/faq/FaqListActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/faq/FaqListActivityTest.kt @@ -30,13 +30,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -140,7 +143,9 @@ class FaqListActivityTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/help/HelpActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/help/HelpActivityTest.kt index 69bad4b72cc..48375856775 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/help/HelpActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/help/HelpActivityTest.kt @@ -29,13 +29,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -142,7 +145,9 @@ class HelpActivityTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, NetworkModule::class, ExplorationStorageModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/help/HelpFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/help/HelpFragmentTest.kt index 2edef7165b0..7d45ce45b5c 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/help/HelpFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/help/HelpFragmentTest.kt @@ -54,13 +54,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -1180,7 +1183,9 @@ class HelpFragmentTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/home/HomeActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/home/HomeActivityTest.kt index d60ec860aec..388b7d6ae21 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/home/HomeActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/home/HomeActivityTest.kt @@ -76,13 +76,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -1745,7 +1748,9 @@ class HomeActivityTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/home/RecentlyPlayedFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/home/RecentlyPlayedFragmentTest.kt index 0a525d1c9a9..ed091721a70 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/home/RecentlyPlayedFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/home/RecentlyPlayedFragmentTest.kt @@ -63,13 +63,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -1494,7 +1497,9 @@ class RecentlyPlayedFragmentTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/home/TopicSummaryViewModelTest.kt b/app/src/sharedTest/java/org/oppia/android/app/home/TopicSummaryViewModelTest.kt index 958bf9b1f15..47af9a09fb0 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/home/TopicSummaryViewModelTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/home/TopicSummaryViewModelTest.kt @@ -31,13 +31,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -361,7 +364,9 @@ class TopicSummaryViewModelTest { ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - NetworkConfigProdModule::class, PlatformParameterSingletonModule::class + NetworkConfigProdModule::class, PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/home/WelcomeViewModelTest.kt b/app/src/sharedTest/java/org/oppia/android/app/home/WelcomeViewModelTest.kt index 65a83551f3e..4cd7decfaf1 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/home/WelcomeViewModelTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/home/WelcomeViewModelTest.kt @@ -31,13 +31,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -345,7 +348,9 @@ class WelcomeViewModelTest { ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - NetworkConfigProdModule::class, PlatformParameterSingletonModule::class + NetworkConfigProdModule::class, PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/home/promotedlist/PromotedStoryListViewModelTest.kt b/app/src/sharedTest/java/org/oppia/android/app/home/promotedlist/PromotedStoryListViewModelTest.kt index 13894d9a2d4..75ec22943e1 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/home/promotedlist/PromotedStoryListViewModelTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/home/promotedlist/PromotedStoryListViewModelTest.kt @@ -32,13 +32,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -357,7 +360,9 @@ class PromotedStoryListViewModelTest { ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - NetworkConfigProdModule::class, PlatformParameterSingletonModule::class + NetworkConfigProdModule::class, PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/home/promotedlist/PromotedStoryViewModelTest.kt b/app/src/sharedTest/java/org/oppia/android/app/home/promotedlist/PromotedStoryViewModelTest.kt index 1313cc9c660..3095b208e65 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/home/promotedlist/PromotedStoryViewModelTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/home/promotedlist/PromotedStoryViewModelTest.kt @@ -30,13 +30,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -367,7 +370,9 @@ class PromotedStoryViewModelTest { ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - NetworkConfigProdModule::class, PlatformParameterSingletonModule::class + NetworkConfigProdModule::class, PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/mydownloads/MyDownloadsFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/mydownloads/MyDownloadsFragmentTest.kt index 662eeef51a6..4e218635d4d 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/mydownloads/MyDownloadsFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/mydownloads/MyDownloadsFragmentTest.kt @@ -39,13 +39,16 @@ import org.oppia.android.app.utility.EspressoTestsMatchers.matchCurrentTabTitle import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -221,7 +224,9 @@ class MyDownloadsFragmentTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/onboarding/OnboardingActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/onboarding/OnboardingActivityTest.kt index af6b7269724..91b9c1164e5 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/onboarding/OnboardingActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/onboarding/OnboardingActivityTest.kt @@ -29,13 +29,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -139,7 +142,9 @@ class OnboardingActivityTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/onboarding/OnboardingFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/onboarding/OnboardingFragmentTest.kt index 87389344071..3f6b2105c76 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/onboarding/OnboardingFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/onboarding/OnboardingFragmentTest.kt @@ -56,13 +56,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -682,7 +685,9 @@ class OnboardingFragmentTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/ongoingtopiclist/OngoingTopicListActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/ongoingtopiclist/OngoingTopicListActivityTest.kt index 171f1b0e8af..396c3c2c8ae 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/ongoingtopiclist/OngoingTopicListActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/ongoingtopiclist/OngoingTopicListActivityTest.kt @@ -48,13 +48,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -446,7 +449,9 @@ class OngoingTopicListActivityTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/options/AppLanguageActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/options/AppLanguageActivityTest.kt index 03da5a775d5..8b5e9873e87 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/options/AppLanguageActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/options/AppLanguageActivityTest.kt @@ -29,13 +29,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -149,7 +152,9 @@ class AppLanguageActivityTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/options/AppLanguageFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/options/AppLanguageFragmentTest.kt index 254e35c9113..df2e7442cae 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/options/AppLanguageFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/options/AppLanguageFragmentTest.kt @@ -38,13 +38,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -244,7 +247,9 @@ class AppLanguageFragmentTest { ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - NetworkConfigProdModule::class, PlatformParameterSingletonModule::class + NetworkConfigProdModule::class, PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/options/AudioLanguageActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/options/AudioLanguageActivityTest.kt index 7ecf36b535d..e91975c33d6 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/options/AudioLanguageActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/options/AudioLanguageActivityTest.kt @@ -29,13 +29,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -149,7 +152,9 @@ class AudioLanguageActivityTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/options/AudioLanguageFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/options/AudioLanguageFragmentTest.kt index 66de1647b23..856ee69f349 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/options/AudioLanguageFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/options/AudioLanguageFragmentTest.kt @@ -37,13 +37,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -237,7 +240,9 @@ class AudioLanguageFragmentTest { ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - NetworkConfigProdModule::class, PlatformParameterSingletonModule::class + NetworkConfigProdModule::class, PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/options/OptionsActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/options/OptionsActivityTest.kt index 5efa873b07c..09492b1bad0 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/options/OptionsActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/options/OptionsActivityTest.kt @@ -29,13 +29,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -141,7 +144,9 @@ class OptionsActivityTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/options/OptionsFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/options/OptionsFragmentTest.kt index d6339898f06..246a0929c01 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/options/OptionsFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/options/OptionsFragmentTest.kt @@ -51,13 +51,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -674,7 +677,9 @@ class OptionsFragmentTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/options/ReadingTextSizeActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/options/ReadingTextSizeActivityTest.kt index 5cd7267de8a..37f49d2f52c 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/options/ReadingTextSizeActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/options/ReadingTextSizeActivityTest.kt @@ -29,13 +29,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -149,7 +152,9 @@ class ReadingTextSizeActivityTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/options/ReadingTextSizeFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/options/ReadingTextSizeFragmentTest.kt index 566c462e0db..08d958b81ba 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/options/ReadingTextSizeFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/options/ReadingTextSizeFragmentTest.kt @@ -44,13 +44,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -303,7 +306,9 @@ class ReadingTextSizeFragmentTest { ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - NetworkConfigProdModule::class, PlatformParameterSingletonModule::class + NetworkConfigProdModule::class, PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/parser/CustomBulletSpanTest.kt b/app/src/sharedTest/java/org/oppia/android/app/parser/CustomBulletSpanTest.kt index ed7afe12eb6..159218cf20c 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/parser/CustomBulletSpanTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/parser/CustomBulletSpanTest.kt @@ -31,13 +31,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -241,7 +244,9 @@ class CustomBulletSpanTest { ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - NetworkConfigProdModule::class, PlatformParameterSingletonModule::class + NetworkConfigProdModule::class, PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) diff --git a/app/src/sharedTest/java/org/oppia/android/app/parser/HtmlParserTest.kt b/app/src/sharedTest/java/org/oppia/android/app/parser/HtmlParserTest.kt index 2c95c3add3b..6362346e0fa 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/parser/HtmlParserTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/parser/HtmlParserTest.kt @@ -57,13 +57,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -643,7 +646,9 @@ class HtmlParserTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/player/audio/AudioFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/player/audio/AudioFragmentTest.kt index 50fbd545bdb..5f803b1f60a 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/player/audio/AudioFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/player/audio/AudioFragmentTest.kt @@ -53,13 +53,16 @@ import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.audio.AudioPlayerController import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -470,7 +473,9 @@ class AudioFragmentTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/player/exploration/ExplorationActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/player/exploration/ExplorationActivityTest.kt index e79c1a441ce..16868b4f728 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/player/exploration/ExplorationActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/player/exploration/ExplorationActivityTest.kt @@ -84,13 +84,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -2037,7 +2040,9 @@ class ExplorationActivityTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, TestExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) diff --git a/app/src/sharedTest/java/org/oppia/android/app/player/state/StateFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/player/state/StateFragmentTest.kt index 6c0917f22b4..2fd6c8b24fd 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/player/state/StateFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/player/state/StateFragmentTest.kt @@ -103,13 +103,16 @@ import org.oppia.android.app.utility.clickPoint import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -2874,7 +2877,9 @@ class StateFragmentTest { ExplorationStorageModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, NetworkModule::class, NetworkConfigProdModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/profile/AddProfileActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/profile/AddProfileActivityTest.kt index 9ddc39c067d..021448bad2b 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/profile/AddProfileActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/profile/AddProfileActivityTest.kt @@ -63,13 +63,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -1711,7 +1714,9 @@ class AddProfileActivityTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/profile/AdminAuthActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/profile/AdminAuthActivityTest.kt index f8638be04a1..a666d92be7b 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/profile/AdminAuthActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/profile/AdminAuthActivityTest.kt @@ -49,13 +49,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -660,7 +663,9 @@ class AdminAuthActivityTest { ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/profile/AdminPinActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/profile/AdminPinActivityTest.kt index b028e931d58..1aa691c4a82 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/profile/AdminPinActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/profile/AdminPinActivityTest.kt @@ -58,13 +58,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -1078,7 +1081,9 @@ class AdminPinActivityTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/profile/PinPasswordActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/profile/PinPasswordActivityTest.kt index 1b2943a2d3f..4578cd593b3 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/profile/PinPasswordActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/profile/PinPasswordActivityTest.kt @@ -52,13 +52,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -1151,7 +1154,9 @@ class PinPasswordActivityTest { ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/profile/ProfileChooserFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/profile/ProfileChooserFragmentTest.kt index 7138aac599c..aea8c488ca5 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/profile/ProfileChooserFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/profile/ProfileChooserFragmentTest.kt @@ -51,13 +51,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -518,7 +521,9 @@ class ProfileChooserFragmentTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/profileprogress/ProfilePictureActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/profileprogress/ProfilePictureActivityTest.kt index feea5e9b922..701b630d964 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/profileprogress/ProfilePictureActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/profileprogress/ProfilePictureActivityTest.kt @@ -37,13 +37,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -192,7 +195,9 @@ class ProfilePictureActivityTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/profileprogress/ProfileProgressActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/profileprogress/ProfileProgressActivityTest.kt index 8284f76cf64..4f2f42f5912 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/profileprogress/ProfileProgressActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/profileprogress/ProfileProgressActivityTest.kt @@ -29,13 +29,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -143,7 +146,9 @@ class ProfileProgressActivityTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/profileprogress/ProfileProgressFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/profileprogress/ProfileProgressFragmentTest.kt index d7c34f86734..fe7ff24d608 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/profileprogress/ProfileProgressFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/profileprogress/ProfileProgressFragmentTest.kt @@ -69,13 +69,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -844,7 +847,9 @@ class ProfileProgressFragmentTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/recyclerview/BindableAdapterTest.kt b/app/src/sharedTest/java/org/oppia/android/app/recyclerview/BindableAdapterTest.kt index 8ca7d3dd9ba..e12136e90cd 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/recyclerview/BindableAdapterTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/recyclerview/BindableAdapterTest.kt @@ -68,13 +68,16 @@ import org.oppia.android.databinding.TestTextViewForIntWithDataBindingBinding import org.oppia.android.databinding.TestTextViewForLiveDataWithDataBindingBinding import org.oppia.android.databinding.TestTextViewForStringWithDataBindingBinding import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -756,7 +759,9 @@ class BindableAdapterTest { ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/resumelesson/ResumeLessonActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/resumelesson/ResumeLessonActivityTest.kt index 70e1c86d40d..17bbe1ad2b9 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/resumelesson/ResumeLessonActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/resumelesson/ResumeLessonActivityTest.kt @@ -44,13 +44,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -213,7 +216,9 @@ class ResumeLessonActivityTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/resumelesson/ResumeLessonFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/resumelesson/ResumeLessonFragmentTest.kt index 93fa9266efc..ef1954c639e 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/resumelesson/ResumeLessonFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/resumelesson/ResumeLessonFragmentTest.kt @@ -42,13 +42,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -275,7 +278,9 @@ class ResumeLessonFragmentTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileEditActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileEditActivityTest.kt index 3c11f43e592..7f351099569 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileEditActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileEditActivityTest.kt @@ -49,13 +49,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -565,7 +568,9 @@ class ProfileEditActivityTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileListActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileListActivityTest.kt index 98b36aa1f3a..e4a8d7d8ba6 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileListActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileListActivityTest.kt @@ -29,13 +29,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -139,7 +142,9 @@ class ProfileListActivityTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileListFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileListFragmentTest.kt index 841f849c063..5e6478efe3e 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileListFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileListFragmentTest.kt @@ -44,13 +44,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -377,7 +380,9 @@ class ProfileListFragmentTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileRenameActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileRenameActivityTest.kt index 16c2eb4085a..794efaf80ee 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileRenameActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileRenameActivityTest.kt @@ -30,13 +30,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -162,7 +165,9 @@ class ProfileRenameActivityTest { ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileRenameFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileRenameFragmentTest.kt index ca237fbd195..a9ecb8e5861 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileRenameFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileRenameFragmentTest.kt @@ -46,13 +46,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -451,7 +454,9 @@ class ProfileRenameFragmentTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileResetPinActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileResetPinActivityTest.kt index f36a2a3a2a2..b3e697a860e 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileResetPinActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileResetPinActivityTest.kt @@ -49,13 +49,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -1040,7 +1043,9 @@ class ProfileResetPinActivityTest { ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/splash/SplashActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/splash/SplashActivityTest.kt index 4dfa5800e92..d1c617371da 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/splash/SplashActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/splash/SplashActivityTest.kt @@ -50,13 +50,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -481,7 +484,9 @@ class SplashActivityTest { ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/story/StoryActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/story/StoryActivityTest.kt index b0e77c6d24f..e8a9327bc5d 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/story/StoryActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/story/StoryActivityTest.kt @@ -43,13 +43,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -223,7 +226,9 @@ class StoryActivityTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/story/StoryFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/story/StoryFragmentTest.kt index e056e5d7dab..d1c7ccef311 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/story/StoryFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/story/StoryFragmentTest.kt @@ -77,13 +77,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -887,7 +890,9 @@ class StoryFragmentTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/testing/DragDropTestActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/testing/DragDropTestActivityTest.kt index fe7173623e8..bcb8f2a1cf8 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/testing/DragDropTestActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/testing/DragDropTestActivityTest.kt @@ -40,13 +40,16 @@ import org.oppia.android.app.utility.RecyclerViewCoordinatesProvider import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -212,7 +215,9 @@ class DragDropTestActivityTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/testing/ImageRegionSelectionInteractionViewTest.kt b/app/src/sharedTest/java/org/oppia/android/app/testing/ImageRegionSelectionInteractionViewTest.kt index 3f12a269df1..9e3bfe67c80 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/testing/ImageRegionSelectionInteractionViewTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/testing/ImageRegionSelectionInteractionViewTest.kt @@ -51,13 +51,16 @@ import org.oppia.android.app.utility.clickPoint import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -379,7 +382,9 @@ class ImageRegionSelectionInteractionViewTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/testing/InputInteractionViewTestActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/testing/InputInteractionViewTestActivityTest.kt index d9100849877..9b7437a6d02 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/testing/InputInteractionViewTestActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/testing/InputInteractionViewTestActivityTest.kt @@ -45,13 +45,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -1028,7 +1031,9 @@ class InputInteractionViewTestActivityTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/testing/NavigationDrawerActivityDebugTest.kt b/app/src/sharedTest/java/org/oppia/android/app/testing/NavigationDrawerActivityDebugTest.kt index 9d23dd7c2d5..95365580884 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/testing/NavigationDrawerActivityDebugTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/testing/NavigationDrawerActivityDebugTest.kt @@ -66,13 +66,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -431,7 +434,9 @@ class NavigationDrawerActivityDebugTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/testing/NavigationDrawerActivityProdTest.kt b/app/src/sharedTest/java/org/oppia/android/app/testing/NavigationDrawerActivityProdTest.kt index f98a53fd6bc..32761957182 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/testing/NavigationDrawerActivityProdTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/testing/NavigationDrawerActivityProdTest.kt @@ -74,13 +74,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -980,7 +983,8 @@ class NavigationDrawerActivityProdTest { DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - NetworkConfigProdModule::class + NetworkConfigProdModule::class, NumericExpressionInputModule::class, + AlgebraicExpressionInputModule::class, MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/testing/TestFontScaleConfigurationUtilActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/testing/TestFontScaleConfigurationUtilActivityTest.kt index 5ad3aa73abc..4536b24af0f 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/testing/TestFontScaleConfigurationUtilActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/testing/TestFontScaleConfigurationUtilActivityTest.kt @@ -35,13 +35,16 @@ import org.oppia.android.app.utility.FontSizeMatcher.Companion.withFontSize import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -190,7 +193,9 @@ class TestFontScaleConfigurationUtilActivityTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/testing/TopicTestActivityForStoryTest.kt b/app/src/sharedTest/java/org/oppia/android/app/testing/TopicTestActivityForStoryTest.kt index 7e808fe423f..c706b9d6bae 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/testing/TopicTestActivityForStoryTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/testing/TopicTestActivityForStoryTest.kt @@ -41,13 +41,16 @@ import org.oppia.android.app.utility.EspressoTestsMatchers.matchCurrentTabTitle import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -187,7 +190,9 @@ class TopicTestActivityForStoryTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/thirdparty/LicenseListActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/thirdparty/LicenseListActivityTest.kt index 64b472286ea..9c108b1aed1 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/thirdparty/LicenseListActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/thirdparty/LicenseListActivityTest.kt @@ -30,13 +30,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -151,7 +154,9 @@ class LicenseListActivityTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) diff --git a/app/src/sharedTest/java/org/oppia/android/app/thirdparty/LicenseListFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/thirdparty/LicenseListFragmentTest.kt index ef451afb505..ebdec396258 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/thirdparty/LicenseListFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/thirdparty/LicenseListFragmentTest.kt @@ -46,13 +46,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -365,7 +368,9 @@ class LicenseListFragmentTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) diff --git a/app/src/sharedTest/java/org/oppia/android/app/thirdparty/LicenseTextViewerActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/thirdparty/LicenseTextViewerActivityTest.kt index 9b55e0f23df..2f296d0a7dc 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/thirdparty/LicenseTextViewerActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/thirdparty/LicenseTextViewerActivityTest.kt @@ -31,13 +31,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -160,7 +163,9 @@ class LicenseTextViewerActivityTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) diff --git a/app/src/sharedTest/java/org/oppia/android/app/thirdparty/LicenseTextViewerFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/thirdparty/LicenseTextViewerFragmentTest.kt index c4f6c6789b5..4a9ab14597c 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/thirdparty/LicenseTextViewerFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/thirdparty/LicenseTextViewerFragmentTest.kt @@ -36,13 +36,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -343,7 +346,9 @@ class LicenseTextViewerFragmentTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) diff --git a/app/src/sharedTest/java/org/oppia/android/app/thirdparty/ThirdPartyDependencyListActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/thirdparty/ThirdPartyDependencyListActivityTest.kt index 375e562c6b8..9de615de34c 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/thirdparty/ThirdPartyDependencyListActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/thirdparty/ThirdPartyDependencyListActivityTest.kt @@ -30,13 +30,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -148,7 +151,9 @@ class ThirdPartyDependencyListActivityTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) diff --git a/app/src/sharedTest/java/org/oppia/android/app/thirdparty/ThirdPartyDependencyListFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/thirdparty/ThirdPartyDependencyListFragmentTest.kt index 87e71a94ddb..55d903219bd 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/thirdparty/ThirdPartyDependencyListFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/thirdparty/ThirdPartyDependencyListFragmentTest.kt @@ -45,13 +45,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -475,7 +478,9 @@ class ThirdPartyDependencyListFragmentTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) diff --git a/app/src/sharedTest/java/org/oppia/android/app/topic/TopicActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/topic/TopicActivityTest.kt index a3cb999be4d..1a41429def0 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/topic/TopicActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/topic/TopicActivityTest.kt @@ -42,13 +42,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -197,7 +200,9 @@ class TopicActivityTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/topic/TopicFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/topic/TopicFragmentTest.kt index 551141b4af3..5f47407774b 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/topic/TopicFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/topic/TopicFragmentTest.kt @@ -53,13 +53,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -652,7 +655,9 @@ class TopicFragmentTest { ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/topic/conceptcard/ConceptCardFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/topic/conceptcard/ConceptCardFragmentTest.kt index 7de3ff0569c..0baa3f515a0 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/topic/conceptcard/ConceptCardFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/topic/conceptcard/ConceptCardFragmentTest.kt @@ -51,13 +51,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -427,7 +430,9 @@ class ConceptCardFragmentTest { ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/topic/info/TopicInfoFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/topic/info/TopicInfoFragmentTest.kt index 2ab597a75d1..42cd11e3ed3 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/topic/info/TopicInfoFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/topic/info/TopicInfoFragmentTest.kt @@ -55,13 +55,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -483,7 +486,9 @@ class TopicInfoFragmentTest { ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/topic/lessons/TopicLessonsFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/topic/lessons/TopicLessonsFragmentTest.kt index 2c4251fd0f2..18a483907d1 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/topic/lessons/TopicLessonsFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/topic/lessons/TopicLessonsFragmentTest.kt @@ -61,13 +61,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -1000,7 +1003,9 @@ class TopicLessonsFragmentTest { ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/topic/practice/TopicPracticeFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/topic/practice/TopicPracticeFragmentTest.kt index 56fa8cb4e05..ce2bebc8bf4 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/topic/practice/TopicPracticeFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/topic/practice/TopicPracticeFragmentTest.kt @@ -52,13 +52,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -424,7 +427,9 @@ class TopicPracticeFragmentTest { ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerActivityTest.kt index 6d3d747fe1d..c0a3f4595c0 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerActivityTest.kt @@ -75,13 +75,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -719,7 +722,9 @@ class QuestionPlayerActivityTest { ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/topic/revision/TopicRevisionFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/topic/revision/TopicRevisionFragmentTest.kt index e8e933c21ce..ab3d409fece 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/topic/revision/TopicRevisionFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/topic/revision/TopicRevisionFragmentTest.kt @@ -53,13 +53,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -320,7 +323,9 @@ class TopicRevisionFragmentTest { ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/topic/revisioncard/RevisionCardActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/topic/revisioncard/RevisionCardActivityTest.kt index d08e2a61010..2ae6f908dee 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/topic/revisioncard/RevisionCardActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/topic/revisioncard/RevisionCardActivityTest.kt @@ -43,13 +43,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -278,7 +281,9 @@ class RevisionCardActivityTest { ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/topic/revisioncard/RevisionCardFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/topic/revisioncard/RevisionCardFragmentTest.kt index 95f22c51025..8bff4e25299 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/topic/revisioncard/RevisionCardFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/topic/revisioncard/RevisionCardFragmentTest.kt @@ -61,13 +61,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -590,7 +593,9 @@ class RevisionCardFragmentTest { ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/utility/RatioExtensionsTest.kt b/app/src/sharedTest/java/org/oppia/android/app/utility/RatioExtensionsTest.kt index 2260eace947..ff080b94a34 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/utility/RatioExtensionsTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/utility/RatioExtensionsTest.kt @@ -28,13 +28,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -143,7 +146,9 @@ class RatioExtensionsTest { ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/walkthrough/WalkthroughActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/walkthrough/WalkthroughActivityTest.kt index 9f4fcfa2482..cd00f8aa28b 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/walkthrough/WalkthroughActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/walkthrough/WalkthroughActivityTest.kt @@ -37,13 +37,16 @@ import org.oppia.android.app.utility.ProgressMatcher.Companion.withProgress import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -195,7 +198,9 @@ class WalkthroughActivityTest { ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/walkthrough/WalkthroughFinalFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/walkthrough/WalkthroughFinalFragmentTest.kt index 6cf37058cad..ee00467d323 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/walkthrough/WalkthroughFinalFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/walkthrough/WalkthroughFinalFragmentTest.kt @@ -44,13 +44,16 @@ import org.oppia.android.app.utility.ProgressMatcher.Companion.withProgress import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -280,7 +283,9 @@ class WalkthroughFinalFragmentTest { ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/walkthrough/WalkthroughTopicListFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/walkthrough/WalkthroughTopicListFragmentTest.kt index a535b248c50..21ff6100425 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/walkthrough/WalkthroughTopicListFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/walkthrough/WalkthroughTopicListFragmentTest.kt @@ -45,13 +45,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -306,7 +309,9 @@ class WalkthroughTopicListFragmentTest { ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/walkthrough/WalkthroughWelcomeFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/walkthrough/WalkthroughWelcomeFragmentTest.kt index 849c5b61ce3..0a39884dd55 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/walkthrough/WalkthroughWelcomeFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/walkthrough/WalkthroughWelcomeFragmentTest.kt @@ -40,13 +40,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -203,7 +206,9 @@ class WalkthroughWelcomeFragmentTest { ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/test/java/org/oppia/android/app/activity/ActivityIntentFactoriesTest.kt b/app/src/test/java/org/oppia/android/app/activity/ActivityIntentFactoriesTest.kt index 4331cd3e682..9a145af0a7e 100644 --- a/app/src/test/java/org/oppia/android/app/activity/ActivityIntentFactoriesTest.kt +++ b/app/src/test/java/org/oppia/android/app/activity/ActivityIntentFactoriesTest.kt @@ -31,13 +31,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -170,7 +173,9 @@ class ActivityIntentFactoriesTest { ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - ActivityIntentFactoriesModule::class, PlatformParameterSingletonModule::class + ActivityIntentFactoriesModule::class, PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/test/java/org/oppia/android/app/home/HomeActivityLocalTest.kt b/app/src/test/java/org/oppia/android/app/home/HomeActivityLocalTest.kt index b4cbbcd59d9..8e62d7024a1 100644 --- a/app/src/test/java/org/oppia/android/app/home/HomeActivityLocalTest.kt +++ b/app/src/test/java/org/oppia/android/app/home/HomeActivityLocalTest.kt @@ -32,13 +32,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -143,7 +146,8 @@ class HomeActivityLocalTest { ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - NetworkConfigProdModule::class + NetworkConfigProdModule::class, NumericExpressionInputModule::class, + AlgebraicExpressionInputModule::class, MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/test/java/org/oppia/android/app/parser/StringToFractionParserTest.kt b/app/src/test/java/org/oppia/android/app/parser/StringToFractionParserTest.kt index 1d71c8c8afd..2e0be2a1d38 100644 --- a/app/src/test/java/org/oppia/android/app/parser/StringToFractionParserTest.kt +++ b/app/src/test/java/org/oppia/android/app/parser/StringToFractionParserTest.kt @@ -28,13 +28,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -479,7 +482,9 @@ class StringToFractionParserTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/test/java/org/oppia/android/app/parser/StringToRatioParserTest.kt b/app/src/test/java/org/oppia/android/app/parser/StringToRatioParserTest.kt index 57867f5f370..9ece48261e7 100644 --- a/app/src/test/java/org/oppia/android/app/parser/StringToRatioParserTest.kt +++ b/app/src/test/java/org/oppia/android/app/parser/StringToRatioParserTest.kt @@ -28,13 +28,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -256,7 +259,9 @@ class StringToRatioParserTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/test/java/org/oppia/android/app/player/exploration/ExplorationActivityLocalTest.kt b/app/src/test/java/org/oppia/android/app/player/exploration/ExplorationActivityLocalTest.kt index 6ff810db3a9..c721dce2972 100644 --- a/app/src/test/java/org/oppia/android/app/player/exploration/ExplorationActivityLocalTest.kt +++ b/app/src/test/java/org/oppia/android/app/player/exploration/ExplorationActivityLocalTest.kt @@ -33,13 +33,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -211,7 +214,8 @@ class ExplorationActivityLocalTest { ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - NetworkConfigProdModule::class + NetworkConfigProdModule::class, NumericExpressionInputModule::class, + AlgebraicExpressionInputModule::class, MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/test/java/org/oppia/android/app/player/state/StateFragmentLocalTest.kt b/app/src/test/java/org/oppia/android/app/player/state/StateFragmentLocalTest.kt index dc0ff28e020..4dc01162509 100644 --- a/app/src/test/java/org/oppia/android/app/player/state/StateFragmentLocalTest.kt +++ b/app/src/test/java/org/oppia/android/app/player/state/StateFragmentLocalTest.kt @@ -87,13 +87,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -2061,7 +2064,9 @@ class StateFragmentLocalTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/test/java/org/oppia/android/app/profile/ProfileChooserFragmentLocalTest.kt b/app/src/test/java/org/oppia/android/app/profile/ProfileChooserFragmentLocalTest.kt index 3c9833ab88c..08f6ac552f3 100644 --- a/app/src/test/java/org/oppia/android/app/profile/ProfileChooserFragmentLocalTest.kt +++ b/app/src/test/java/org/oppia/android/app/profile/ProfileChooserFragmentLocalTest.kt @@ -30,13 +30,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -133,7 +136,9 @@ class ProfileChooserFragmentLocalTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/test/java/org/oppia/android/app/story/StoryActivityLocalTest.kt b/app/src/test/java/org/oppia/android/app/story/StoryActivityLocalTest.kt index 05458296564..ccb38b584c9 100644 --- a/app/src/test/java/org/oppia/android/app/story/StoryActivityLocalTest.kt +++ b/app/src/test/java/org/oppia/android/app/story/StoryActivityLocalTest.kt @@ -31,13 +31,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -157,7 +160,9 @@ class StoryActivityLocalTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/test/java/org/oppia/android/app/testing/CompletedStoryListSpanTest.kt b/app/src/test/java/org/oppia/android/app/testing/CompletedStoryListSpanTest.kt index 1080c3c329b..d9d447ac23f 100644 --- a/app/src/test/java/org/oppia/android/app/testing/CompletedStoryListSpanTest.kt +++ b/app/src/test/java/org/oppia/android/app/testing/CompletedStoryListSpanTest.kt @@ -33,13 +33,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -165,7 +168,8 @@ class CompletedStoryListSpanTest { ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - NetworkConfigProdModule::class + NetworkConfigProdModule::class, NumericExpressionInputModule::class, + AlgebraicExpressionInputModule::class, MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/test/java/org/oppia/android/app/testing/HomeSpanTest.kt b/app/src/test/java/org/oppia/android/app/testing/HomeSpanTest.kt index 91d1cb2e664..842529cd959 100644 --- a/app/src/test/java/org/oppia/android/app/testing/HomeSpanTest.kt +++ b/app/src/test/java/org/oppia/android/app/testing/HomeSpanTest.kt @@ -33,13 +33,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -179,7 +182,8 @@ class HomeSpanTest { ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - NetworkConfigProdModule::class + NetworkConfigProdModule::class, NumericExpressionInputModule::class, + AlgebraicExpressionInputModule::class, MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/test/java/org/oppia/android/app/testing/OngoingTopicListSpanTest.kt b/app/src/test/java/org/oppia/android/app/testing/OngoingTopicListSpanTest.kt index 370944949d3..ce592d56c0e 100644 --- a/app/src/test/java/org/oppia/android/app/testing/OngoingTopicListSpanTest.kt +++ b/app/src/test/java/org/oppia/android/app/testing/OngoingTopicListSpanTest.kt @@ -34,13 +34,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -176,7 +179,8 @@ class OngoingTopicListSpanTest { ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - NetworkConfigProdModule::class + NetworkConfigProdModule::class, NumericExpressionInputModule::class, + AlgebraicExpressionInputModule::class, MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/test/java/org/oppia/android/app/testing/PlatformParameterIntegrationTest.kt b/app/src/test/java/org/oppia/android/app/testing/PlatformParameterIntegrationTest.kt index 73932a62756..3cad9b897a2 100644 --- a/app/src/test/java/org/oppia/android/app/testing/PlatformParameterIntegrationTest.kt +++ b/app/src/test/java/org/oppia/android/app/testing/PlatformParameterIntegrationTest.kt @@ -47,13 +47,16 @@ import org.oppia.android.data.backends.gae.OppiaRetrofit import org.oppia.android.data.backends.gae.RemoteAuthNetworkInterceptor import org.oppia.android.data.backends.gae.api.PlatformParameterService import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -349,7 +352,9 @@ class PlatformParameterIntegrationTest { ExplorationStorageModule::class, TestNetworkModule::class, RetrofitTestModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, - ActivityRecreatorTestModule::class, PlatformParameterSingletonModule::class + ActivityRecreatorTestModule::class, PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/test/java/org/oppia/android/app/testing/ProfileChooserSpanTest.kt b/app/src/test/java/org/oppia/android/app/testing/ProfileChooserSpanTest.kt index 83f154ff62d..9d428db0559 100644 --- a/app/src/test/java/org/oppia/android/app/testing/ProfileChooserSpanTest.kt +++ b/app/src/test/java/org/oppia/android/app/testing/ProfileChooserSpanTest.kt @@ -32,13 +32,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -378,7 +381,8 @@ class ProfileChooserSpanTest { ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - NetworkConfigProdModule::class + NetworkConfigProdModule::class, NumericExpressionInputModule::class, + AlgebraicExpressionInputModule::class, MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/test/java/org/oppia/android/app/testing/ProfileProgressSpanCount.kt b/app/src/test/java/org/oppia/android/app/testing/ProfileProgressSpanCount.kt index 247e6382b90..461919298a4 100644 --- a/app/src/test/java/org/oppia/android/app/testing/ProfileProgressSpanCount.kt +++ b/app/src/test/java/org/oppia/android/app/testing/ProfileProgressSpanCount.kt @@ -33,13 +33,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -162,7 +165,8 @@ class ProfileProgressSpanCount { ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - NetworkConfigProdModule::class + NetworkConfigProdModule::class, NumericExpressionInputModule::class, + AlgebraicExpressionInputModule::class, MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/test/java/org/oppia/android/app/testing/RecentlyPlayedSpanTest.kt b/app/src/test/java/org/oppia/android/app/testing/RecentlyPlayedSpanTest.kt index 1f3325e4521..f6eeaa709ce 100644 --- a/app/src/test/java/org/oppia/android/app/testing/RecentlyPlayedSpanTest.kt +++ b/app/src/test/java/org/oppia/android/app/testing/RecentlyPlayedSpanTest.kt @@ -35,13 +35,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -297,7 +300,8 @@ class RecentlyPlayedSpanTest { ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - NetworkConfigProdModule::class + NetworkConfigProdModule::class, NumericExpressionInputModule::class, + AlgebraicExpressionInputModule::class, MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/test/java/org/oppia/android/app/testing/TopicRevisionSpanTest.kt b/app/src/test/java/org/oppia/android/app/testing/TopicRevisionSpanTest.kt index c4e56b6316a..931f5c69356 100644 --- a/app/src/test/java/org/oppia/android/app/testing/TopicRevisionSpanTest.kt +++ b/app/src/test/java/org/oppia/android/app/testing/TopicRevisionSpanTest.kt @@ -32,13 +32,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -162,7 +165,8 @@ class TopicRevisionSpanTest { ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - NetworkConfigProdModule::class + NetworkConfigProdModule::class, NumericExpressionInputModule::class, + AlgebraicExpressionInputModule::class, MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/test/java/org/oppia/android/app/testing/activity/TestActivityTest.kt b/app/src/test/java/org/oppia/android/app/testing/activity/TestActivityTest.kt index 915e262e512..b13424928ea 100644 --- a/app/src/test/java/org/oppia/android/app/testing/activity/TestActivityTest.kt +++ b/app/src/test/java/org/oppia/android/app/testing/activity/TestActivityTest.kt @@ -29,13 +29,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -187,7 +190,9 @@ class TestActivityTest { ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, ActivityRecreatorTestModule::class, LocaleProdModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/test/java/org/oppia/android/app/testing/administratorcontrols/AdministratorControlsFragmentTest.kt b/app/src/test/java/org/oppia/android/app/testing/administratorcontrols/AdministratorControlsFragmentTest.kt index 0984dbbae3b..842475f0213 100644 --- a/app/src/test/java/org/oppia/android/app/testing/administratorcontrols/AdministratorControlsFragmentTest.kt +++ b/app/src/test/java/org/oppia/android/app/testing/administratorcontrols/AdministratorControlsFragmentTest.kt @@ -43,13 +43,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -222,7 +225,9 @@ class AdministratorControlsFragmentTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/test/java/org/oppia/android/app/testing/options/OptionsFragmentTest.kt b/app/src/test/java/org/oppia/android/app/testing/options/OptionsFragmentTest.kt index 7b3b36d0d01..41d04fdc19e 100644 --- a/app/src/test/java/org/oppia/android/app/testing/options/OptionsFragmentTest.kt +++ b/app/src/test/java/org/oppia/android/app/testing/options/OptionsFragmentTest.kt @@ -42,13 +42,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -271,7 +274,9 @@ class OptionsFragmentTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/test/java/org/oppia/android/app/testing/player/split/PlayerSplitScreenTesting.kt b/app/src/test/java/org/oppia/android/app/testing/player/split/PlayerSplitScreenTesting.kt index db50ed5a66b..2750f397034 100644 --- a/app/src/test/java/org/oppia/android/app/testing/player/split/PlayerSplitScreenTesting.kt +++ b/app/src/test/java/org/oppia/android/app/testing/player/split/PlayerSplitScreenTesting.kt @@ -30,13 +30,16 @@ import org.oppia.android.app.utility.SplitScreenManager import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -190,7 +193,8 @@ class PlayerSplitScreenTesting { ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - NetworkConfigProdModule::class + NetworkConfigProdModule::class, NumericExpressionInputModule::class, + AlgebraicExpressionInputModule::class, MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/test/java/org/oppia/android/app/testing/player/state/StateFragmentAccessibilityTest.kt b/app/src/test/java/org/oppia/android/app/testing/player/state/StateFragmentAccessibilityTest.kt index b198682e10c..6cc58786788 100644 --- a/app/src/test/java/org/oppia/android/app/testing/player/state/StateFragmentAccessibilityTest.kt +++ b/app/src/test/java/org/oppia/android/app/testing/player/state/StateFragmentAccessibilityTest.kt @@ -37,13 +37,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -204,7 +207,8 @@ class StateFragmentAccessibilityTest { ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - NetworkConfigProdModule::class + NetworkConfigProdModule::class, NumericExpressionInputModule::class, + AlgebraicExpressionInputModule::class, MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/test/java/org/oppia/android/app/topic/info/TopicInfoFragmentLocalTest.kt b/app/src/test/java/org/oppia/android/app/topic/info/TopicInfoFragmentLocalTest.kt index 0d22b3a092d..131d7e63b57 100644 --- a/app/src/test/java/org/oppia/android/app/topic/info/TopicInfoFragmentLocalTest.kt +++ b/app/src/test/java/org/oppia/android/app/topic/info/TopicInfoFragmentLocalTest.kt @@ -29,13 +29,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -145,7 +148,9 @@ class TopicInfoFragmentLocalTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/test/java/org/oppia/android/app/topic/lessons/TopicLessonsFragmentLocalTest.kt b/app/src/test/java/org/oppia/android/app/topic/lessons/TopicLessonsFragmentLocalTest.kt index 534a0d63c58..51045169537 100644 --- a/app/src/test/java/org/oppia/android/app/topic/lessons/TopicLessonsFragmentLocalTest.kt +++ b/app/src/test/java/org/oppia/android/app/topic/lessons/TopicLessonsFragmentLocalTest.kt @@ -28,13 +28,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -148,7 +151,9 @@ class TopicLessonsFragmentLocalTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/test/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerActivityLocalTest.kt b/app/src/test/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerActivityLocalTest.kt index 71f96f4660d..42f7fad317e 100644 --- a/app/src/test/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerActivityLocalTest.kt +++ b/app/src/test/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerActivityLocalTest.kt @@ -45,13 +45,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -411,7 +414,9 @@ class QuestionPlayerActivityLocalTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/test/java/org/oppia/android/app/topic/revisioncard/RevisionCardActivityLocalTest.kt b/app/src/test/java/org/oppia/android/app/topic/revisioncard/RevisionCardActivityLocalTest.kt index eaa6ad78915..1cdc640a5ce 100644 --- a/app/src/test/java/org/oppia/android/app/topic/revisioncard/RevisionCardActivityLocalTest.kt +++ b/app/src/test/java/org/oppia/android/app/topic/revisioncard/RevisionCardActivityLocalTest.kt @@ -28,13 +28,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -137,7 +140,9 @@ class RevisionCardActivityLocalTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/test/java/org/oppia/android/app/translation/AppLanguageResourceHandlerTest.kt b/app/src/test/java/org/oppia/android/app/translation/AppLanguageResourceHandlerTest.kt index 9ae15728414..835f8d78d19 100644 --- a/app/src/test/java/org/oppia/android/app/translation/AppLanguageResourceHandlerTest.kt +++ b/app/src/test/java/org/oppia/android/app/translation/AppLanguageResourceHandlerTest.kt @@ -34,13 +34,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -512,7 +515,9 @@ class AppLanguageResourceHandlerTest { ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleTestModule::class, ActivityRecreatorTestModule::class, - ActivityIntentFactoriesModule::class, PlatformParameterSingletonModule::class + ActivityIntentFactoriesModule::class, PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/test/java/org/oppia/android/app/translation/AppLanguageWatcherMixinTest.kt b/app/src/test/java/org/oppia/android/app/translation/AppLanguageWatcherMixinTest.kt index b86d62f1f51..6c368dad4eb 100644 --- a/app/src/test/java/org/oppia/android/app/translation/AppLanguageWatcherMixinTest.kt +++ b/app/src/test/java/org/oppia/android/app/translation/AppLanguageWatcherMixinTest.kt @@ -39,13 +39,16 @@ import org.oppia.android.app.translation.testing.TestActivityRecreator import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -260,7 +263,9 @@ class AppLanguageWatcherMixinTest { ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleTestModule::class, ActivityRecreatorTestModule::class, - ActivityIntentFactoriesModule::class, PlatformParameterSingletonModule::class + ActivityIntentFactoriesModule::class, PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/test/java/org/oppia/android/app/utility/datetime/DateTimeUtilTest.kt b/app/src/test/java/org/oppia/android/app/utility/datetime/DateTimeUtilTest.kt index 3f4504a1045..6e92fd30212 100644 --- a/app/src/test/java/org/oppia/android/app/utility/datetime/DateTimeUtilTest.kt +++ b/app/src/test/java/org/oppia/android/app/utility/datetime/DateTimeUtilTest.kt @@ -30,13 +30,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -174,7 +177,9 @@ class DateTimeUtilTest { HtmlParserEntityTypeModule::class, NetworkConnectionDebugUtilModule::class, DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/domain/src/main/java/org/oppia/android/domain/classify/InteractionsModule.kt b/domain/src/main/java/org/oppia/android/domain/classify/InteractionsModule.kt index 4175bda19eb..3dec6a50142 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/InteractionsModule.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/InteractionsModule.kt @@ -4,13 +4,16 @@ import dagger.Module import dagger.Provides import dagger.multibindings.IntoMap import dagger.multibindings.StringKey +import org.oppia.android.domain.classify.rules.AlgebraicExpressionInputRules import org.oppia.android.domain.classify.rules.ContinueRules import org.oppia.android.domain.classify.rules.DragDropSortInputRules import org.oppia.android.domain.classify.rules.FractionInputRules import org.oppia.android.domain.classify.rules.ImageClickInputRules import org.oppia.android.domain.classify.rules.ItemSelectionInputRules +import org.oppia.android.domain.classify.rules.MathEquationInputRules import org.oppia.android.domain.classify.rules.MultipleChoiceInputRules import org.oppia.android.domain.classify.rules.NumberWithUnitsRules +import org.oppia.android.domain.classify.rules.NumericExpressionInputRules import org.oppia.android.domain.classify.rules.NumericInputRules import org.oppia.android.domain.classify.rules.RatioExpressionInputRules import org.oppia.android.domain.classify.rules.TextInputRules @@ -107,4 +110,32 @@ class InteractionsModule { ): InteractionClassifier { return GenericInteractionClassifier(ruleClassifiers) } + + @Provides + @IntoMap + @StringKey("NumericExpressionInput") + fun provideNumericExpressionInputInteractionClassifier( + @NumericExpressionInputRules ruleClassifiers: Map + ): InteractionClassifier { + return GenericInteractionClassifier(ruleClassifiers) + } + + @Provides + @IntoMap + @StringKey("AlgebraicExpressionInput") + fun provideAlgebraicExpressionInputInteractionClassifier( + @AlgebraicExpressionInputRules ruleClassifiers: + Map + ): InteractionClassifier { + return GenericInteractionClassifier(ruleClassifiers) + } + + @Provides + @IntoMap + @StringKey("MathEquationInput") + fun provideMathEquationInputInteractionClassifier( + @MathEquationInputRules ruleClassifiers: Map + ): InteractionClassifier { + return GenericInteractionClassifier(ruleClassifiers) + } } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/AnswerClassificationControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/AnswerClassificationControllerTest.kt index 2f682aee259..ff0fb19c488 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/AnswerClassificationControllerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/AnswerClassificationControllerTest.kt @@ -31,13 +31,16 @@ import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createReal import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createSetOfTranslatableHtmlContentIds import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createString import org.oppia.android.domain.classify.InteractionObjectTestBuilder.createTranslatableSetOfNormalizedString +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -844,7 +847,9 @@ class AnswerClassificationControllerTest { ImageClickInputModule::class, RatioInputModule::class, LocaleProdModule::class, FakeOppiaClockModule::class, LoggerModule::class, TestDispatcherModule::class, LogStorageModule::class, NetworkConnectionUtilDebugModule::class, - TestLogReportingModule::class, AssetModule::class, RobolectricModule::class + TestLogReportingModule::class, AssetModule::class, RobolectricModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent { diff --git a/domain/src/test/java/org/oppia/android/domain/exploration/ExplorationDataControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/exploration/ExplorationDataControllerTest.kt index 1c7d718ef49..361bedc9528 100644 --- a/domain/src/test/java/org/oppia/android/domain/exploration/ExplorationDataControllerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/exploration/ExplorationDataControllerTest.kt @@ -25,13 +25,16 @@ import org.oppia.android.app.model.EphemeralState import org.oppia.android.app.model.Exploration import org.oppia.android.app.model.ExplorationCheckpoint import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -351,7 +354,8 @@ class ExplorationDataControllerTest { RatioInputModule::class, RobolectricModule::class, FakeOppiaClockModule::class, TestExplorationStorageModule::class, HintsAndSolutionConfigModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, - AssetModule::class, LocaleProdModule::class + AssetModule::class, LocaleProdModule::class, NumericExpressionInputModule::class, + AlgebraicExpressionInputModule::class, MathEquationInputModule::class ] ) interface TestApplicationComponent : DataProvidersInjector { diff --git a/domain/src/test/java/org/oppia/android/domain/exploration/ExplorationProgressControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/exploration/ExplorationProgressControllerTest.kt index a87412cf98b..337a0a98132 100644 --- a/domain/src/test/java/org/oppia/android/domain/exploration/ExplorationProgressControllerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/exploration/ExplorationProgressControllerTest.kt @@ -45,13 +45,16 @@ import org.oppia.android.app.model.TranslatableHtmlContentId import org.oppia.android.app.model.UserAnswer import org.oppia.android.app.model.WrittenTranslationLanguageSelection import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -3618,7 +3621,8 @@ class ExplorationProgressControllerTest { RatioInputModule::class, RobolectricModule::class, FakeOppiaClockModule::class, TestExplorationStorageModule::class, HintsAndSolutionConfigModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, - AssetModule::class, LocaleProdModule::class + AssetModule::class, LocaleProdModule::class, NumericExpressionInputModule::class, + AlgebraicExpressionInputModule::class, MathEquationInputModule::class ] ) interface TestApplicationComponent : DataProvidersInjector { diff --git a/domain/src/test/java/org/oppia/android/domain/question/QuestionAssessmentProgressControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/question/QuestionAssessmentProgressControllerTest.kt index c914d63f58e..5539f784c4f 100644 --- a/domain/src/test/java/org/oppia/android/domain/question/QuestionAssessmentProgressControllerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/question/QuestionAssessmentProgressControllerTest.kt @@ -38,13 +38,16 @@ import org.oppia.android.app.model.UserAnswer import org.oppia.android.app.model.UserAssessmentPerformance import org.oppia.android.app.model.WrittenTranslationLanguageSelection import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -1832,7 +1835,8 @@ class QuestionAssessmentProgressControllerTest { RatioInputModule::class, RobolectricModule::class, FakeOppiaClockModule::class, CachingTestModule::class, HintsAndSolutionConfigModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, - AssetModule::class, LocaleProdModule::class + AssetModule::class, LocaleProdModule::class, NumericExpressionInputModule::class, + AlgebraicExpressionInputModule::class, MathEquationInputModule::class ] ) interface TestApplicationComponent : DataProvidersInjector { diff --git a/domain/src/test/java/org/oppia/android/domain/question/QuestionTrainingControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/question/QuestionTrainingControllerTest.kt index 5f592a50171..f116d87232d 100644 --- a/domain/src/test/java/org/oppia/android/domain/question/QuestionTrainingControllerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/question/QuestionTrainingControllerTest.kt @@ -23,13 +23,16 @@ import org.mockito.junit.MockitoRule import org.oppia.android.app.model.EphemeralQuestion import org.oppia.android.app.model.ProfileId import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -329,7 +332,9 @@ class QuestionTrainingControllerTest { LogStorageModule::class, TestDispatcherModule::class, RatioInputModule::class, RobolectricModule::class, FakeOppiaClockModule::class, CachingTestModule::class, HintsAndSolutionConfigModule::class, HintsAndSolutionProdModule::class, - NetworkConnectionUtilDebugModule::class, AssetModule::class, LocaleProdModule::class + NetworkConnectionUtilDebugModule::class, AssetModule::class, LocaleProdModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : DataProvidersInjector { diff --git a/instrumentation/src/java/org/oppia/android/instrumentation/application/TestApplicationComponent.kt b/instrumentation/src/java/org/oppia/android/instrumentation/application/TestApplicationComponent.kt index 3e4cc2d9011..498b1f36e04 100644 --- a/instrumentation/src/java/org/oppia/android/instrumentation/application/TestApplicationComponent.kt +++ b/instrumentation/src/java/org/oppia/android/instrumentation/application/TestApplicationComponent.kt @@ -16,13 +16,16 @@ import org.oppia.android.app.topic.PracticeTabModule import org.oppia.android.app.translation.ActivityRecreatorProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -86,6 +89,8 @@ import javax.inject.Singleton DeveloperOptionsModule::class, PlatformParameterSyncUpWorkerModule::class, NetworkConnectionUtilDebugModule::class, EndToEndTestNetworkConfigModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorProdModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class, // TODO(#59): Remove this module once we completely migrate to Bazel from Gradle as we can then // directly exclude debug files from the build and thus won't be requiring this module. NetworkConnectionDebugUtilModule::class diff --git a/testing/src/test/java/org/oppia/android/testing/junit/InitializeDefaultLocaleRuleCustomContextTest.kt b/testing/src/test/java/org/oppia/android/testing/junit/InitializeDefaultLocaleRuleCustomContextTest.kt index cd6da6c4408..e434ea51251 100644 --- a/testing/src/test/java/org/oppia/android/testing/junit/InitializeDefaultLocaleRuleCustomContextTest.kt +++ b/testing/src/test/java/org/oppia/android/testing/junit/InitializeDefaultLocaleRuleCustomContextTest.kt @@ -29,13 +29,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -253,7 +256,9 @@ class InitializeDefaultLocaleRuleCustomContextTest { ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, ActivityRecreatorTestModule::class, LocaleProdModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/testing/src/test/java/org/oppia/android/testing/junit/InitializeDefaultLocaleRuleOmissionTest.kt b/testing/src/test/java/org/oppia/android/testing/junit/InitializeDefaultLocaleRuleOmissionTest.kt index bbade8b3c62..65c8900a1a0 100644 --- a/testing/src/test/java/org/oppia/android/testing/junit/InitializeDefaultLocaleRuleOmissionTest.kt +++ b/testing/src/test/java/org/oppia/android/testing/junit/InitializeDefaultLocaleRuleOmissionTest.kt @@ -26,13 +26,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -129,7 +132,9 @@ class InitializeDefaultLocaleRuleOmissionTest { ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, ActivityRecreatorTestModule::class, LocaleProdModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/testing/src/test/java/org/oppia/android/testing/junit/InitializeDefaultLocaleRuleTest.kt b/testing/src/test/java/org/oppia/android/testing/junit/InitializeDefaultLocaleRuleTest.kt index 437fcfc545d..6846fd3c69a 100644 --- a/testing/src/test/java/org/oppia/android/testing/junit/InitializeDefaultLocaleRuleTest.kt +++ b/testing/src/test/java/org/oppia/android/testing/junit/InitializeDefaultLocaleRuleTest.kt @@ -30,13 +30,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -133,7 +136,9 @@ class InitializeDefaultLocaleRuleTest { ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, ActivityRecreatorTestModule::class, LocaleProdModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { From 10db2ce6cd65b93393ca57d101963cfab1f0f554 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 15 Dec 2021 23:16:44 -0800 Subject: [PATCH 028/162] Add a11y string generation for math expressions. This is mostly copied from #2173. --- app/BUILD.bazel | 4 + .../android/app/utility/math/BUILD.bazel | 22 + .../math/MathExpressionAccessibilityUtil.kt | 196 +++++ .../MathExpressionAccessibilityUtilTest.kt | 724 ++++++++++++++++++ .../testing/math/MathEquationSubject.kt | 2 +- .../testing/math/MathExpressionSubject.kt | 2 +- 6 files changed, 948 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/org/oppia/android/app/utility/math/BUILD.bazel create mode 100644 app/src/main/java/org/oppia/android/app/utility/math/MathExpressionAccessibilityUtil.kt create mode 100644 app/src/test/java/org/oppia/android/app/utility/math/MathExpressionAccessibilityUtilTest.kt diff --git a/app/BUILD.bazel b/app/BUILD.bazel index 3e09ece2557..80d5ca54a6d 100644 --- a/app/BUILD.bazel +++ b/app/BUILD.bazel @@ -801,6 +801,7 @@ TEST_DEPS = [ ":test_deps", "//app/src/main/java/org/oppia/android/app/testing/activity:test_activity", "//app/src/main/java/org/oppia/android/app/translation/testing:test_module", + "//app/src/main/java/org/oppia/android/app/utility/math:math_expression_accessibility_util", "//domain", "//domain/src/main/java/org/oppia/android/domain/audio:audio_player_controller", "//domain/src/main/java/org/oppia/android/domain/onboarding/testing:fake_exploration_meta_data_retriever", @@ -813,6 +814,8 @@ TEST_DEPS = [ "//testing/src/main/java/org/oppia/android/testing/espresso:konfetti_view_matcher", "//testing/src/main/java/org/oppia/android/testing/espresso:text_input_action", "//testing/src/main/java/org/oppia/android/testing/junit:initialize_default_locale_rule", + "//testing/src/main/java/org/oppia/android/testing/math:math_equation_subject", + "//testing/src/main/java/org/oppia/android/testing/math:math_expression_subject", "//testing/src/main/java/org/oppia/android/testing/mockito", "//testing/src/main/java/org/oppia/android/testing/network", "//testing/src/main/java/org/oppia/android/testing/network:test_module", @@ -848,6 +851,7 @@ TEST_DEPS = [ "//utility/src/main/java/org/oppia/android/util/accessibility:test_module", "//utility/src/main/java/org/oppia/android/util/caching:asset_prod_module", "//utility/src/main/java/org/oppia/android/util/caching/testing:caching_test_module", + "//utility/src/main/java/org/oppia/android/util/math:parser", ] # App module tests. Note that all tests are assumed to be tests with resources (even though not all diff --git a/app/src/main/java/org/oppia/android/app/utility/math/BUILD.bazel b/app/src/main/java/org/oppia/android/app/utility/math/BUILD.bazel new file mode 100644 index 00000000000..5b5ee7cb433 --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/utility/math/BUILD.bazel @@ -0,0 +1,22 @@ +""" +General purposes utilities corresponding to displaying math expressions & constructs. +""" + +load("@dagger//:workspace_defs.bzl", "dagger_rules") +load("@io_bazel_rules_kotlin//kotlin:kotlin.bzl", "kt_android_library") + +kt_android_library( + name = "math_expression_accessibility_util", + srcs = [ + "MathExpressionAccessibilityUtil.kt", + ], + visibility = ["//app:app_visibility"], + deps = [ + ":dagger", + "//model/src/main/proto:languages_java_proto_lite", + "//model/src/main/proto:math_java_proto_lite", + "//utility/src/main/java/org/oppia/android/util/math:extensions", + ], +) + +dagger_rules() diff --git a/app/src/main/java/org/oppia/android/app/utility/math/MathExpressionAccessibilityUtil.kt b/app/src/main/java/org/oppia/android/app/utility/math/MathExpressionAccessibilityUtil.kt new file mode 100644 index 00000000000..c353b625048 --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/utility/math/MathExpressionAccessibilityUtil.kt @@ -0,0 +1,196 @@ +package org.oppia.android.app.utility.math + +import org.oppia.android.app.model.MathBinaryOperation +import org.oppia.android.app.model.MathBinaryOperation.Operator.ADD +import org.oppia.android.app.model.MathBinaryOperation.Operator.DIVIDE +import org.oppia.android.app.model.MathBinaryOperation.Operator.EXPONENTIATE +import org.oppia.android.app.model.MathBinaryOperation.Operator.MULTIPLY +import org.oppia.android.app.model.MathBinaryOperation.Operator.SUBTRACT +import org.oppia.android.app.model.MathEquation +import org.oppia.android.app.model.MathExpression +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.BINARY_OPERATION +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.CONSTANT +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.EXPRESSIONTYPE_NOT_SET +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.FUNCTION_CALL +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.GROUP +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.UNARY_OPERATION +import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.VARIABLE +import org.oppia.android.app.model.MathFunctionCall.FunctionType +import org.oppia.android.app.model.MathFunctionCall.FunctionType.FUNCTION_UNSPECIFIED +import org.oppia.android.app.model.MathFunctionCall.FunctionType.SQUARE_ROOT +import org.oppia.android.app.model.MathUnaryOperation.Operator.NEGATE +import org.oppia.android.app.model.MathUnaryOperation.Operator.POSITIVE +import org.oppia.android.app.model.OppiaLanguage +import org.oppia.android.app.model.OppiaLanguage.ARABIC +import org.oppia.android.app.model.OppiaLanguage.BRAZILIAN_PORTUGUESE +import org.oppia.android.app.model.OppiaLanguage.ENGLISH +import org.oppia.android.app.model.OppiaLanguage.HINDI +import org.oppia.android.app.model.OppiaLanguage.HINGLISH +import org.oppia.android.app.model.OppiaLanguage.LANGUAGE_UNSPECIFIED +import org.oppia.android.app.model.OppiaLanguage.PORTUGUESE +import org.oppia.android.app.model.OppiaLanguage.UNRECOGNIZED +import org.oppia.android.app.model.Real.RealTypeCase.INTEGER +import org.oppia.android.util.math.toPlainText +import java.text.NumberFormat +import java.util.Locale +import javax.inject.Inject +import org.oppia.android.app.model.MathBinaryOperation.Operator as BinaryOperator +import org.oppia.android.app.model.MathUnaryOperation.Operator as UnaryOperator + +class MathExpressionAccessibilityUtil @Inject constructor() { + fun convertToHumanReadableString( + equation: MathEquation, + language: OppiaLanguage, + divAsFraction: Boolean + ): String? { + return when (language) { + ENGLISH -> equation.toHumanReadableEnglishString(divAsFraction) + ARABIC, HINDI, HINGLISH, PORTUGUESE, BRAZILIAN_PORTUGUESE, LANGUAGE_UNSPECIFIED, + UNRECOGNIZED -> null + } + } + + fun convertToHumanReadableString( + expression: MathExpression, + language: OppiaLanguage, + divAsFraction: Boolean + ): String? { + return when (language) { + ENGLISH -> expression.toHumanReadableEnglishString(divAsFraction) + ARABIC, HINDI, HINGLISH, PORTUGUESE, BRAZILIAN_PORTUGUESE, LANGUAGE_UNSPECIFIED, + UNRECOGNIZED -> null + } + } + + private companion object { + // TODO: move these to the UI layer & have them utilize non-translatable strings. + private val numberFormat by lazy { NumberFormat.getNumberInstance(Locale.US) } + private val singularOrdinalNames = mapOf( + 1 to "oneth", + 2 to "half", + 3 to "third", + 4 to "fourth", + 5 to "fifth", + 6 to "sixth", + 7 to "seventh", + 8 to "eighth", + 9 to "ninth", + 10 to "tenth", + ) + private val pluralOrdinalNames = mapOf( + 1 to "oneths", + 2 to "halves", + 3 to "thirds", + 4 to "fourths", + 5 to "fifths", + 6 to "sixths", + 7 to "sevenths", + 8 to "eighths", + 9 to "ninths", + 10 to "tenths", + ) + + private fun MathEquation.toHumanReadableEnglishString(divAsFraction: Boolean): String? { + val lhsStr = leftSide.toHumanReadableEnglishString(divAsFraction) + val rhsStr = rightSide.toHumanReadableEnglishString(divAsFraction) + return if (lhsStr != null && rhsStr != null) "$lhsStr equals $rhsStr" else null + } + + private fun MathExpression.toHumanReadableEnglishString(divAsFraction: Boolean): String? { + // Reference: + // https://docs.google.com/document/d/1P-dldXQ08O-02ZRG978paiWOSz0dsvcKpDgiV_rKH_Y/view. + return when (expressionTypeCase) { + CONSTANT -> if (constant.realTypeCase == INTEGER) { + numberFormat.format(constant.integer.toLong()) + } else constant.toPlainText() + VARIABLE -> when (variable) { + "z" -> "zed" + "Z" -> "Zed" + else -> variable + } + BINARY_OPERATION -> { + val lhs = binaryOperation.leftOperand + val rhs = binaryOperation.rightOperand + val lhsStr = lhs.toHumanReadableEnglishString(divAsFraction) + val rhsStr = rhs.toHumanReadableEnglishString(divAsFraction) + if (lhsStr == null || rhsStr == null) return null + when (binaryOperation.operator) { + ADD -> "$lhsStr plus $rhsStr" + SUBTRACT -> "$lhsStr minus $rhsStr" + MULTIPLY -> { + if (binaryOperation.canBeReadAsImplicitMultiplication()) { + "$lhsStr $rhsStr" + } else "$lhsStr times $rhsStr" + } + DIVIDE -> { + if (divAsFraction && lhs.isConstantInteger() && rhs.isConstantInteger()) { + val numerator = lhs.constant.integer + val denominator = rhs.constant.integer + if (numerator in 0..10 && denominator in 1..10 && denominator >= numerator) { + val ordinalName = + if (numerator == 1) { + singularOrdinalNames.getValue(denominator) + } else pluralOrdinalNames.getValue(denominator) + "$numerator $ordinalName" + } else "$lhsStr over $rhsStr" + } else if (divAsFraction) { + "the fraction with numerator $lhsStr and denominator $rhsStr" + } else "$lhsStr divided by $rhsStr" + } + EXPONENTIATE -> "$lhsStr raised to the power of $rhsStr" + BinaryOperator.OPERATOR_UNSPECIFIED, BinaryOperator.UNRECOGNIZED, null -> null + } + } + UNARY_OPERATION -> { + val operandStr = unaryOperation.operand.toHumanReadableEnglishString(divAsFraction) + when (unaryOperation.operator) { + NEGATE -> operandStr?.let { "negative $it" } + POSITIVE -> operandStr?.let { "positive $it" } + UnaryOperator.OPERATOR_UNSPECIFIED, UnaryOperator.UNRECOGNIZED, null -> null + } + } + FUNCTION_CALL -> { + val argStr = functionCall.argument.toHumanReadableEnglishString(divAsFraction) + when (functionCall.functionType) { + SQUARE_ROOT -> argStr?.let { + if (functionCall.argument.isSingleTerm()) { + "square root of $it" + } else "start square root $it end square root" + } + FUNCTION_UNSPECIFIED, FunctionType.UNRECOGNIZED, null -> null + } + } + GROUP -> group.toHumanReadableEnglishString(divAsFraction)?.let { + if (isSingleTerm()) it else "open parenthesis $it close parenthesis" + } + EXPRESSIONTYPE_NOT_SET, null -> null + } + } + + private fun MathBinaryOperation.canBeReadAsImplicitMultiplication(): Boolean { + // Note that exponentiation is specialized since it's higher precedence than multiplication + // which means the graph won't look like "constant * variable" for polynomial terms like 2x^4 + // (which are cases the system should read using implicit multiplication, e.g. "two x raised + // to the power of 4"). + if (!isImplicit || !leftOperand.isConstant()) return false + return rightOperand.isVariable() || rightOperand.isExponentiation() + } + + private fun MathExpression.isConstantInteger(): Boolean = + expressionTypeCase == CONSTANT && constant.realTypeCase == INTEGER + + private fun MathExpression.isConstant(): Boolean = expressionTypeCase == CONSTANT + + private fun MathExpression.isVariable(): Boolean = expressionTypeCase == VARIABLE + + private fun MathExpression.isExponentiation(): Boolean = + expressionTypeCase == BINARY_OPERATION && binaryOperation.operator == EXPONENTIATE + + private fun MathExpression.isSingleTerm(): Boolean = when (expressionTypeCase) { + CONSTANT, VARIABLE, FUNCTION_CALL -> true + BINARY_OPERATION, UNARY_OPERATION -> false + GROUP -> group.isSingleTerm() + EXPRESSIONTYPE_NOT_SET, null -> false + } + } +} diff --git a/app/src/test/java/org/oppia/android/app/utility/math/MathExpressionAccessibilityUtilTest.kt b/app/src/test/java/org/oppia/android/app/utility/math/MathExpressionAccessibilityUtilTest.kt new file mode 100644 index 00000000000..6ada50c67ff --- /dev/null +++ b/app/src/test/java/org/oppia/android/app/utility/math/MathExpressionAccessibilityUtilTest.kt @@ -0,0 +1,724 @@ +package org.oppia.android.app.utility.math + +import android.app.Application +import androidx.appcompat.app.AppCompatActivity +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.StringSubject +import com.google.common.truth.Truth.assertThat +import com.google.common.truth.Truth.assertWithMessage +import dagger.BindsInstance +import dagger.Component +import dagger.Module +import dagger.Provides +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.oppia.android.app.activity.ActivityComponent +import org.oppia.android.app.activity.ActivityComponentFactory +import org.oppia.android.app.application.ApplicationComponent +import org.oppia.android.app.application.ApplicationInjector +import org.oppia.android.app.application.ApplicationInjectorProvider +import org.oppia.android.app.application.ApplicationModule +import org.oppia.android.app.application.ApplicationStartupListenerModule +import org.oppia.android.app.devoptions.DeveloperOptionsModule +import org.oppia.android.app.devoptions.DeveloperOptionsStarterModule +import org.oppia.android.app.model.MathEquation +import org.oppia.android.app.model.MathExpression +import org.oppia.android.app.model.OppiaLanguage +import org.oppia.android.app.model.OppiaLanguage.ARABIC +import org.oppia.android.app.model.OppiaLanguage.BRAZILIAN_PORTUGUESE +import org.oppia.android.app.model.OppiaLanguage.ENGLISH +import org.oppia.android.app.model.OppiaLanguage.HINDI +import org.oppia.android.app.model.OppiaLanguage.HINGLISH +import org.oppia.android.app.model.OppiaLanguage.LANGUAGE_UNSPECIFIED +import org.oppia.android.app.model.OppiaLanguage.PORTUGUESE +import org.oppia.android.app.model.OppiaLanguage.UNRECOGNIZED +import org.oppia.android.app.topic.PracticeTabModule +import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule +import org.oppia.android.data.backends.gae.NetworkConfigProdModule +import org.oppia.android.data.backends.gae.NetworkModule +import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule +import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule +import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule +import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule +import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule +import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule +import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule +import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule +import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule +import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule +import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule +import org.oppia.android.domain.exploration.lightweightcheckpointing.ExplorationStorageModule +import org.oppia.android.domain.hintsandsolution.HintsAndSolutionConfigModule +import org.oppia.android.domain.hintsandsolution.HintsAndSolutionProdModule +import org.oppia.android.domain.onboarding.ExpirationMetaDataRetrieverModule +import org.oppia.android.domain.oppialogger.LogStorageModule +import org.oppia.android.domain.platformparameter.PlatformParameterModule +import org.oppia.android.domain.platformparameter.PlatformParameterSingletonModule +import org.oppia.android.domain.question.QuestionModule +import org.oppia.android.domain.topic.PrimeTopicAssetsControllerModule +import org.oppia.android.domain.workmanager.WorkManagerConfigurationModule +import org.oppia.android.testing.TestLogReportingModule +import org.oppia.android.testing.math.MathEquationSubject +import org.oppia.android.testing.math.MathEquationSubject.Companion.assertThat +import org.oppia.android.testing.math.MathExpressionSubject +import org.oppia.android.testing.math.MathExpressionSubject.Companion.assertThat +import org.oppia.android.testing.robolectric.RobolectricModule +import org.oppia.android.testing.threading.TestDispatcherModule +import org.oppia.android.testing.time.FakeOppiaClockModule +import org.oppia.android.util.accessibility.AccessibilityTestModule +import org.oppia.android.util.caching.AssetModule +import org.oppia.android.util.caching.testing.CachingTestModule +import org.oppia.android.util.gcsresource.GcsResourceModule +import org.oppia.android.util.locale.LocaleProdModule +import org.oppia.android.util.logging.EnableConsoleLog +import org.oppia.android.util.logging.EnableFileLog +import org.oppia.android.util.logging.GlobalLogLevel +import org.oppia.android.util.logging.LogLevel +import org.oppia.android.util.math.MathExpressionParser +import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode +import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult +import org.oppia.android.util.networking.NetworkConnectionDebugUtilModule +import org.oppia.android.util.networking.NetworkConnectionUtilDebugModule +import org.oppia.android.util.parser.html.HtmlParserEntityTypeModule +import org.oppia.android.util.parser.image.GlideImageLoaderModule +import org.oppia.android.util.parser.image.ImageParsingModule +import org.robolectric.annotation.Config +import org.robolectric.annotation.LooperMode +import javax.inject.Inject +import javax.inject.Singleton + +/** Tests for [MathExpressionAccessibilityUtil]. */ +@RunWith(AndroidJUnit4::class) +@LooperMode(LooperMode.Mode.PAUSED) +@Config(application = MathExpressionAccessibilityUtilTest.TestApplication::class) +class MathExpressionAccessibilityUtilTest { + @Inject lateinit var util: MathExpressionAccessibilityUtil + + @Before + fun setUp() { + setUpTestApplicationComponent() + } + + @Test + fun testHumanReadableString() { + // TODO: split up & move to separate test suites. Finish test cases (if anymore are needed). + + val exp1 = parseNumericExpressionSuccessfullyWithAllErrors("1") + assertThat(exp1).forHumanReadable(ARABIC).doesNotConvertToString() + + assertThat(exp1).forHumanReadable(HINDI).doesNotConvertToString() + + assertThat(exp1).forHumanReadable(HINGLISH).doesNotConvertToString() + + assertThat(exp1).forHumanReadable(PORTUGUESE).doesNotConvertToString() + + assertThat(exp1).forHumanReadable(BRAZILIAN_PORTUGUESE).doesNotConvertToString() + + assertThat(exp1).forHumanReadable(LANGUAGE_UNSPECIFIED).doesNotConvertToString() + + assertThat(exp1).forHumanReadable(UNRECOGNIZED).doesNotConvertToString() + + val exp2 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x") + assertThat(exp2).forHumanReadable(ARABIC).doesNotConvertToString() + + assertThat(exp2).forHumanReadable(HINDI).doesNotConvertToString() + + assertThat(exp2).forHumanReadable(HINGLISH).doesNotConvertToString() + + assertThat(exp2).forHumanReadable(PORTUGUESE).doesNotConvertToString() + + assertThat(exp2).forHumanReadable(BRAZILIAN_PORTUGUESE).doesNotConvertToString() + + assertThat(exp2).forHumanReadable(LANGUAGE_UNSPECIFIED).doesNotConvertToString() + + assertThat(exp2).forHumanReadable(UNRECOGNIZED).doesNotConvertToString() + + val eq1 = parseAlgebraicEquationSuccessfullyWithAllErrors("x=1") + assertThat(eq1).forHumanReadable(ARABIC).doesNotConvertToString() + + assertThat(eq1).forHumanReadable(HINDI).doesNotConvertToString() + + assertThat(eq1).forHumanReadable(HINGLISH).doesNotConvertToString() + + assertThat(eq1).forHumanReadable(PORTUGUESE).doesNotConvertToString() + + assertThat(eq1).forHumanReadable(BRAZILIAN_PORTUGUESE).doesNotConvertToString() + + assertThat(eq1).forHumanReadable(LANGUAGE_UNSPECIFIED).doesNotConvertToString() + + assertThat(eq1).forHumanReadable(UNRECOGNIZED).doesNotConvertToString() + + // specific cases (from rules & other cases): + val exp3 = parseNumericExpressionSuccessfullyWithAllErrors("1") + assertThat(exp3).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1") + assertThat(exp3).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1") + + val exp49 = parseNumericExpressionSuccessfullyWithAllErrors("-1") + assertThat(exp49).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("negative 1") + + val exp50 = parseNumericExpressionSuccessfullyWithAllErrors("+1") + assertThat(exp50).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("positive 1") + + val exp4 = parseNumericExpressionSuccessfullyWithoutOptionalErrors("((1))") + assertThat(exp4).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1") + + val exp5 = parseNumericExpressionSuccessfullyWithAllErrors("1+2") + assertThat(exp5).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 plus 2") + + val exp6 = parseNumericExpressionSuccessfullyWithAllErrors("1-2") + assertThat(exp6).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 minus 2") + + val exp7 = parseNumericExpressionSuccessfullyWithAllErrors("1*2") + assertThat(exp7).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 times 2") + + val exp8 = parseNumericExpressionSuccessfullyWithAllErrors("1/2") + assertThat(exp8).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 divided by 2") + + val exp9 = parseNumericExpressionSuccessfullyWithAllErrors("1+(1-2)") + assertThat(exp9) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("1 plus open parenthesis 1 minus 2 close parenthesis") + + val exp10 = parseNumericExpressionSuccessfullyWithAllErrors("2^3") + assertThat(exp10) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("2 raised to the power of 3") + + val exp11 = parseNumericExpressionSuccessfullyWithAllErrors("2^(1+2)") + assertThat(exp11) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("2 raised to the power of open parenthesis 1 plus 2 close parenthesis") + + val exp12 = parseNumericExpressionSuccessfullyWithAllErrors("100000*2") + assertThat(exp12).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("100,000 times 2") + + val exp13 = parseNumericExpressionSuccessfullyWithAllErrors("sqrt(2)") + assertThat(exp13).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("square root of 2") + + val exp14 = parseNumericExpressionSuccessfullyWithAllErrors("√2") + assertThat(exp14).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("square root of 2") + + val exp15 = parseNumericExpressionSuccessfullyWithAllErrors("sqrt(1+2)") + assertThat(exp15) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("start square root 1 plus 2 end square root") + + val singularOrdinalNames = mapOf( + 1 to "oneth", + 2 to "half", + 3 to "third", + 4 to "fourth", + 5 to "fifth", + 6 to "sixth", + 7 to "seventh", + 8 to "eighth", + 9 to "ninth", + 10 to "tenth", + ) + val pluralOrdinalNames = mapOf( + 1 to "oneths", + 2 to "halves", + 3 to "thirds", + 4 to "fourths", + 5 to "fifths", + 6 to "sixths", + 7 to "sevenths", + 8 to "eighths", + 9 to "ninths", + 10 to "tenths", + ) + for (denominatorToCheck in 1..10) { + for (numeratorToCheck in 0..denominatorToCheck) { + val exp16 = + parseNumericExpressionSuccessfullyWithAllErrors("$numeratorToCheck/$denominatorToCheck") + + val ordinalName = + if (numeratorToCheck == 1) { + singularOrdinalNames.getValue(denominatorToCheck) + } else pluralOrdinalNames.getValue(denominatorToCheck) + assertThat(exp16) + .forHumanReadable(ENGLISH) + .convertsWithFractionsToStringThat() + .isEqualTo("$numeratorToCheck $ordinalName") + } + } + + val exp17 = parseNumericExpressionSuccessfullyWithAllErrors("-1/3") + assertThat(exp17) + .forHumanReadable(ENGLISH) + .convertsWithFractionsToStringThat() + .isEqualTo("negative 1 third") + + val exp18 = parseNumericExpressionSuccessfullyWithAllErrors("-2/3") + assertThat(exp18) + .forHumanReadable(ENGLISH) + .convertsWithFractionsToStringThat() + .isEqualTo("negative 2 thirds") + + val exp19 = parseNumericExpressionSuccessfullyWithAllErrors("10/11") + assertThat(exp19) + .forHumanReadable(ENGLISH) + .convertsWithFractionsToStringThat() + .isEqualTo("10 over 11") + + val exp20 = parseNumericExpressionSuccessfullyWithAllErrors("121/7986") + assertThat(exp20) + .forHumanReadable(ENGLISH) + .convertsWithFractionsToStringThat() + .isEqualTo("121 over 7,986") + + val exp21 = parseNumericExpressionSuccessfullyWithAllErrors("8/7") + assertThat(exp21) + .forHumanReadable(ENGLISH) + .convertsWithFractionsToStringThat() + .isEqualTo("8 over 7") + + val exp22 = parseNumericExpressionSuccessfullyWithAllErrors("-10/-30") + assertThat(exp22) + .forHumanReadable(ENGLISH) + .convertsWithFractionsToStringThat() + .isEqualTo("negative the fraction with numerator 10 and denominator negative 30") + + val exp23 = parseAlgebraicExpressionSuccessfullyWithAllErrors("1") + assertThat(exp23).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1") + + val exp24 = parseAlgebraicExpressionSuccessfullyWithoutOptionalErrors("((1))") + assertThat(exp24).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1") + + val exp25 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x") + assertThat(exp25).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("x") + + val exp26 = parseAlgebraicExpressionSuccessfullyWithoutOptionalErrors("((x))") + assertThat(exp26).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("x") + + val exp51 = parseAlgebraicExpressionSuccessfullyWithAllErrors("-x") + assertThat(exp51).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("negative x") + + val exp52 = parseAlgebraicExpressionSuccessfullyWithAllErrors("+x") + assertThat(exp52).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("positive x") + + val exp27 = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+x") + assertThat(exp27).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 plus x") + + val exp28 = parseAlgebraicExpressionSuccessfullyWithAllErrors("1-x") + assertThat(exp28).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 minus x") + + val exp29 = parseAlgebraicExpressionSuccessfullyWithAllErrors("1*x") + assertThat(exp29).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 times x") + + val exp30 = parseAlgebraicExpressionSuccessfullyWithAllErrors("1/x") + assertThat(exp30).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 divided by x") + + val exp31 = parseAlgebraicExpressionSuccessfullyWithAllErrors("1/x") + assertThat(exp31) + .forHumanReadable(ENGLISH) + .convertsWithFractionsToStringThat() + .isEqualTo("the fraction with numerator 1 and denominator x") + + val exp32 = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+(1-x)") + assertThat(exp32) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("1 plus open parenthesis 1 minus x close parenthesis") + + val exp33 = parseAlgebraicExpressionSuccessfullyWithAllErrors("2x") + assertThat(exp33).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("2 x") + + val exp34 = parseAlgebraicExpressionSuccessfullyWithAllErrors("xy") + assertThat(exp34).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("x times y") + + val exp35 = parseAlgebraicExpressionSuccessfullyWithAllErrors("z") + assertThat(exp35).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("zed") + + val exp36 = parseAlgebraicExpressionSuccessfullyWithAllErrors("2xz") + assertThat(exp36).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("2 x times zed") + + val exp37 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x^2") + assertThat(exp37) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("x raised to the power of 2") + + val exp38 = parseAlgebraicExpressionSuccessfullyWithoutOptionalErrors("x^(1+x)") + assertThat(exp38) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("x raised to the power of open parenthesis 1 plus x close parenthesis") + + val exp39 = parseAlgebraicExpressionSuccessfullyWithAllErrors("100000*2") + assertThat(exp39).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("100,000 times 2") + + val exp40 = parseAlgebraicExpressionSuccessfullyWithAllErrors("sqrt(2)") + assertThat(exp40).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("square root of 2") + + val exp41 = parseAlgebraicExpressionSuccessfullyWithAllErrors("sqrt(x)") + assertThat(exp41).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("square root of x") + + val exp42 = parseAlgebraicExpressionSuccessfullyWithAllErrors("√2") + assertThat(exp42).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("square root of 2") + + val exp43 = parseAlgebraicExpressionSuccessfullyWithAllErrors("√x") + assertThat(exp43).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("square root of x") + + val exp44 = parseAlgebraicExpressionSuccessfullyWithAllErrors("sqrt(1+2)") + assertThat(exp44) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("start square root 1 plus 2 end square root") + + val exp45 = parseAlgebraicExpressionSuccessfullyWithAllErrors("sqrt(1+x)") + assertThat(exp45) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("start square root 1 plus x end square root") + + val exp46 = parseAlgebraicExpressionSuccessfullyWithAllErrors("√(1+x)") + assertThat(exp46) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("start square root open parenthesis 1 plus x close parenthesis end square root") + + for (denominatorToCheck in 1..10) { + for (numeratorToCheck in 0..denominatorToCheck) { + val exp16 = + parseAlgebraicExpressionSuccessfullyWithAllErrors("$numeratorToCheck/$denominatorToCheck") + + val ordinalName = + if (numeratorToCheck == 1) { + singularOrdinalNames.getValue(denominatorToCheck) + } else pluralOrdinalNames.getValue(denominatorToCheck) + assertThat(exp16) + .forHumanReadable(ENGLISH) + .convertsWithFractionsToStringThat() + .isEqualTo("$numeratorToCheck $ordinalName") + } + } + + val exp47 = parseAlgebraicExpressionSuccessfullyWithAllErrors("1") + assertThat(exp47).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1") + + val exp48 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x(5-y)") + assertThat(exp48) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("x times open parenthesis 5 minus y close parenthesis") + + val eq2 = parseAlgebraicEquationSuccessfullyWithAllErrors("x=1/y") + assertThat(eq2) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("x equals 1 divided by y") + + val eq3 = parseAlgebraicEquationSuccessfullyWithAllErrors("x=1/2") + assertThat(eq3) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("x equals 1 divided by 2") + + val eq4 = parseAlgebraicEquationSuccessfullyWithAllErrors("x=1/y") + assertThat(eq4) + .forHumanReadable(ENGLISH) + .convertsWithFractionsToStringThat() + .isEqualTo("x equals the fraction with numerator 1 and denominator y") + + val eq5 = parseAlgebraicEquationSuccessfullyWithAllErrors("x=1/2") + assertThat(eq5) + .forHumanReadable(ENGLISH) + .convertsWithFractionsToStringThat() + .isEqualTo("x equals 1 half") + + // Tests from examples in the PRD + val eq6 = parseAlgebraicEquationSuccessfullyWithAllErrors("3x^2+4y=62") + assertThat(eq6) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("3 x raised to the power of 2 plus 4 y equals 62") + + val exp53 = parseAlgebraicExpressionSuccessfullyWithAllErrors("(x+6)/(x-4)") + assertThat(exp53) + .forHumanReadable(ENGLISH) + .convertsWithFractionsToStringThat() + .isEqualTo( + "the fraction with numerator open parenthesis x plus 6 close parenthesis and denominator" + + " open parenthesis x minus 4 close parenthesis" + ) + + val exp54 = parseAlgebraicExpressionSuccessfullyWithoutOptionalErrors("4*(x)^(2)+20x") + assertThat(exp54) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("4 times x raised to the power of 2 plus 20 x") + + val exp55 = parseAlgebraicExpressionSuccessfullyWithAllErrors("3+x-5") + assertThat(exp55).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("3 plus x minus 5") + + val exp56 = + parseAlgebraicExpressionSuccessfullyWithAllErrors( + "Z+A-Z", allowedVariables = listOf("A", "Z") + ) + assertThat(exp56).forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("Zed plus A minus Zed") + + val exp57 = + parseAlgebraicExpressionSuccessfullyWithAllErrors( + "6C-5A-1", allowedVariables = listOf("A", "C") + ) + assertThat(exp57) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("6 C minus 5 A minus 1") + + val exp58 = + parseAlgebraicExpressionSuccessfullyWithAllErrors( + "5*Z-w", allowedVariables = listOf("Z", "w") + ) + assertThat(exp58) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("5 times Zed minus w") + + val exp59 = + parseAlgebraicExpressionSuccessfullyWithAllErrors( + "L*S-3S+L", allowedVariables = listOf("L", "S") + ) + assertThat(exp59) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("L times S minus 3 S plus L") + + val exp60 = parseAlgebraicExpressionSuccessfullyWithAllErrors("2*(2+6+3+4)") + assertThat(exp60) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("2 times open parenthesis 2 plus 6 plus 3 plus 4 close parenthesis") + + val exp61 = parseAlgebraicExpressionSuccessfullyWithAllErrors("sqrt(64)") + assertThat(exp61) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("square root of 64") + + val exp62 = + parseAlgebraicExpressionSuccessfullyWithAllErrors( + "√(a+b)", allowedVariables = listOf("a", "b") + ) + assertThat(exp62) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("start square root open parenthesis a plus b close parenthesis end square root") + + val exp63 = parseAlgebraicExpressionSuccessfullyWithAllErrors("3*10^-5") + assertThat(exp63) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo("3 times 10 raised to the power of negative 5") + + val exp64 = + parseAlgebraicExpressionSuccessfullyWithoutOptionalErrors( + "((x+2y)+5*(a-2b)+z)", allowedVariables = listOf("x", "y", "a", "b", "z") + ) + assertThat(exp64) + .forHumanReadable(ENGLISH) + .convertsToStringThat() + .isEqualTo( + "open parenthesis open parenthesis x plus 2 y close parenthesis plus 5 times open" + + " parenthesis a minus 2 b close parenthesis plus zed close parenthesis" + ) + } + + private fun MathExpressionSubject.forHumanReadable( + language: OppiaLanguage + ): HumanReadableStringChecker { + return HumanReadableStringChecker(language) { divAsFraction -> + util.convertToHumanReadableString(actual, language, divAsFraction) + } + } + + private fun MathEquationSubject.forHumanReadable( + language: OppiaLanguage + ): HumanReadableStringChecker { + return HumanReadableStringChecker(language) { divAsFraction -> + util.convertToHumanReadableString(actual, language, divAsFraction) + } + } + + private fun setUpTestApplicationComponent() { + ApplicationProvider.getApplicationContext().inject(this) + } + + private class HumanReadableStringChecker( + private val language: OppiaLanguage, + private val maybeConvertToHumanReadableString: (Boolean) -> String? + ) { + fun convertsToStringThat(): StringSubject = + assertThat(convertToHumanReadableString(language, /* divAsFraction= */ false)) + + fun convertsWithFractionsToStringThat(): StringSubject = + assertThat(convertToHumanReadableString(language, /* divAsFraction= */ true)) + + fun doesNotConvertToString() { + assertWithMessage("Expected to not convert to: $language") + .that(maybeConvertToHumanReadableString(/* divAsFraction= */ false)) + .isNull() + } + + private fun convertToHumanReadableString( + language: OppiaLanguage, + divAsFraction: Boolean + ): String { + val readableString = maybeConvertToHumanReadableString(divAsFraction) + assertWithMessage("Expected to convert to: $language").that(readableString).isNotNull() + return checkNotNull(readableString) // Verified in the above assertion check. + } + } + + // TODO(#89): Move this to a common test application component. + @Module + class TestModule { + // TODO(#59): Either isolate these to their own shared test module, or use the real logging + // module in tests to avoid needing to specify these settings for tests. + @EnableConsoleLog + @Provides + fun provideEnableConsoleLog(): Boolean = true + + @EnableFileLog + @Provides + fun provideEnableFileLog(): Boolean = false + + @GlobalLogLevel + @Provides + fun provideGlobalLogLevel(): LogLevel = LogLevel.VERBOSE + } + + // TODO(#89): Move this to a common test application component. + @Singleton + @Component( + modules = [ + TestModule::class, RobolectricModule::class, FakeOppiaClockModule::class, + TestLogReportingModule::class, TestDispatcherModule::class, ApplicationModule::class, + ApplicationStartupListenerModule::class, WorkManagerConfigurationModule::class, + ImageParsingModule::class, AccessibilityTestModule::class, PracticeTabModule::class, + GcsResourceModule::class, NetworkConnectionUtilDebugModule::class, LogStorageModule::class, + NetworkModule::class, PlatformParameterModule::class, HintsAndSolutionProdModule::class, + CachingTestModule::class, InteractionsModule::class, ExplorationStorageModule::class, + QuestionModule::class, NetworkConfigProdModule::class, ContinueModule::class, + FractionInputModule::class, ItemSelectionInputModule::class, MultipleChoiceInputModule::class, + NumberWithUnitsRuleModule::class, NumericInputRuleModule::class, TextInputRuleModule::class, + DragDropSortInputModule::class, ImageClickInputModule::class, RatioInputModule::class, + HintsAndSolutionConfigModule::class, ExpirationMetaDataRetrieverModule::class, + GlideImageLoaderModule::class, PrimeTopicAssetsControllerModule::class, + HtmlParserEntityTypeModule::class, NetworkConnectionDebugUtilModule::class, + DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, AssetModule::class, + LocaleProdModule::class, ActivityRecreatorTestModule::class, + PlatformParameterSingletonModule::class, NumericExpressionInputModule::class, + AlgebraicExpressionInputModule::class, MathEquationInputModule::class + ] + ) + interface TestApplicationComponent : ApplicationComponent { + @Component.Builder + interface Builder { + @BindsInstance + fun setApplication(application: Application): Builder + + fun build(): TestApplicationComponent + } + + fun inject(test: MathExpressionAccessibilityUtilTest) + } + + class TestApplication : Application(), ActivityComponentFactory, ApplicationInjectorProvider { + private val component: TestApplicationComponent by lazy { + DaggerMathExpressionAccessibilityUtilTest_TestApplicationComponent.builder() + .setApplication(this) + .build() + } + + fun inject(test: MathExpressionAccessibilityUtilTest) { + component.inject(test) + } + + override fun createActivityComponent(activity: AppCompatActivity): ActivityComponent { + return component.getActivityComponentBuilderProvider().get().setActivity(activity).build() + } + + override fun getApplicationInjector(): ApplicationInjector = component + } + + private companion object { + private fun parseNumericExpressionSuccessfullyWithAllErrors( + expression: String + ): MathExpression { + val result = parseNumericExpressionInternal(expression, ErrorCheckingMode.ALL_ERRORS) + return (result as MathParsingResult.Success).result + } + + private fun parseNumericExpressionSuccessfullyWithoutOptionalErrors( + expression: String + ): MathExpression { + val result = + parseNumericExpressionInternal( + expression, ErrorCheckingMode.REQUIRED_ONLY + ) + return (result as MathParsingResult.Success).result + } + + private fun parseNumericExpressionInternal( + expression: String, + errorCheckingMode: ErrorCheckingMode + ): MathParsingResult { + return MathExpressionParser.parseNumericExpression(expression, errorCheckingMode) + } + + private fun parseAlgebraicExpressionSuccessfullyWithAllErrors( + expression: String, + allowedVariables: List = listOf("x", "y", "z") + ): MathExpression { + val result = + parseAlgebraicExpressionInternal(expression, ErrorCheckingMode.ALL_ERRORS, allowedVariables) + return (result as MathParsingResult.Success).result + } + + private fun parseAlgebraicExpressionSuccessfullyWithoutOptionalErrors( + expression: String, + allowedVariables: List = listOf("x", "y", "z") + ): MathExpression { + val result = + parseAlgebraicExpressionInternal( + expression, ErrorCheckingMode.REQUIRED_ONLY, allowedVariables + ) + return (result as MathParsingResult.Success).result + } + + private fun parseAlgebraicExpressionInternal( + expression: String, + errorCheckingMode: ErrorCheckingMode, + allowedVariables: List = listOf("x", "y", "z") + ): MathParsingResult { + return MathExpressionParser.parseAlgebraicExpression( + expression, allowedVariables, errorCheckingMode + ) + } + + private fun parseAlgebraicEquationSuccessfullyWithAllErrors( + expression: String, + allowedVariables: List = listOf("x", "y", "z") + ): MathEquation { + val result = + MathExpressionParser.parseAlgebraicEquation( + expression, allowedVariables, + ErrorCheckingMode.ALL_ERRORS + ) + return (result as MathParsingResult.Success).result + } + } +} diff --git a/testing/src/main/java/org/oppia/android/testing/math/MathEquationSubject.kt b/testing/src/main/java/org/oppia/android/testing/math/MathEquationSubject.kt index 373b1434b0e..3e4b44e7449 100644 --- a/testing/src/main/java/org/oppia/android/testing/math/MathEquationSubject.kt +++ b/testing/src/main/java/org/oppia/android/testing/math/MathEquationSubject.kt @@ -11,7 +11,7 @@ import org.oppia.android.util.math.toRawLatex class MathEquationSubject( metadata: FailureMetadata, - private val actual: MathEquation + val actual: MathEquation ) : LiteProtoSubject(metadata, actual) { fun hasLeftHandSideThat(): MathExpressionSubject = assertThat(actual.leftSide) diff --git a/testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt b/testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt index eea079a7e4b..b8e816fb60e 100644 --- a/testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt +++ b/testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt @@ -25,7 +25,7 @@ import org.oppia.android.util.math.toRawLatex // See: https://kotlinlang.org/docs/type-safe-builders.html. class MathExpressionSubject( metadata: FailureMetadata, - private val actual: MathExpression + val actual: MathExpression ) : LiteProtoSubject(metadata, actual) { fun hasStructureThatMatches(init: ExpressionComparator.() -> Unit) { // TODO: maybe verify that all aspects are verified? From d5dd596639571e8f47bfd6659a7ba4b61bc8b431 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 15 Dec 2021 23:52:17 -0800 Subject: [PATCH 029/162] Fix broken test post-refactor. --- .../NumericInputEqualsRuleClassifierProviderTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputEqualsRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputEqualsRuleClassifierProviderTest.kt index 1c7f3c2eb96..f7485b13545 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputEqualsRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputEqualsRuleClassifierProviderTest.kt @@ -11,8 +11,8 @@ import org.junit.Test import org.junit.runner.RunWith import org.oppia.android.app.model.WrittenTranslationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder -import org.oppia.android.domain.util.FLOAT_EQUALITY_INTERVAL import org.oppia.android.testing.assertThrows +import org.oppia.android.util.math.FLOAT_EQUALITY_INTERVAL import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode import javax.inject.Inject From aceddf8022a6c52efe73d5302fe4b5985b38b045 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 15 Dec 2021 23:57:43 -0800 Subject: [PATCH 030/162] Add reasonable import for abs(). --- .../java/org/oppia/android/util/math/FractionExtensions.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt index e229fd49e40..41ca8ff2643 100644 --- a/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt @@ -1,6 +1,7 @@ package org.oppia.android.util.math import org.oppia.android.app.model.Fraction +import kotlin.math.abs import kotlin.math.absoluteValue /** Returns whether this fraction has a fractional component. */ @@ -210,7 +211,7 @@ fun Int.toWholeNumberFraction(): Fraction { val intValue = this return Fraction.newBuilder().apply { isNegative = intValue < 0 - wholeNumber = kotlin.math.abs(intValue) + wholeNumber = abs(intValue) numerator = 0 denominator = 1 }.build() From 6e511d705d2e9f6cb2228de350b47429307697ef Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 16 Dec 2021 22:46:21 -0800 Subject: [PATCH 031/162] Integrate KotliTeX via MathTagHandler. This implementation is heavily based on #3194, including Akshay's custom fork (which was re-forked and slightly patched to work in the latest Oppia Android version). --- WORKSPACE | 8 ++ domain/src/main/assets/GJ2rLXRKD5hw_1.json | 2 +- .../src/main/assets/GJ2rLXRKD5hw_1.textproto | 2 +- third_party/BUILD.bazel | 9 +++ utility/BUILD.bazel | 1 + utility/build.gradle | 1 + .../android/util/parser/html/HtmlParser.kt | 15 ++-- .../util/parser/html/MathTagHandler.kt | 74 ++++++++++++------- 8 files changed, 79 insertions(+), 33 deletions(-) diff --git a/WORKSPACE b/WORKSPACE index cbcd442d1b2..1a1193d1219 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -129,6 +129,14 @@ git_repository( remote = "https://github.com/oppia/androidsvg", ) +# A custom fork of KotliTeX that removes resources artifacts that break the build, and updates the +# min target SDK version to be compatible with Oppia. +git_repository( + name = "kotlitex", + commit = "26d3eb4cc148e6dae198f96a23c29d6c05cbcc56", + remote = "https://github.com/oppia/kotlitex", +) + bind( name = "databinding_annotation_processor", actual = "//tools/android:compiler_annotation_processor", diff --git a/domain/src/main/assets/GJ2rLXRKD5hw_1.json b/domain/src/main/assets/GJ2rLXRKD5hw_1.json index 40e4b2e2875..5e98aa89a01 100644 --- a/domain/src/main/assets/GJ2rLXRKD5hw_1.json +++ b/domain/src/main/assets/GJ2rLXRKD5hw_1.json @@ -4,7 +4,7 @@ "page_contents": { "subtitled_html": { "content_id": "content", - "html": "

Description of subtopic is here.

.

This is sample subtopic with dummy content related to Fractions.

Fractions represent equal parts of a whole or a collection. Fraction of a whole: When we divide a whole into equal parts, each part is a fraction of the whole. A fraction has two parts. The number on the top of the line is called the numerator. It tells how many equal parts of the whole or collection are taken. The number below the line is called the denominator. It shows the total divisible number of equal parts the whole into or the total number of equal parts which are there in a collection." + "html": "

Description of subtopic is here.

.

This is sample subtopic with dummy content related to Fractions:

Fractions represent equal parts of a whole or a collection. Fraction of a whole: When we divide a whole into equal parts, each part is a fraction of the whole. A fraction has two parts. The number on the top of the line is called the numerator. It tells how many equal parts of the whole or collection are taken. The number below the line is called the denominator. It shows the total divisible number of equal parts the whole into or the total number of equal parts which are there in a collection." }, "recorded_voiceovers": { "voiceovers_mapping": { diff --git a/domain/src/main/assets/GJ2rLXRKD5hw_1.textproto b/domain/src/main/assets/GJ2rLXRKD5hw_1.textproto index 3d9625b2b4b..85a8298d90d 100644 --- a/domain/src/main/assets/GJ2rLXRKD5hw_1.textproto +++ b/domain/src/main/assets/GJ2rLXRKD5hw_1.textproto @@ -1,6 +1,6 @@ subtopic_title: "What is a Fraction?" page_contents { - html: "

Description of subtopic is here.

.

This is sample subtopic with dummy content related to Fractions.

Fractions represent equal parts of a whole or a collection. Fraction of a whole: When we divide a whole into equal parts, each part is a fraction of the whole. A fraction has two parts. The number on the top of the line is called the numerator. It tells how many equal parts of the whole or collection are taken. The number below the line is called the denominator. It shows the total divisible number of equal parts the whole into or the total number of equal parts which are there in a collection." + html: "

Description of subtopic is here.

.

This is sample subtopic with dummy content related to Fractions:

Fractions represent equal parts of a whole or a collection. Fraction of a whole: When we divide a whole into equal parts, each part is a fraction of the whole. A fraction has two parts. The number on the top of the line is called the numerator. It tells how many equal parts of the whole or collection are taken. The number below the line is called the denominator. It shows the total divisible number of equal parts the whole into or the total number of equal parts which are there in a collection." content_id: "content" } recorded_voiceover { diff --git a/third_party/BUILD.bazel b/third_party/BUILD.bazel index 860faf7687e..ade7145d461 100644 --- a/third_party/BUILD.bazel +++ b/third_party/BUILD.bazel @@ -57,6 +57,7 @@ android_library( android_library( name = "robolectric_android-all", + testonly = True, visibility = ["//visibility:public"], exports = [ "@robolectric//bazel:android-all", @@ -73,6 +74,14 @@ java_library( ], ) +android_library( + name = "io_github_karino2_kotlitex", + visibility = ["//visibility:public"], + exports = [ + "@kotlitex//kotlitex", + ], +) + # Define a separate target for the Glide annotation processor compiler. Unfortunately, this library # can't encapsulate all of Glide (i.e. by exporting the main Glide dependency) since that includes # Android assets which java_library targets do not export. diff --git a/utility/BUILD.bazel b/utility/BUILD.bazel index 513122f87ef..080e36f0200 100644 --- a/utility/BUILD.bazel +++ b/utility/BUILD.bazel @@ -65,6 +65,7 @@ kt_android_library( "//third_party:com_github_bumptech_glide_glide", "//third_party:com_google_guava_guava", "//third_party:glide_compiler", + "//third_party:io_github_karino2_kotlitex", "//third_party:org_jetbrains_kotlinx_kotlinx-coroutines-core", "//utility/src/main/java/org/oppia/android/util/caching:annotations", "//utility/src/main/java/org/oppia/android/util/caching:asset_repository", diff --git a/utility/build.gradle b/utility/build.gradle index 412f6564637..c5a16ac0d77 100644 --- a/utility/build.gradle +++ b/utility/build.gradle @@ -65,6 +65,7 @@ dependencies { 'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0-alpha03', 'androidx.work:work-runtime-ktx:2.4.0', 'com.github.oppia:androidsvg:6bd15f69caee3e6857fcfcd123023716b4adec1d', + 'com.github.oppia:kotlitex:26d3eb4cc148e6dae198f96a23c29d6c05cbcc56', 'com.github.bumptech.glide:glide:4.11.0', 'com.google.dagger:dagger:2.24', 'com.google.firebase:firebase-analytics-ktx:17.5.0', diff --git a/utility/src/main/java/org/oppia/android/util/parser/html/HtmlParser.kt b/utility/src/main/java/org/oppia/android/util/parser/html/HtmlParser.kt index 41606b659cb..30496ad07db 100755 --- a/utility/src/main/java/org/oppia/android/util/parser/html/HtmlParser.kt +++ b/utility/src/main/java/org/oppia/android/util/parser/html/HtmlParser.kt @@ -1,5 +1,6 @@ package org.oppia.android.util.parser.html +import android.content.Context import android.text.Spannable import android.text.SpannableStringBuilder import android.text.method.LinkMovementMethod @@ -12,6 +13,7 @@ import javax.inject.Inject /** Html Parser to parse custom Oppia tags with Android-compatible versions. */ class HtmlParser private constructor( + private val context: Context, private val urlImageParserFactory: UrlImageParser.Factory, private val gcsResourceName: String, private val entityType: String, @@ -32,7 +34,6 @@ class HtmlParser private constructor( } private val bulletTagHandler by lazy { BulletTagHandler() } private val imageTagHandler by lazy { ImageTagHandler(consoleLogger) } - private val mathTagHandler by lazy { MathTagHandler(consoleLogger) } /** * Parses a raw HTML string with support for custom Oppia tags. @@ -84,7 +85,7 @@ class HtmlParser private constructor( htmlContentTextView, gcsResourceName, entityType, entityId, imageCenterAlign ) val htmlSpannable = CustomHtmlContentHandler.fromHtml( - htmlContent, imageGetter, computeCustomTagHandlers(supportsConceptCards) + htmlContent, imageGetter, computeCustomTagHandlers(supportsConceptCards, htmlContentTextView) ) val spannableBuilder = CustomBulletSpan.replaceBulletSpan( @@ -99,12 +100,14 @@ class HtmlParser private constructor( } private fun computeCustomTagHandlers( - supportsConceptCards: Boolean + supportsConceptCards: Boolean, + htmlContentTextView: TextView ): Map { val handlersMap = mutableMapOf() handlersMap[CUSTOM_BULLET_LIST_TAG] = bulletTagHandler handlersMap[CUSTOM_IMG_TAG] = imageTagHandler - handlersMap[CUSTOM_MATH_TAG] = mathTagHandler + handlersMap[CUSTOM_MATH_TAG] = + MathTagHandler(consoleLogger, context.assets, htmlContentTextView.lineHeight.toFloat()) if (supportsConceptCards) { handlersMap[CUSTOM_CONCEPT_CARD_TAG] = conceptCardTagHandler } @@ -143,7 +146,8 @@ class HtmlParser private constructor( /** Factory for creating new [HtmlParser]s. */ class Factory @Inject constructor( private val urlImageParserFactory: UrlImageParser.Factory, - private val consoleLogger: ConsoleLogger + private val consoleLogger: ConsoleLogger, + private val context: Context ) { /** * Returns a new [HtmlParser] with the specified entity type and ID for loading images, and an @@ -157,6 +161,7 @@ class HtmlParser private constructor( customOppiaTagActionListener: CustomOppiaTagActionListener? = null ): HtmlParser { return HtmlParser( + context, urlImageParserFactory, gcsResourceName, entityType, diff --git a/utility/src/main/java/org/oppia/android/util/parser/html/MathTagHandler.kt b/utility/src/main/java/org/oppia/android/util/parser/html/MathTagHandler.kt index 0e2329e1bae..af2fc83b14f 100644 --- a/utility/src/main/java/org/oppia/android/util/parser/html/MathTagHandler.kt +++ b/utility/src/main/java/org/oppia/android/util/parser/html/MathTagHandler.kt @@ -1,8 +1,10 @@ package org.oppia.android.util.parser.html +import android.content.res.AssetManager import android.text.Editable import android.text.Spannable import android.text.style.ImageSpan +import io.github.karino2.kotlitex.view.MathExpressionSpan import org.json.JSONObject import org.oppia.android.util.logging.ConsoleLogger import org.xml.sax.Attributes @@ -16,7 +18,9 @@ private const val CUSTOM_MATH_SVG_PATH_ATTRIBUTE = "math_content-with-value" * [CustomHtmlContentHandler]. */ class MathTagHandler( - private val consoleLogger: ConsoleLogger + private val consoleLogger: ConsoleLogger, + private val assetManager: AssetManager, + private val lineHeight: Float ) : CustomHtmlContentHandler.CustomTagHandler { override fun handleTag( attributes: Attributes, @@ -29,39 +33,57 @@ class MathTagHandler( val content = MathContent.parseMathContent( attributes.getJsonObjectValue(CUSTOM_MATH_SVG_PATH_ATTRIBUTE) ) - if (content != null) { - // Insert an image span where the custom tag currently is to load the SVG. In the future, this - // could also load a LaTeX span, instead. Note that this approach is based on Android's Html - // parser. - val drawable = - imageRetriever.loadDrawable( - content.svgFilename, - CustomHtmlContentHandler.ImageRetriever.Type.INLINE_TEXT_IMAGE + val newSpan = when (content) { + is MathContent.MathAsSvg -> { + ImageSpan( + imageRetriever.loadDrawable( + content.svgFilename, + CustomHtmlContentHandler.ImageRetriever.Type.INLINE_TEXT_IMAGE + ), + content.svgFilename ) - val (startIndex, endIndex) = output.run { - // Use a control character to ensure that there's at least 1 character on which to "attach" - // the image when rendering the HTML. - val startIndex = length - append('\uFFFC') - return@run startIndex to length } - output.setSpan( - ImageSpan(drawable, content.svgFilename), - startIndex, - endIndex, - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE - ) - } else consoleLogger.e("MathTagHandler", "Failed to parse math tag") + is MathContent.MathAsLatex -> { + MathExpressionSpan(content.rawLatex, lineHeight, assetManager, isMathMode = true) + } + null -> { + consoleLogger.e("MathTagHandler", "Failed to parse math tag") + return + } + } + + // Insert an image span where the custom tag currently is to load the SVG/LaTeX span. Note that + // this approach is based on Android's HTML parser. + val (startIndex, endIndex) = output.run { + // Use a control character to ensure that there's at least 1 character on which to + // "attach" the image when rendering the HTML. + val startIndex = length + append('\uFFFC') + return@run startIndex to length + } + output.setSpan( + newSpan, + startIndex, + endIndex, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE + ) } - private data class MathContent(val rawLatex: String, val svgFilename: String) { + private sealed class MathContent { + data class MathAsSvg(val svgFilename: String) : MathContent() + + data class MathAsLatex(val rawLatex: String) : MathContent() + companion object { internal fun parseMathContent(obj: JSONObject?): MathContent? { + // Kotlitex expects escaped backslashes. val rawLatex = obj?.getOptionalString("raw_latex") val svgFilename = obj?.getOptionalString("svg_filename") - return if (rawLatex != null && svgFilename != null) { - MathContent(rawLatex, svgFilename) - } else null + return when { + svgFilename != null -> MathAsSvg(svgFilename) + rawLatex != null -> MathAsLatex(rawLatex) + else -> null + } } /** From 754d42df252d549ecd7007f462853dbb7387f1c5 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 16 Dec 2021 23:38:24 -0800 Subject: [PATCH 032/162] Add configurable inline LaTeX rendering support. --- .../org/oppia/android/util/parser/html/HtmlParser.kt | 12 ++++++++++-- .../oppia/android/util/parser/html/MathTagHandler.kt | 7 +++++-- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/utility/src/main/java/org/oppia/android/util/parser/html/HtmlParser.kt b/utility/src/main/java/org/oppia/android/util/parser/html/HtmlParser.kt index 30496ad07db..75601fc8a4b 100755 --- a/utility/src/main/java/org/oppia/android/util/parser/html/HtmlParser.kt +++ b/utility/src/main/java/org/oppia/android/util/parser/html/HtmlParser.kt @@ -19,6 +19,7 @@ class HtmlParser private constructor( private val entityType: String, private val entityId: String, private val imageCenterAlign: Boolean, + private val useInlineMathRendering: Boolean, private val consoleLogger: ConsoleLogger, customOppiaTagActionListener: CustomOppiaTagActionListener? ) { @@ -107,7 +108,12 @@ class HtmlParser private constructor( handlersMap[CUSTOM_BULLET_LIST_TAG] = bulletTagHandler handlersMap[CUSTOM_IMG_TAG] = imageTagHandler handlersMap[CUSTOM_MATH_TAG] = - MathTagHandler(consoleLogger, context.assets, htmlContentTextView.lineHeight.toFloat()) + MathTagHandler( + consoleLogger, + context.assets, + htmlContentTextView.lineHeight.toFloat(), + useInlineMathRendering + ) if (supportsConceptCards) { handlersMap[CUSTOM_CONCEPT_CARD_TAG] = conceptCardTagHandler } @@ -158,7 +164,8 @@ class HtmlParser private constructor( entityType: String, entityId: String, imageCenterAlign: Boolean, - customOppiaTagActionListener: CustomOppiaTagActionListener? = null + customOppiaTagActionListener: CustomOppiaTagActionListener? = null, + useInlineMathRendering: Boolean = true ): HtmlParser { return HtmlParser( context, @@ -167,6 +174,7 @@ class HtmlParser private constructor( entityType, entityId, imageCenterAlign, + useInlineMathRendering, consoleLogger, customOppiaTagActionListener ) diff --git a/utility/src/main/java/org/oppia/android/util/parser/html/MathTagHandler.kt b/utility/src/main/java/org/oppia/android/util/parser/html/MathTagHandler.kt index af2fc83b14f..481bfbed042 100644 --- a/utility/src/main/java/org/oppia/android/util/parser/html/MathTagHandler.kt +++ b/utility/src/main/java/org/oppia/android/util/parser/html/MathTagHandler.kt @@ -20,7 +20,8 @@ private const val CUSTOM_MATH_SVG_PATH_ATTRIBUTE = "math_content-with-value" class MathTagHandler( private val consoleLogger: ConsoleLogger, private val assetManager: AssetManager, - private val lineHeight: Float + private val lineHeight: Float, + private val useInlineRendering: Boolean ) : CustomHtmlContentHandler.CustomTagHandler { override fun handleTag( attributes: Attributes, @@ -44,7 +45,9 @@ class MathTagHandler( ) } is MathContent.MathAsLatex -> { - MathExpressionSpan(content.rawLatex, lineHeight, assetManager, isMathMode = true) + MathExpressionSpan( + content.rawLatex, lineHeight, assetManager, isMathMode = !useInlineRendering + ) } null -> { consoleLogger.e("MathTagHandler", "Failed to parse math tag") From 312708c58a4845d55b1ea3000bd7ab25d7bd0035 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 7 Jan 2022 18:17:44 -0800 Subject: [PATCH 033/162] Fix equals errors for equations. This splits the current error into two: one for no equals being present (& adds it), and one for too many equals. This better supports the UI errors that need to be displayed to the user in these cases. --- .../android/util/math/MathExpressionParser.kt | 95 +++++++++++++++---- .../android/util/math/MathParsingError.kt | 10 +- .../util/math/MathExpressionParserTest.kt | 12 ++- 3 files changed, 91 insertions(+), 26 deletions(-) diff --git a/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt b/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt index 80b0acd49b3..ebebe862430 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt @@ -23,7 +23,6 @@ import org.oppia.android.app.model.Real import org.oppia.android.util.math.MathExpressionParser.ParseContext.AlgebraicExpressionContext import org.oppia.android.util.math.MathExpressionParser.ParseContext.NumericExpressionContext import org.oppia.android.util.math.MathParsingError.DisabledVariablesInUseError -import org.oppia.android.util.math.MathParsingError.EquationHasWrongNumberOfEqualsError import org.oppia.android.util.math.MathParsingError.EquationMissingLhsOrRhsError import org.oppia.android.util.math.MathParsingError.ExponentIsVariableExpressionError import org.oppia.android.util.math.MathParsingError.ExponentTooLargeError @@ -63,6 +62,8 @@ import org.oppia.android.util.math.MathTokenizer.Companion.Token.RightParenthesi import org.oppia.android.util.math.MathTokenizer.Companion.Token.SquareRootSymbol import org.oppia.android.util.math.MathTokenizer.Companion.Token.VariableName import kotlin.math.absoluteValue +import org.oppia.android.util.math.MathParsingError.EquationHasTooManyEqualsError +import org.oppia.android.util.math.MathParsingError.EquationIsMissingEqualsError class MathExpressionParser private constructor(private val parseContext: ParseContext) { // TODO: @@ -96,7 +97,12 @@ class MathExpressionParser private constructor(private val parseContext: ParseCo return EquationMissingLhsOrRhsError.toFailure() } - val lhsResult = parseGenericExpression().also { + val lhsResult = parseGenericExpression().maybeFail { + // An equals sign must be present. + if (!parseContext.hasNextTokenOfType()) { + EquationIsMissingEqualsError + } else null + }.also { parseContext.consumeTokenOfType() }.maybeFail { if (!parseContext.hasMoreTokens()) { @@ -146,9 +152,9 @@ class MathExpressionParser private constructor(private val parseContext: ParseCo private fun parseGenericAddExpressionRhs(): MathParsingResult { // generic_add_expression_rhs = plus_operator , generic_mult_div_expression ; - return parseContext.consumeTokenOfType().maybeFail { + return parseContext.consumeTokenOfType().maybeFail { token -> if (!parseContext.hasMoreTokens()) { - NoVariableOrNumberAfterBinaryOperatorError(ADD) + NoVariableOrNumberAfterBinaryOperatorError(ADD, parseContext.extractSubexpression(token)) } else null }.flatMap { parseGenericMultDivExpression() @@ -157,9 +163,11 @@ class MathExpressionParser private constructor(private val parseContext: ParseCo private fun parseGenericSubExpressionRhs(): MathParsingResult { // generic_sub_expression_rhs = minus_operator , generic_mult_div_expression ; - return parseContext.consumeTokenOfType().maybeFail { + return parseContext.consumeTokenOfType().maybeFail { token -> if (!parseContext.hasMoreTokens()) { - NoVariableOrNumberAfterBinaryOperatorError(SUBTRACT) + NoVariableOrNumberAfterBinaryOperatorError( + SUBTRACT, parseContext.extractSubexpression(token) + ) } else null }.flatMap { parseGenericMultDivExpression() @@ -209,9 +217,11 @@ class MathExpressionParser private constructor(private val parseContext: ParseCo private fun parseGenericMultExpressionRhs(): MathParsingResult { // generic_mult_expression_rhs = multiplication_operator , generic_exp_expression ; - return parseContext.consumeTokenOfType().maybeFail { + return parseContext.consumeTokenOfType().maybeFail { token -> if (!parseContext.hasMoreTokens()) { - NoVariableOrNumberAfterBinaryOperatorError(MULTIPLY) + NoVariableOrNumberAfterBinaryOperatorError( + MULTIPLY, parseContext.extractSubexpression(token) + ) } else null }.flatMap { parseGenericExpExpression() @@ -220,9 +230,9 @@ class MathExpressionParser private constructor(private val parseContext: ParseCo private fun parseGenericDivExpressionRhs(): MathParsingResult { // generic_div_expression_rhs = division_operator , generic_exp_expression ; - return parseContext.consumeTokenOfType().maybeFail { + return parseContext.consumeTokenOfType().maybeFail { token -> if (!parseContext.hasMoreTokens()) { - NoVariableOrNumberAfterBinaryOperatorError(DIVIDE) + NoVariableOrNumberAfterBinaryOperatorError(DIVIDE, parseContext.extractSubexpression(token)) } else null }.flatMap { parseGenericExpExpression() @@ -270,9 +280,11 @@ class MathExpressionParser private constructor(private val parseContext: ParseCo operator = EXPONENTIATE, rhsResult = lhsResult.flatMap { parseContext.consumeTokenOfType() - }.maybeFail { + }.maybeFail { token -> if (!parseContext.hasMoreTokens()) { - NoVariableOrNumberAfterBinaryOperatorError(EXPONENTIATE) + NoVariableOrNumberAfterBinaryOperatorError( + EXPONENTIATE, parseContext.extractSubexpression(token) + ) } else null }.flatMap { parseGenericExpExpression() @@ -307,7 +319,8 @@ class MathExpressionParser private constructor(private val parseContext: ParseCo } nextToken is BinaryOperatorToken -> { NoVariableOrNumberBeforeBinaryOperatorError( - operator = nextToken.getBinaryOperator() + operator = nextToken.getBinaryOperator(), + operatorSymbol = parseContext.extractSubexpression(nextToken) ).toFailure() } else -> GenericError.toFailure() @@ -315,7 +328,7 @@ class MathExpressionParser private constructor(private val parseContext: ParseCo } is EqualsSymbol -> { if (parseContext is AlgebraicExpressionContext && parseContext.isPartOfEquation) { - EquationHasWrongNumberOfEqualsError.toFailure() + EquationHasTooManyEqualsError.toFailure() } else GenericError.toFailure() } is IncompleteFunctionName -> nextToken.toFailure() @@ -592,7 +605,7 @@ class MathExpressionParser private constructor(private val parseContext: ParseCo is LeftParenthesisSymbol, is RightParenthesisSymbol -> UnbalancedParenthesesError is EqualsSymbol -> { if (parseContext is AlgebraicExpressionContext && parseContext.isPartOfEquation) { - EquationHasWrongNumberOfEqualsError + EquationHasTooManyEqualsError } else GenericError } is IncompleteFunctionName -> nextToken.toError() @@ -699,8 +712,11 @@ class MathExpressionParser private constructor(private val parseContext: ParseCo fun parseNumericExpression( rawExpression: String, errorCheckingMode: ErrorCheckingMode = ErrorCheckingMode.ALL_ERRORS - ): MathParsingResult = - createNumericParser(rawExpression, errorCheckingMode).parseGenericExpressionGrammar() + ): MathParsingResult { + return createNumericParser(rawExpression, errorCheckingMode) + .parseGenericExpressionGrammar() + .map { it.stripParseInfo() } + } fun parseAlgebraicExpression( rawExpression: String, @@ -709,7 +725,7 @@ class MathExpressionParser private constructor(private val parseContext: ParseCo ): MathParsingResult { return createAlgebraicParser( rawExpression, isPartOfEquation = false, allowedVariables, errorCheckingMode - ).parseGenericExpressionGrammar() + ).parseGenericExpressionGrammar().map { it.stripParseInfo() } } fun parseAlgebraicEquation( @@ -719,7 +735,7 @@ class MathExpressionParser private constructor(private val parseContext: ParseCo ): MathParsingResult { return createAlgebraicParser( rawExpression, isPartOfEquation = true, allowedVariables, errorCheckingMode - ).parseGenericEquationGrammar() + ).parseGenericEquationGrammar().map { it.stripParseInfo() } } private fun createNumericParser( @@ -1040,5 +1056,46 @@ class MathExpressionParser private constructor(private val parseContext: ParseCo CONSTANT, VARIABLE, EXPRESSIONTYPE_NOT_SET, null -> false } } + + private fun MathExpression.stripParseInfo(): MathExpression { + return when (expressionTypeCase) { + BINARY_OPERATION -> { + toBuilder().apply { + binaryOperation = this@stripParseInfo.binaryOperation.toBuilder().apply { + leftOperand = this@stripParseInfo.binaryOperation.leftOperand.stripParseInfo() + rightOperand = this@stripParseInfo.binaryOperation.rightOperand.stripParseInfo() + }.build() + }.build() + } + UNARY_OPERATION -> { + toBuilder().apply { + unaryOperation = this@stripParseInfo.unaryOperation.toBuilder().apply { + operand = this@stripParseInfo.unaryOperation.operand.stripParseInfo() + }.build() + }.build() + } + FUNCTION_CALL -> { + toBuilder().apply { + functionCall = this@stripParseInfo.functionCall.toBuilder().apply { + argument = this@stripParseInfo.functionCall.argument.stripParseInfo() + }.build() + }.build() + } + GROUP -> { + toBuilder().apply { + group = this@stripParseInfo.group.stripParseInfo() + }.build() + } + CONSTANT, VARIABLE, EXPRESSIONTYPE_NOT_SET, null -> this + }.toBuilder().apply { + parseStartIndex = 0 + parseEndIndex = 0 + }.build() + } + + private fun MathEquation.stripParseInfo(): MathEquation = toBuilder().apply { + leftSide = this@stripParseInfo.leftSide.stripParseInfo() + rightSide = this@stripParseInfo.rightSide.stripParseInfo() + }.build() } } diff --git a/utility/src/main/java/org/oppia/android/util/math/MathParsingError.kt b/utility/src/main/java/org/oppia/android/util/math/MathParsingError.kt index 44fd1debb4a..ec19c7d745b 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathParsingError.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathParsingError.kt @@ -36,11 +36,13 @@ sealed class MathParsingError { object SubsequentUnaryOperatorsError : MathParsingError() data class NoVariableOrNumberBeforeBinaryOperatorError( - val operator: MathBinaryOperation.Operator + val operator: MathBinaryOperation.Operator, + val operatorSymbol: String ) : MathParsingError() data class NoVariableOrNumberAfterBinaryOperatorError( - val operator: MathBinaryOperation.Operator + val operator: MathBinaryOperation.Operator, + val operatorSymbol: String ) : MathParsingError() object ExponentIsVariableExpressionError : MathParsingError() @@ -57,7 +59,9 @@ sealed class MathParsingError { data class DisabledVariablesInUseError(val variables: List) : MathParsingError() - object EquationHasWrongNumberOfEqualsError : MathParsingError() + object EquationIsMissingEqualsError: MathParsingError() + + object EquationHasTooManyEqualsError: MathParsingError() object EquationMissingLhsOrRhsError : MathParsingError() diff --git a/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt index 3c50966b01e..88e0a46ac79 100644 --- a/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt @@ -10,7 +10,8 @@ import org.oppia.android.app.model.MathExpression import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult import org.oppia.android.util.math.MathParsingError.DisabledVariablesInUseError -import org.oppia.android.util.math.MathParsingError.EquationHasWrongNumberOfEqualsError +import org.oppia.android.util.math.MathParsingError.EquationHasTooManyEqualsError +import org.oppia.android.util.math.MathParsingError.EquationIsMissingEqualsError import org.oppia.android.util.math.MathParsingError.EquationMissingLhsOrRhsError import org.oppia.android.util.math.MathParsingError.ExponentIsVariableExpressionError import org.oppia.android.util.math.MathParsingError.ExponentTooLargeError @@ -240,13 +241,16 @@ class MathExpressionParserTest { assertThat((failure56 as DisabledVariablesInUseError).variables).containsExactly("x", "y", "z") val failure30 = expectFailureWhenParsingAlgebraicEquation("x==2") - assertThat(failure30).isInstanceOf(EquationHasWrongNumberOfEqualsError::class.java) + assertThat(failure30).isInstanceOf(EquationHasTooManyEqualsError::class.java) val failure31 = expectFailureWhenParsingAlgebraicEquation("x=2=y") - assertThat(failure31).isInstanceOf(EquationHasWrongNumberOfEqualsError::class.java) + assertThat(failure31).isInstanceOf(EquationHasTooManyEqualsError::class.java) val failure32 = expectFailureWhenParsingAlgebraicEquation("x=2=") - assertThat(failure32).isInstanceOf(EquationHasWrongNumberOfEqualsError::class.java) + assertThat(failure32).isInstanceOf(EquationHasTooManyEqualsError::class.java) + + val failure59 = expectFailureWhenParsingAlgebraicEquation("x") + assertThat(failure59).isInstanceOf(EquationIsMissingEqualsError::class.java) val failure33 = expectFailureWhenParsingAlgebraicEquation("x=") assertThat(failure33).isInstanceOf(EquationMissingLhsOrRhsError::class.java) From 9826b11704e1e2e51ee6c549661ea0b4e9d27e84 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 7 Jan 2022 18:19:45 -0800 Subject: [PATCH 034/162] Fix rational^rational powers. This generifies the sqrt algorithm to support n-roots so that rationals raised by rationals can actually work & retain the rational (in cases where the root can actually be taken). --- .../oppia/android/util/math/RealExtensions.kt | 106 ++++++++++-------- 1 file changed, 60 insertions(+), 46 deletions(-) diff --git a/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt index 83605b05808..f1d3e12d9e2 100644 --- a/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt @@ -1,5 +1,6 @@ package org.oppia.android.util.math +import kotlin.math.absoluteValue import org.oppia.android.app.model.Fraction import org.oppia.android.app.model.Real import org.oppia.android.app.model.Real.RealTypeCase.INTEGER @@ -84,15 +85,9 @@ fun Real.pow(rhs: Real): Real { RATIONAL -> { // Left-hand side is Fraction. when (rhs.realTypeCase) { - RATIONAL -> recompute { - if (rhs.rational.isOnlyWholeNumber()) { - // The fraction can be retained. - it.setRational(rational.pow(rhs.rational.wholeNumber)) - } else { - // The fraction can't realistically be retained since it's being raised to an actual - // fraction, resulting in an irrational number. - it.setIrrational(rational.toDouble().pow(rhs.rational.toDouble())) - } + // Anything raised by a fraction is pow'd by the numerator and rooted by the denominator. + RATIONAL -> rhs.rational.toImproperForm().let { power -> + rational.pow(power.numerator).root(power.denominator, power.isNegative) } IRRATIONAL -> recompute { it.setIrrational(rational.pow(rhs.irrational)) } INTEGER -> recompute { it.setRational(rational.pow(rhs.integer)) } @@ -111,14 +106,12 @@ fun Real.pow(rhs: Real): Real { INTEGER -> { // Left-hand side is an integer. when (rhs.realTypeCase) { - RATIONAL -> { - if (rhs.rational.isOnlyWholeNumber()) { - // Whole number-only fractions are effectively just int^int. - integer.pow(rhs.rational.wholeNumber) - } else { - // Otherwise, raising by a fraction will result in an irrational number. - recompute { it.setIrrational(integer.toDouble().pow(rhs.rational.toDouble())) } - } + // An integer raised to a fraction can use the same approach as above (fraction raised to + // fraction) by treating the integer as a whole number fraction. + RATIONAL -> rhs.rational.toImproperForm().let { power -> + integer.toWholeNumberFraction() + .pow(power.numerator) + .root(power.denominator, power.isNegative) } IRRATIONAL -> recompute { it.setIrrational(integer.toDouble().pow(rhs.irrational)) } INTEGER -> integer.pow(rhs.integer) @@ -196,43 +189,60 @@ private fun Int.pow(exp: Int): Real { } } -private fun sqrt(fraction: Fraction): Real { - val improper = fraction.toImproperForm() +private fun sqrt(fraction: Fraction): Real = fraction.root(base = 2, invert = false) - // Attempt to take the root of the fraction's numerator & denominator. - val numeratorRoot = sqrt(improper.numerator) - val denominatorRoot = sqrt(improper.denominator) +private fun Fraction.root(base: Int, invert: Boolean): Real { + check(base > 1) { "Expected base of 2 or higher, not: $base" } - // If both values stayed as integers, the original fraction can be retained. Otherwise, the - // fraction must be evaluated by performing a division. - return Real.newBuilder().apply { - if (numeratorRoot.realTypeCase == denominatorRoot.realTypeCase && numeratorRoot.isInteger()) { - val rootedFraction = Fraction.newBuilder().apply { - isNegative = improper.isNegative - numerator = numeratorRoot.integer - denominator = denominatorRoot.integer + val adjustedFraction = toImproperForm() + val adjustedNum = + if (adjustedFraction.isNegative) -adjustedFraction.numerator else adjustedFraction.numerator + val adjustedDenom = adjustedFraction.denominator + val rootedNumerator = if (invert) root(adjustedDenom, base) else root(adjustedNum, base) + val rootedDenominator = if (invert) root(adjustedNum, base) else root(adjustedDenom, base) + return if (rootedNumerator.isInteger() && rootedDenominator.isInteger()) { + Real.newBuilder().apply { + rational = Fraction.newBuilder().apply { + isNegative = rootedNumerator.isNegative() || rootedDenominator.isNegative() + numerator = rootedNumerator.integer.absoluteValue + denominator = rootedDenominator.integer.absoluteValue }.build().toProperForm() - if (rootedFraction.isOnlyWholeNumber()) { - // If the fractional form doesn't need to be kept, remove it. - integer = rootedFraction.toWholeNumber() - } else { - rational = rootedFraction - } - } else { - irrational = numeratorRoot.toDouble() - } - }.build() + }.build() + } else { + // One or both of the components of the fraction can't be rooted, so compute an irrational + // version. + Real.newBuilder().apply { + irrational = rootedNumerator.toDouble() / rootedDenominator.toDouble() + }.build() + } } -private fun sqrt(int: Int): Real { - // First, check if the integer is a square. Reference for possible methods: +private fun sqrt(int: Int): Real = root(int, base = 2) + +private fun root(int: Int, base: Int): Real { + // First, check if the integer is a root. Base reference for possible methods: // https://www.researchgate.net/post/How-to-decide-if-a-given-number-will-have-integer-square-root-or-not. - var potentialRoot = 2 - while ((potentialRoot * potentialRoot) < int) { + check(base > 1) { "Expected base of 2 or higher, not: $base" } + check((int < 0 && base.isOdd()) || int >= 0) { "Radicand results in imaginary number: $int" } + + if (int == 1) { + // 1^x is always 1. + return Real.newBuilder().apply { + integer = 1 + }.build() + } + + val radicand = int.absoluteValue + var potentialRoot = base + while (potentialRoot.pow(base).integer < radicand) { potentialRoot++ } - if (potentialRoot * potentialRoot == int) { + if (potentialRoot.pow(base).integer == radicand) { // There's an exact integer representation of the root. + if (int < 0 && base.isOdd()) { + // Odd roots of negative numbers retain the negative. + potentialRoot = -potentialRoot + } return Real.newBuilder().apply { integer = potentialRoot }.build() @@ -240,10 +250,14 @@ private fun sqrt(int: Int): Real { // Otherwise, compute the irrational square root. return Real.newBuilder().apply { - irrational = kotlin.math.sqrt(int.toDouble()) + irrational = if (base == 2) { + kotlin.math.sqrt(int.toDouble()) + } else int.toDouble().pow(1.0 / base.toDouble()) }.build() } +private fun Int.isOdd() = this % 2 == 1 + private fun Real.recompute(transform: (Real.Builder) -> Real.Builder): Real { return transform(newBuilderForType()).build() } From 58206b39b3c27d876cdfeb1a939d78be4b7ee680 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 7 Jan 2022 18:22:50 -0800 Subject: [PATCH 035/162] Ensure rational terms reduce to ints. This ensures cases like 8/1 become just '8' coefficients rather than staying as an irrational (for simplification). --- .../math/ExpressionToPolynomialConverter.kt | 5 ++- .../android/util/math/PolynomialExtensions.kt | 31 ++++++++++++++++++- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/utility/src/main/java/org/oppia/android/util/math/ExpressionToPolynomialConverter.kt b/utility/src/main/java/org/oppia/android/util/math/ExpressionToPolynomialConverter.kt index d16ac87fce1..15b9678626b 100644 --- a/utility/src/main/java/org/oppia/android/util/math/ExpressionToPolynomialConverter.kt +++ b/utility/src/main/java/org/oppia/android/util/math/ExpressionToPolynomialConverter.kt @@ -27,7 +27,10 @@ import org.oppia.android.app.model.MathUnaryOperation.Operator as UnaryOperator class ExpressionToPolynomialConverter private constructor() { companion object { fun MathExpression.reduceToPolynomial(): Polynomial? = - replaceSquareRoots().reduceToPolynomialAux()?.removeUnnecessaryVariables()?.sort() + replaceSquareRoots().reduceToPolynomialAux() + ?.removeUnnecessaryVariables() + ?.simplifyRationals() + ?.sort() private fun MathExpression.replaceSquareRoots(): MathExpression { return when (expressionTypeCase) { diff --git a/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt index 74b1187b6e0..0f35926b63f 100644 --- a/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt @@ -120,6 +120,18 @@ fun Polynomial.removeUnnecessaryVariables(): Polynomial { }.build().ensureAtLeastConstant() } +fun Polynomial.simplifyRationals(): Polynomial { + return Polynomial.newBuilder().apply { + addAllTerm( + this@simplifyRationals.termList.map { term -> + term.toBuilder().apply { + coefficient = term.coefficient.maybeSimplifyRationalToInteger() + }.build() + } + ) + }.build() +} + fun Polynomial.sort(): Polynomial = Polynomial.newBuilder().apply { addAllTerm(this@sort.termList.sortedWith(POLYNOMIAL_TERM_COMPARATOR)) }.build() @@ -305,7 +317,11 @@ private fun Term.pow(rational: Fraction): Term? { if (newVariablePowers.any { !it.isOnlyWholeNumber() }) return null return Term.newBuilder().apply { - coefficient = this@pow.coefficient + coefficient = this@pow.coefficient.pow( + Real.newBuilder().apply { + this.rational = rational + }.build() + ) addAllVariable( this@pow.variableList.zip(newVariablePowers).map { (variable, newPower) -> variable.toBuilder().apply { @@ -356,3 +372,16 @@ private fun List.toPowerMap(): Map { private fun Map.toVariableList(): List { return map { (name, power) -> Variable.newBuilder().setName(name).setPower(power).build() } } + +private fun Real.maybeSimplifyRationalToInteger(): Real = when (realTypeCase) { + Real.RealTypeCase.RATIONAL -> { + if (rational.isOnlyWholeNumber()) { + Real.newBuilder().apply { + integer = this@maybeSimplifyRationalToInteger.rational.toWholeNumber() + }.build() + } else this + } + // Nothing to do in these cases. + Real.RealTypeCase.IRRATIONAL, Real.RealTypeCase.INTEGER, Real.RealTypeCase.REALTYPE_NOT_SET, + null -> this +} From e355d4ae5ab153236f4a5c3769c1ee5f004b173f Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 7 Jan 2022 18:51:20 -0800 Subject: [PATCH 036/162] Add partial equivalence checking for new rules. This ensures irrational constants don't fail to compare when computed due to rounding inconsistencies in FPUs. --- ...putIsEquivalentToRuleClassifierProvider.kt | 3 +- ...atchesExactlyWithRuleClassifierProvider.kt | 3 +- ...vialManipulationsRuleClassifierProvider.kt | 3 +- .../org/oppia/android/util/math/BUILD.bazel | 12 +++++ .../math/ComparableOperationListExtensions.kt | 52 +++++++++++++++++++ .../util/math/MathExpressionExtensions.kt | 29 +++++++++++ .../android/util/math/PolynomialExtensions.kt | 18 ++++++- .../oppia/android/util/math/RealExtensions.kt | 14 +++-- 8 files changed, 127 insertions(+), 7 deletions(-) create mode 100644 utility/src/main/java/org/oppia/android/util/math/ComparableOperationListExtensions.kt diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProvider.kt index ecf89663802..50094be6215 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProvider.kt @@ -11,6 +11,7 @@ import org.oppia.android.util.math.MathExpressionParser import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult import org.oppia.android.util.math.toPolynomial import javax.inject.Inject +import org.oppia.android.util.math.approximatelyEquals class NumericExpressionInputIsEquivalentToRuleClassifierProvider @Inject constructor( private val classifierFactory: GenericRuleClassifier.Factory, @@ -31,7 +32,7 @@ class NumericExpressionInputIsEquivalentToRuleClassifierProvider @Inject constru ): Boolean { val answerExpression = parsePolynomial(answer) ?: return false val inputExpression = parsePolynomial(input) ?: return false - return answerExpression == inputExpression + return answerExpression.approximatelyEquals(inputExpression) } private fun parsePolynomial(rawExpression: String): Polynomial? { diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProvider.kt index 9ce52a59519..fe58ff2dca8 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProvider.kt @@ -10,6 +10,7 @@ import org.oppia.android.util.logging.ConsoleLogger import org.oppia.android.util.math.MathExpressionParser import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult import javax.inject.Inject +import org.oppia.android.util.math.approximatelyEquals class NumericExpressionInputMatchesExactlyWithRuleClassifierProvider @Inject constructor( private val classifierFactory: GenericRuleClassifier.Factory, @@ -30,7 +31,7 @@ class NumericExpressionInputMatchesExactlyWithRuleClassifierProvider @Inject con ): Boolean { val answerExpression = parseNumericExpression(answer) ?: return false val inputExpression = parseNumericExpression(input) ?: return false - return answerExpression == inputExpression + return answerExpression.approximatelyEquals(inputExpression) } private fun parseNumericExpression(rawExpression: String): MathExpression? { diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt index d1ea260e948..5e17c0c4173 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt @@ -11,6 +11,7 @@ import org.oppia.android.util.math.MathExpressionParser import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult import org.oppia.android.util.math.toComparableOperationList import javax.inject.Inject +import org.oppia.android.util.math.approximatelyEquals class NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider @Inject constructor( @@ -32,7 +33,7 @@ class NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvide ): Boolean { val answerExpression = parseComparableOperationList(answer) ?: return false val inputExpression = parseComparableOperationList(input) ?: return false - return answerExpression == inputExpression + return answerExpression.approximatelyEquals(inputExpression) } private fun parseComparableOperationList(rawExpression: String): ComparableOperationList? { diff --git a/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel b/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel index 3bdacb733c2..1543a82a77d 100644 --- a/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel @@ -10,6 +10,7 @@ android_library( "//:oppia_api_visibility", ], exports = [ + ":comparable_operation_list_extensions", ":comparator_extensions", ":float_extensions", ":fraction_extensions", @@ -71,6 +72,17 @@ kt_android_library( ], ) +kt_android_library( + name = "comparable_operation_list_extensions", + srcs = [ + "ComparableOperationListExtensions.kt", + ], + deps = [ + ":real_extensions", + "//model/src/main/proto:math_java_proto_lite", + ], +) + kt_android_library( name = "comparator_extensions", srcs = [ diff --git a/utility/src/main/java/org/oppia/android/util/math/ComparableOperationListExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/ComparableOperationListExtensions.kt new file mode 100644 index 00000000000..32af4ff4acb --- /dev/null +++ b/utility/src/main/java/org/oppia/android/util/math/ComparableOperationListExtensions.kt @@ -0,0 +1,52 @@ +package org.oppia.android.util.math + +import org.oppia.android.app.model.ComparableOperationList +import org.oppia.android.app.model.ComparableOperationList.CommutativeAccumulation +import org.oppia.android.app.model.ComparableOperationList.ComparableOperation +import org.oppia.android.app.model.ComparableOperationList.NonCommutativeOperation + +/** + * Returns whether this [ComparableOperationList] is approximately equal to another, that is, + * whether it exactly matches the other except for constants (which instead utilize + * [Real.approximatelyEquals]). + */ +fun ComparableOperationList.approximatelyEquals(other: ComparableOperationList): Boolean { + return rootOperation.approximatelyEquals(other.rootOperation) +} + +private fun ComparableOperation.approximatelyEquals(other: ComparableOperation): Boolean { + if (isNegated != other.isNegated) return false + if (isInverted != other.isInverted) return false + if (comparisonTypeCase != other.comparisonTypeCase) return false + return when (comparisonTypeCase) { + ComparableOperation.ComparisonTypeCase.COMMUTATIVE_ACCUMULATION -> + commutativeAccumulation.approximatelyEquals(other.commutativeAccumulation) + ComparableOperation.ComparisonTypeCase.NON_COMMUTATIVE_OPERATION -> + nonCommutativeOperation.approximatelyEquals(other.nonCommutativeOperation) + ComparableOperation.ComparisonTypeCase.CONSTANT_TERM -> + constantTerm.approximatelyEquals(other.constantTerm) + ComparableOperation.ComparisonTypeCase.VARIABLE_TERM -> variableTerm == other.variableTerm + ComparableOperation.ComparisonTypeCase.COMPARISONTYPE_NOT_SET, null -> true + } +} + +private fun CommutativeAccumulation.approximatelyEquals(other: CommutativeAccumulation): Boolean { + if (accumulationType != other.accumulationType) return false + if (combinedOperationsCount != other.combinedOperationsCount) return false + return combinedOperationsList.zip(other.combinedOperationsList).all { (first, second) -> + first.approximatelyEquals(second) + } +} + +private fun NonCommutativeOperation.approximatelyEquals(other: NonCommutativeOperation): Boolean { + if (operationTypeCase != other.operationTypeCase) return false + return when (operationTypeCase) { + NonCommutativeOperation.OperationTypeCase.EXPONENTIATION -> { + exponentiation.leftOperand.approximatelyEquals(other.exponentiation.leftOperand) + && exponentiation.rightOperand.approximatelyEquals(other.exponentiation.rightOperand) + } + NonCommutativeOperation.OperationTypeCase.SQUARE_ROOT -> + squareRoot.approximatelyEquals(other.squareRoot) + NonCommutativeOperation.OperationTypeCase.OPERATIONTYPE_NOT_SET, null -> true + } +} diff --git a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt index 39e59ce99bb..22c66b37776 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt @@ -28,6 +28,35 @@ fun MathExpression.toComparableOperationList(): ComparableOperationList = fun MathExpression.toPolynomial(): Polynomial? = stripGroups().reduceToPolynomial() +/** + * Returns whether this [MathExpression] approximately equals another, that is, that it fully + * matches in its AST representation but all constants are compared using + * [Real.approximatelyEquals]. Further, this does not check parser markers when considering + * equivalence. + */ +fun MathExpression.approximatelyEquals(other: MathExpression): Boolean { + if (expressionTypeCase != other.expressionTypeCase) return false + return when (expressionTypeCase) { + CONSTANT -> constant.approximatelyEquals(other.constant) + VARIABLE -> variable == other.variable + BINARY_OPERATION -> { + binaryOperation.operator == other.binaryOperation.operator + && binaryOperation.leftOperand.approximatelyEquals(other.binaryOperation.leftOperand) + && binaryOperation.rightOperand.approximatelyEquals(other.binaryOperation.rightOperand) + } + UNARY_OPERATION -> { + unaryOperation.operator == other.unaryOperation.operator + && unaryOperation.operand.approximatelyEquals(other.unaryOperation.operand) + } + FUNCTION_CALL -> { + functionCall.functionType == other.functionCall.functionType + && functionCall.argument.approximatelyEquals(other.functionCall.argument) + } + GROUP -> group.approximatelyEquals(other.group) + EXPRESSIONTYPE_NOT_SET, null -> true + } +} + private fun MathExpression.stripGroups(): MathExpression { return when (expressionTypeCase) { BINARY_OPERATION -> toBuilder().apply { diff --git a/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt index 0f35926b63f..33b00c26326 100644 --- a/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt @@ -38,6 +38,22 @@ fun Polynomial.isSingleTerm(): Boolean = termList.size == 1 fun Polynomial.isApproximatelyZero(): Boolean = termList.all { it.coefficient.isApproximatelyZero() } // Zero polynomials only have 0 coefs. +/** + * Returns whether this [Polynomial] approximately equals an other, that is, that the polynomial has + * the exact same terms and approximately equal coefficients (see [Real.approximatelyEquals]). + */ +fun Polynomial.approximatelyEquals(other: Polynomial): Boolean { + if (termCount != other.termCount) return false + + // Terms can be zipped since they should be sorted prior to checking equivalence. + return termList.zip(other.termList).all { (first, second) -> first.approximatelyEquals(second) } +} + +private fun Term.approximatelyEquals(other: Term): Boolean { + // The variable lists can be exactly matched since they're sorted. + return coefficient.approximatelyEquals(other.coefficient) && variableList == other.variableList +} + /** * Returns the first term coefficient from this polynomial. This corresponds to the whole value of * the polynomial iff isConstant() returns true, otherwise this value isn't useful. @@ -65,7 +81,7 @@ private fun Term.toPlainText(): String { // Include the coefficient if there is one (coefficients of 1 are ignored only if there are // variables present). productValues += when { - variableList.isEmpty() || !abs(coefficient).isApproximatelyEqualTo(1.0) -> when { + variableList.isEmpty() || !abs(coefficient).approximatelyEquals(1.0) -> when { coefficient.isRational() && variableList.isNotEmpty() -> "(${coefficient.toPlainText()})" else -> coefficient.toPlainText() } diff --git a/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt index 31bcf9702d4..6ff0552b88f 100644 --- a/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt @@ -47,11 +47,19 @@ fun Real.isNegative(): Boolean = when (realTypeCase) { REALTYPE_NOT_SET, null -> throw Exception("Invalid real: $this.") } -fun Real.isApproximatelyEqualTo(value: Double): Boolean { - return toDouble().approximatelyEquals(value) +fun Real.isApproximatelyZero(): Boolean = approximatelyEquals(0.0) + +/** + * Returns whether this [Real] approximately equals another, that is, if they evaluate to + * approximately the same value (see [Double.approximatelyEquals]). + */ +fun Real.approximatelyEquals(other: Real): Boolean { + return approximatelyEquals(other.toDouble()) } -fun Real.isApproximatelyZero(): Boolean = isApproximatelyEqualTo(0.0) +fun Real.approximatelyEquals(value: Double): Boolean { + return toDouble().approximatelyEquals(value) +} fun Real.toDouble(): Double { return when (realTypeCase) { From b073b4419d6d591b02ef7aaa5acad9ead2db8bd5 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 7 Jan 2022 18:53:00 -0800 Subject: [PATCH 037/162] Add partial constant checking for new rules. --- ...braicExpressionInputIsEquivalentToRuleClassifierProvider.kt | 3 ++- ...cExpressionInputMatchesExactlyWithRuleClassifierProvider.kt | 3 ++- ...putMatchesUpToTrivialManipulationsRuleClassifierProvider.kt | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputIsEquivalentToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputIsEquivalentToRuleClassifierProvider.kt index 276e9de2868..a6ad8e8e010 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputIsEquivalentToRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputIsEquivalentToRuleClassifierProvider.kt @@ -11,6 +11,7 @@ import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingRes import org.oppia.android.util.math.MathExpressionParser.Companion.parseAlgebraicExpression import org.oppia.android.util.math.toPolynomial import javax.inject.Inject +import org.oppia.android.util.math.approximatelyEquals class AlgebraicExpressionInputIsEquivalentToRuleClassifierProvider @Inject constructor( private val classifierFactory: GenericRuleClassifier.Factory, @@ -32,7 +33,7 @@ class AlgebraicExpressionInputIsEquivalentToRuleClassifierProvider @Inject const val allowedVariables = classificationContext.extractAllowedVariables() val answerExpression = parsePolynomial(answer, allowedVariables) ?: return false val inputExpression = parsePolynomial(input, allowedVariables) ?: return false - return answerExpression == inputExpression + return answerExpression.approximatelyEquals(inputExpression) } private fun parsePolynomial(rawExpression: String, allowedVariables: List): Polynomial? { diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider.kt index b23d434d2d6..6feeba0696a 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider.kt @@ -10,6 +10,7 @@ import org.oppia.android.util.logging.ConsoleLogger import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult import org.oppia.android.util.math.MathExpressionParser.Companion.parseAlgebraicExpression import javax.inject.Inject +import org.oppia.android.util.math.approximatelyEquals class AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider @Inject constructor( private val classifierFactory: GenericRuleClassifier.Factory, @@ -31,7 +32,7 @@ class AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider @Inject c val allowedVariables = classificationContext.extractAllowedVariables() val answerExpression = parseExpression(answer, allowedVariables) ?: return false val inputExpression = parseExpression(input, allowedVariables) ?: return false - return answerExpression == inputExpression + return answerExpression.approximatelyEquals(inputExpression) } private fun parseExpression( diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt index a7062556901..03c39a3c64f 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt @@ -11,6 +11,7 @@ import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingRes import org.oppia.android.util.math.MathExpressionParser.Companion.parseAlgebraicExpression import org.oppia.android.util.math.toComparableOperationList import javax.inject.Inject +import org.oppia.android.util.math.approximatelyEquals class AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider @Inject constructor( @@ -33,7 +34,7 @@ class AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvi val allowedVariables = classificationContext.extractAllowedVariables() val answerExpression = parseComparableOperationList(answer, allowedVariables) ?: return false val inputExpression = parseComparableOperationList(input, allowedVariables) ?: return false - return answerExpression == inputExpression + return answerExpression.approximatelyEquals(inputExpression) } private fun parseComparableOperationList( From 6a2337f65d655d5a0bf6aaabeff50858790245bb Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 7 Jan 2022 19:00:15 -0800 Subject: [PATCH 038/162] Add partial equivalence checking for new rules. --- ...thEquationInputIsEquivalentToRuleClassifierProvider.kt | 5 +++-- ...uationInputMatchesExactlyWithRuleClassifierProvider.kt | 8 +++++++- ...tchesUpToTrivialManipulationsRuleClassifierProvider.kt | 3 ++- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputIsEquivalentToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputIsEquivalentToRuleClassifierProvider.kt index 07de228a104..62552bb92b9 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputIsEquivalentToRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputIsEquivalentToRuleClassifierProvider.kt @@ -11,6 +11,7 @@ import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingRes import org.oppia.android.util.math.MathExpressionParser.Companion.parseAlgebraicEquation import org.oppia.android.util.math.toPolynomial import javax.inject.Inject +import org.oppia.android.util.math.approximatelyEquals class MathEquationInputIsEquivalentToRuleClassifierProvider @Inject constructor( private val classifierFactory: GenericRuleClassifier.Factory, @@ -34,8 +35,8 @@ class MathEquationInputIsEquivalentToRuleClassifierProvider @Inject constructor( val (inputLhs, inputRhs) = parsePolynomials(input, allowedVariables) ?: return false // Sides may cross-match (i.e. it's fine to reorder around the '='). - return (answerLhs == inputLhs && answerRhs == inputRhs) || - (answerLhs == inputRhs && answerRhs == inputLhs) + return (answerLhs.approximatelyEquals(inputLhs) && answerRhs.approximatelyEquals(inputRhs)) || + (answerLhs.approximatelyEquals(inputRhs) && answerRhs.approximatelyEquals(inputLhs)) } private fun parsePolynomials( diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesExactlyWithRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesExactlyWithRuleClassifierProvider.kt index 3bb7e3d875c..8c9e79a9e9f 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesExactlyWithRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesExactlyWithRuleClassifierProvider.kt @@ -10,6 +10,7 @@ import org.oppia.android.util.logging.ConsoleLogger import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult import org.oppia.android.util.math.MathExpressionParser.Companion.parseAlgebraicEquation import javax.inject.Inject +import org.oppia.android.util.math.approximatelyEquals class MathEquationInputMatchesExactlyWithRuleClassifierProvider @Inject constructor( private val classifierFactory: GenericRuleClassifier.Factory, @@ -31,7 +32,7 @@ class MathEquationInputMatchesExactlyWithRuleClassifierProvider @Inject construc val allowedVariables = classificationContext.extractAllowedVariables() val answerEquation = parseEquation(answer, allowedVariables) ?: return false val inputEquation = parseEquation(input, allowedVariables) ?: return false - return answerEquation == inputEquation + return answerEquation.approximatelyEquals(inputEquation) } private fun parseEquation( @@ -59,5 +60,10 @@ class MathEquationInputMatchesExactlyWithRuleClassifierProvider @Inject construc ?.map { it.normalizedString } ?: listOf() } + + private fun MathEquation.approximatelyEquals(other: MathEquation): Boolean { + return leftSide.approximatelyEquals(other.leftSide) + && rightSide.approximatelyEquals(other.rightSide) + } } } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt index 64b5e6215f6..690e5f3646b 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt @@ -11,6 +11,7 @@ import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingRes import org.oppia.android.util.math.MathExpressionParser.Companion.parseAlgebraicEquation import org.oppia.android.util.math.toComparableOperationList import javax.inject.Inject +import org.oppia.android.util.math.approximatelyEquals class MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProvider @Inject constructor( @@ -35,7 +36,7 @@ class MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProvider val (inputLhs, inputRhs) = parseComparableLists(input, allowedVariables) ?: return false // Sides must match (reordering around the '=' is not allowed by this classifier). - return answerLhs == inputLhs && answerRhs == inputRhs + return answerLhs.approximatelyEquals(inputLhs) && answerRhs.approximatelyEquals(inputRhs) } private fun parseComparableLists( From b7dabcb532c8dc7e34d0a9472b497b75a5ce2f8d Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 7 Jan 2022 19:10:09 -0800 Subject: [PATCH 039/162] Upgrade to fixed KotliTex. This version supports arbitrarily sized roots. --- WORKSPACE | 2 +- utility/build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/WORKSPACE b/WORKSPACE index 1a1193d1219..7cab69bb9ec 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -133,7 +133,7 @@ git_repository( # min target SDK version to be compatible with Oppia. git_repository( name = "kotlitex", - commit = "26d3eb4cc148e6dae198f96a23c29d6c05cbcc56", + commit = "d9466ac308ed9d52da8135f4e8bee036fe000973", remote = "https://github.com/oppia/kotlitex", ) diff --git a/utility/build.gradle b/utility/build.gradle index c5a16ac0d77..834288badb4 100644 --- a/utility/build.gradle +++ b/utility/build.gradle @@ -65,7 +65,7 @@ dependencies { 'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0-alpha03', 'androidx.work:work-runtime-ktx:2.4.0', 'com.github.oppia:androidsvg:6bd15f69caee3e6857fcfcd123023716b4adec1d', - 'com.github.oppia:kotlitex:26d3eb4cc148e6dae198f96a23c29d6c05cbcc56', + 'com.github.oppia:kotlitex:d9466ac308ed9d52da8135f4e8bee036fe000973', 'com.github.bumptech.glide:glide:4.11.0', 'com.google.dagger:dagger:2.24', 'com.google.firebase:firebase-analytics-ktx:17.5.0', From 7740c8111c6c0d6d50cf5842f32fdec3c2d4b4a0 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 13 Jan 2022 14:03:43 -0800 Subject: [PATCH 040/162] Post-merge fix. --- .../src/main/java/org/oppia/android/domain/profile/BUILD.bazel | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/domain/src/main/java/org/oppia/android/domain/profile/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/profile/BUILD.bazel index 606a6311ed7..7475ee9bcbe 100644 --- a/domain/src/main/java/org/oppia/android/domain/profile/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/profile/BUILD.bazel @@ -15,7 +15,7 @@ kt_android_library( "//data/src/main/java/org/oppia/android/data/persistence:cache_store", "//domain/src/main/java/org/oppia/android/domain/oppialogger:oppia_logger", "//domain/src/main/java/org/oppia/android/domain/oppialogger/exceptions:controller", - "//model:profile_java_proto_lite", + "//model/src/main/proto:profile_java_proto_lite", "//third_party:javax_inject_javax_inject", "//utility/src/main/java/org/oppia/android/util/data:async_result", "//utility/src/main/java/org/oppia/android/util/data:data_provider", From 929de4f094889cc92b645edbd1d39fbadef64b4b Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 13 Jan 2022 16:12:30 -0800 Subject: [PATCH 041/162] Add regex check, docs, and resolve TODOs. This also changes regex handling in the check to be more generic for better flexibility when matching files. --- model/BUILD.bazel | 3 +- model/oppia_proto_library.bzl | 22 +++-- .../file_content_validation_checks.textproto | 81 ++++++++++--------- .../oppia/android/scripts/proto/BUILD.bazel | 18 ++--- .../regex/RegexPatternValidationCheck.kt | 8 +- .../regex/RegexPatternValidationCheckTest.kt | 43 ++++++++++ 6 files changed, 116 insertions(+), 59 deletions(-) diff --git a/model/BUILD.bazel b/model/BUILD.bazel index 1755f53361e..a2bc21dd309 100644 --- a/model/BUILD.bazel +++ b/model/BUILD.bazel @@ -1,3 +1,4 @@ """ -TODO: add docs +Temporary package for defining all protos used in the codebase. Eventually, these will be moved to +more targeted data directories post-Gradle. """ diff --git a/model/oppia_proto_library.bzl b/model/oppia_proto_library.bzl index 8f6ac753135..fbd0e3db5f9 100644 --- a/model/oppia_proto_library.bzl +++ b/model/oppia_proto_library.bzl @@ -1,19 +1,25 @@ """ -TODO: add docs +Bazel macros for defining proto libraries. """ load("@rules_proto//proto:defs.bzl", "proto_library") -# TODO: add regex check -# TODO: add TODO to remove -# TODO: maybe close format proto issue with this PR? - -def oppia_proto_library(name, strip_import_prefix = "", **kwargs): +# TODO(#4096): Remove this once it's no longer needed. +def oppia_proto_library(name, **kwargs): """ - TODO: add docs + Defines a new proto library. + + Note that the library is defined with a stripped import prefix which ensures that protos have a + common import directory (which is needed since Gradle builds protos in the same directory + whereas Bazel doesn't by default). This common import directory is needed for cross-proto + textprotos to work correctly. + + Args: + name: str. The name of the proto library. + **kwargs: additional parameters to pass into proto_library. """ proto_library( name = name, - strip_import_prefix = strip_import_prefix, + strip_import_prefix = "", **kwargs ) diff --git a/scripts/assets/file_content_validation_checks.textproto b/scripts/assets/file_content_validation_checks.textproto index 17f2abb6059..5bef515e6df 100644 --- a/scripts/assets/file_content_validation_checks.textproto +++ b/scripts/assets/file_content_validation_checks.textproto @@ -1,16 +1,16 @@ file_content_checks { - file_path_regex: ".+?.kt" + file_path_regex: ".+?\\.kt" prohibited_content_regex: "^import .+?support.+?$" failure_message: "AndroidX should be used instead of the support library" } file_content_checks { - file_path_regex: ".+?.kt" + file_path_regex: ".+?\\.kt" prohibited_content_regex: "CoroutineWorker" failure_message: "For stable tests, prefer using ListenableWorker with an Oppia-managed dispatcher." exempted_file_name: "scripts/src/javatests/org/oppia/android/scripts/regex/RegexPatternValidationCheckTest.kt" } file_content_checks { - file_path_regex: ".+?.kt" + file_path_regex: ".+?\\.kt" prohibited_content_regex: "SettableFuture" failure_message: "SettableFuture should only be used in pre-approved locations since it's easy to potentially mess up & lead to a hanging ListenableFuture." exempted_file_name: "domain/src/main/java/org/oppia/android/domain/oppialogger/loguploader/LogUploadWorker.kt" @@ -18,90 +18,90 @@ file_content_checks { exempted_file_name: "scripts/src/javatests/org/oppia/android/scripts/regex/RegexPatternValidationCheckTest.kt" } file_content_checks { - file_path_regex: ".+?.xml" + file_path_regex: ".+?\\.xml" prohibited_content_regex: "android:gravity=\"left\"" failure_message: "Use android:gravity=\"start\", instead, for proper RTL support" } file_content_checks { - file_path_regex: ".+?.xml" + file_path_regex: ".+?\\.xml" prohibited_content_regex: "android:gravity=\"right\"" failure_message: "Use android:gravity=\"end\", instead, for proper RTL support" } file_content_checks { - file_path_regex: ".+?.xml" + file_path_regex: ".+?\\.xml" prohibited_content_regex: "android:layout_gravity=\"left\"" failure_message: "Use android:layout_gravity=\"start\", instead, for proper RTL support" } file_content_checks { - file_path_regex: ".+?.xml" + file_path_regex: ".+?\\.xml" prohibited_content_regex: "android:layout_gravity=\"right\"" failure_message: "Use android:layout_gravity=\"end\", instead, for proper RTL support" } file_content_checks { - file_path_regex: ".+?.xml" + file_path_regex: ".+?\\.xml" prohibited_content_regex: "paddingLeft|paddingRight|drawableLeft|drawableRight|layout_alignLeft|layout_alignRight|layout_marginLeft|layout_marginRight|layout_alignParentLeft|layout_alignParentRight|layout_toLeftOf|layout_toRightOf|layout_constraintLeft_toLeftOf|layout_constraintLeft_toRightOf|layout_constraintRight_toLeftOf|layout_constraintRight_toRightOf|layout_goneMarginLeft|layout_goneMarginRight" failure_message: "Use start/end versions of layout properties, instead, for proper RTL support" } file_content_checks { - file_path_regex: ".+?.xml" + file_path_regex: ".+?\\.xml" prohibited_content_regex: "app:barrierDirection=\"left\"" failure_message: "Use app:barrierDirection=\"start\", instead, for proper RTL support" } file_content_checks { - file_path_regex: ".+?.xml" + file_path_regex: ".+?\\.xml" prohibited_content_regex: "app:barrierDirection=\"right\"" failure_message: "Use app:barrierDirection=\"end\", instead, for proper RTL support" } file_content_checks { - file_path_regex: ".+?.xml" + file_path_regex: ".+?\\.xml" prohibited_content_regex: "motion:dragDirection=\"left\"" failure_message: "Use motion:dragDirection=\"start\", instead, for proper RTL support" } file_content_checks { - file_path_regex: ".+?.xml" + file_path_regex: ".+?\\.xml" prohibited_content_regex: "motion:dragDirection=\"right\"" failure_message: "Use motion:dragDirection=\"end\", instead, for proper RTL support" } file_content_checks { - file_path_regex: ".+?.xml" + file_path_regex: ".+?\\.xml" prohibited_content_regex: "motion:touchAnchorSide=\"left\"" failure_message: "Use motion:touchAnchorSide=\"start\", instead, for proper RTL support" } file_content_checks { - file_path_regex: ".+?.xml" + file_path_regex: ".+?\\.xml" prohibited_content_regex: "motion:touchAnchorSide=\"right\"" failure_message: "Use motion:touchAnchorSide=\"end\", instead, for proper RTL support" } file_content_checks { - file_path_regex: "app/src/main/res/values/strings.xml" + file_path_regex: "app/src/main/res/values/strings\\.xml" prohibited_content_regex: "Oppia" failure_message: "Oppia should never used directly in a string (since it shouldn't be translated). Instead, use a parameter & insert the string retrieved from app_name." } file_content_checks { - file_path_regex: "app/src/main/res/values/strings.xml" + file_path_regex: "app/src/main/res/values/strings\\.xml" prohibited_content_regex: "translatable=\"false\"" failure_message: "Untranslatable strings should go in untranslated_strings.xml, instead." } file_content_checks { - file_path_regex: ".+?.xml" + file_path_regex: ".+?\\.xml" prohibited_content_regex: "" failure_message: "All strings outside strings.xml must be marked as not translatable, or moved to strings.xml." exempted_file_patterns: "app/src/main/res/values.*?/strings\\.xml" } file_content_checks { - file_path_regex: ".+?.xml" + file_path_regex: ".+?\\.xml" prohibited_content_regex: "" failure_message: "All plurals outside strings.xml must be marked as not translatable, or moved to strings.xml." exempted_file_patterns: "app/src/main/res/values.*?/strings\\.xml" } file_content_checks { - file_path_regex: ".+?.kt" + file_path_regex: ".+?\\.kt" prohibited_content_regex: "android.text.BidiFormatter" failure_message: "Do not use Android's BidiFormatter directly. Instead, use AndroidX's BidiFormatter for KitKat compatibility." exempted_file_name: "scripts/src/javatests/org/oppia/android/scripts/regex/RegexPatternValidationCheckTest.kt" } file_content_checks { - file_path_regex: ".+?.kt" + file_path_regex: ".+?\\.kt" prohibited_content_regex: "androidx.core.text.BidiFormatter" failure_message: "Do not use AndroidX's BidiFormatter directly. Instead, use the wrapper utility OppiaBidiFormatter so that tests can verify that formatting actually occurs on select strings." exempted_file_name: "scripts/src/javatests/org/oppia/android/scripts/regex/RegexPatternValidationCheckTest.kt" @@ -110,7 +110,7 @@ file_content_checks { exempted_file_name: "utility/src/main/java/org/oppia/android/util/locale/OppiaBidiFormatterImpl.kt" } file_content_checks { - file_path_regex: ".+?.kt" + file_path_regex: ".+?\\.kt" prohibited_content_regex: "(format|getString|getStringArray|getQuantityString|getQuantityText|toLowerCase|toUpperCase|capitalize|decapitalize|lowercase|uppercase)\\(" failure_message: "String formatting and resource retrieval should go through AppLanguageResourceHandler, OppiaLocale.DisplayLocale, or OppiaLocale.MachineLocale depending on the context (see each class's documentation for details on when each should be used)." exempted_file_name: "domain/src/main/java/org/oppia/android/domain/locale/DisplayLocaleImpl.kt" @@ -128,29 +128,29 @@ file_content_checks { exempted_file_patterns: "scripts/.+" } file_content_checks { - file_path_regex: ".+?.java" + file_path_regex: ".+?\\.java" prohibited_content_regex: "(format|getString|getStringArray|getQuantityString|getQuantityText|toLowerCase|toUpperCase)\\(" failure_message: "String formatting and resource retrieval should go through AppLanguageResourceHandler, OppiaLocale.DisplayLocale, or OppiaLocale.MachineLocale depending on the context (see each class's documentation for details on when each should be used)." } file_content_checks { - file_path_regex: ".+?.kt" + file_path_regex: ".+?\\.kt" prohibited_content_regex: "ignoreCase\\s*?=" failure_message: "Case-insensitive string operations should be performed using MachineLocale." - exempted_file_patterns: "testing/src/main/.+?.kt" + exempted_file_patterns: "testing/src/main/.+?\\.kt" exempted_file_patterns: "scripts/.+" } file_content_checks { - file_path_regex: ".+?.xml" + file_path_regex: ".+?\\.xml" prohibited_content_regex: "(format|getString|getStringArray)\\(" failure_message: "String formatting and resource retrieval in layouts should go through AppLanguageResourceHandler." } file_content_checks { - file_path_regex: ".+?.xml" + file_path_regex: ".+?\\.xml" prohibited_content_regex: "@string/[^\\s]+?\\(" failure_message: "String formatting and quantity string building shouldn't be done directly through databinding. Instead, pass in AppLanguageResourceHandler from the view model or call a new function through the view model to compute the string. Both should use the handler's locale-safe formatting/quantity string methods." } file_content_checks { - file_path_regex: ".+?.xml" + file_path_regex: ".+?\\.xml" prohibited_content_regex: "@plurals/[^\\s]+?\\(" failure_message: "String plurals shouldn't be constructed directly through databinding. Instead, pass in AppLanguageResourceHandler from the view model or call a new function through the view model to compute the string. Both should use the handler's locale-safe formatting/quantity string methods." } @@ -160,13 +160,13 @@ file_content_checks { failure_message: "Only string type specifiers should use for strings (to avoid runtime errors due to bidirectional wrapping requirements)." } file_content_checks { - file_path_regex: ".+?.kt" + file_path_regex: ".+?\\.kt" prohibited_content_regex: "\\sActivity\\(" failure_message: "Activity should never be subclassed. Use AppCompatActivity, instead." exempted_file_name: "scripts/src/javatests/org/oppia/android/scripts/regex/RegexPatternValidationCheckTest.kt" } file_content_checks { - file_path_regex: ".+?.kt" + file_path_regex: ".+?\\.kt" prohibited_content_regex: "\\sAppCompatActivity\\(" failure_message: "Never subclass AppCompatActivity directly. Instead, use InjectableAppCompatActivity." exempted_file_name: "app/src/main/java/org/oppia/android/app/activity/InjectableAppCompatActivity.kt" @@ -176,30 +176,30 @@ file_content_checks { exempted_file_name: "testing/src/main/java/org/oppia/android/testing/TextInputActionTestActivity.kt" } file_content_checks { - file_path_regex: ".+?.kt" + file_path_regex: ".+?\\.kt" prohibited_content_regex: "\\sDialogFragment\\(" failure_message: "DialogFragment should never be subclassed. Use InjectableDialogFragment, instead." exempted_file_name: "app/src/main/java/org/oppia/android/app/fragment/InjectableDialogFragment.kt" exempted_file_name: "scripts/src/javatests/org/oppia/android/scripts/regex/RegexPatternValidationCheckTest.kt" } file_content_checks { - file_path_regex: ".+?AndroidManifest.xml" + file_path_regex: ".+?AndroidManifest\\.xml" prohibited_content_regex: "android:configChanges" failure_message: "Never explicitly handle configuration changes. Instead, use saved instance states for retaining state across rotations. For other types of configuration changes, follow up with the developer mailing list with how to proceed if you think this is a legitimate case." } file_content_checks { - file_path_regex: ".+?.xml" + file_path_regex: ".+?\\.xml" prohibited_content_regex: "(android:drawableStart)|(android:drawableEnd)|(android:drawableTop)|(android:drawableBottom)|(android:src)" failure_message: "Drawable start/end/top/bottom & image source should use the compat versions, instead, e.g.: app:drawableStartCompat or app:srcCompat, to ensure that vector drawables can load properly in SDK <21 environments." } file_content_checks { - file_path_regex: ".+?.kt" + file_path_regex: ".+?\\.kt" prohibited_content_regex: "java.util.Optional" failure_message: "Prefer using com.google.common.base.Optional (Guava's Optional) since desugaring has some incompatibilities between Bazel & KitKat builds." exempted_file_name: "scripts/src/javatests/org/oppia/android/scripts/regex/RegexPatternValidationCheckTest.kt" } file_content_checks { - file_path_regex: ".+?.kt" + file_path_regex: ".+?\\.kt" prohibited_content_regex: "java\\.util\\.Calendar" failure_message: "Don't use Calendar directly. Instead, use OppiaClock and/or OppiaLocale for calendar-specific operations." exempted_file_name: "scripts/src/javatests/org/oppia/android/scripts/regex/RegexPatternValidationCheckTest.kt" @@ -209,7 +209,7 @@ file_content_checks { exempted_file_name: "utility/src/main/java/org/oppia/android/util/system/OppiaClock.kt" } file_content_checks { - file_path_regex: ".+?.kt" + file_path_regex: ".+?\\.kt" prohibited_content_regex: "java\\.util\\.Date" failure_message: "Don't use Date directly. Instead, perform date-based operations using OppiaLocale." exempted_file_name: "app/src/sharedTest/java/org/oppia/android/app/splash/SplashActivityTest.kt" @@ -221,7 +221,7 @@ file_content_checks { exempted_file_name: "utility/src/main/java/org/oppia/android/util/locale/MachineLocaleImpl.kt" } file_content_checks { - file_path_regex: ".+?.kt" + file_path_regex: ".+?\\.kt" prohibited_content_regex: "java\\.text" failure_message: "Don't perform date/time formatting directly. Instead, use OppiaLocale." exempted_file_name: "app/src/sharedTest/java/org/oppia/android/app/splash/SplashActivityTest.kt" @@ -232,7 +232,7 @@ file_content_checks { exempted_file_name: "utility/src/main/java/org/oppia/android/util/locale/MachineLocaleImpl.kt" } file_content_checks { - file_path_regex: ".+?.kt" + file_path_regex: ".+?\\.kt" prohibited_content_regex: "java\\.util\\.Locale" failure_message: "Don't use Locale directly. Instead, use LocaleController, or OppiaLocale & its subclasses." exempted_file_name: "app/src/main/java/org/oppia/android/app/player/audio/LanguageDialogFragment.kt" @@ -252,8 +252,13 @@ file_content_checks { exempted_file_patterns: "utility/src/(?:((main)|(test)))/java/org/oppia/android/util/locale/.+?\\.kt" } file_content_checks { - file_path_regex: ".+?.kt" + file_path_regex: ".+?\\.kt" prohibited_content_regex: "kotlin\\.properties\\.Delegates" failure_message: "Don't use Delegates; use a lateinit var or nullable primitive var default-initialized to null, instead. Delegates uses reflection internally, have a non-trivial initialization cost, and can cause breakages on KitKat devices. See #3939 for more context." exempted_file_name: "scripts/src/javatests/org/oppia/android/scripts/regex/RegexPatternValidationCheckTest.kt" } +file_content_checks { + file_path_regex: "BUILD" + prohibited_content_regex: "^proto_library\\(" + failure_message: "Don't use proto_library. Use oppia_proto_library instead." +} diff --git a/scripts/src/java/org/oppia/android/scripts/proto/BUILD.bazel b/scripts/src/java/org/oppia/android/scripts/proto/BUILD.bazel index b8ea5902201..60f6588f1a0 100644 --- a/scripts/src/java/org/oppia/android/scripts/proto/BUILD.bazel +++ b/scripts/src/java/org/oppia/android/scripts/proto/BUILD.bazel @@ -1,15 +1,15 @@ """ This library contains all protos used in the scripts module. -In Bazel, proto files are built using the proto_library() and java_lite_proto_library() rules. -The proto_library() rule creates a proto file library to be used in multiple languages. +In Bazel, proto files are built using the oppia_proto_library() and java_lite_proto_library() rules. +The oppia_proto_library() rule creates a proto file library to be used in multiple languages. The java_lite_proto_library() rule takes in a proto_library target and generates java code. -For more context on adding a new proto library, please refer to model/BUILD.bazel +For more context on adding a new proto library, please refer to model/BUILD """ load("@rules_java//java:defs.bzl", "java_lite_proto_library", "java_proto_library") -load("@rules_proto//proto:defs.bzl", "proto_library") +load("//model:oppia_proto_library.bzl", "oppia_proto_library") -proto_library( +oppia_proto_library( name = "affected_tests_proto", srcs = ["affected_tests.proto"], ) @@ -20,7 +20,7 @@ java_lite_proto_library( deps = [":affected_tests_proto"], ) -proto_library( +oppia_proto_library( name = "filename_pattern_validation_checks_proto", srcs = ["filename_pattern_validation_checks.proto"], visibility = ["//scripts:oppia_script_binary_visibility"], @@ -32,7 +32,7 @@ java_lite_proto_library( deps = [":filename_pattern_validation_checks_proto"], ) -proto_library( +oppia_proto_library( name = "file_content_validation_checks_proto", srcs = ["file_content_validation_checks.proto"], visibility = ["//scripts:oppia_script_binary_visibility"], @@ -44,7 +44,7 @@ java_lite_proto_library( deps = [":file_content_validation_checks_proto"], ) -proto_library( +oppia_proto_library( name = "script_exemptions_proto", srcs = ["script_exemptions.proto"], visibility = ["//scripts:oppia_script_binary_visibility"], @@ -56,7 +56,7 @@ java_lite_proto_library( deps = [":script_exemptions_proto"], ) -proto_library( +oppia_proto_library( name = "maven_dependencies_proto", srcs = ["maven_dependencies.proto"], visibility = ["//scripts:oppia_script_binary_visibility"], diff --git a/scripts/src/java/org/oppia/android/scripts/regex/RegexPatternValidationCheck.kt b/scripts/src/java/org/oppia/android/scripts/regex/RegexPatternValidationCheck.kt index 464251b2689..a910ac65a4a 100644 --- a/scripts/src/java/org/oppia/android/scripts/regex/RegexPatternValidationCheck.kt +++ b/scripts/src/java/org/oppia/android/scripts/regex/RegexPatternValidationCheck.kt @@ -218,7 +218,7 @@ private data class MatchableFileContentCheck( * (i.e. that it matches the inclusion pattern and is not explicitly or implicitly excluded). */ fun isFileAffectedByCheck(relativePath: String): Boolean = - filePathRegex.matches(relativePath) && !isFileExempted(relativePath) + filePathRegex.containsMatchIn(relativePath) && !isFileExempted(relativePath) /** * Returns the list of line indexes which contain prohibited content per this check (given an @@ -231,8 +231,10 @@ private data class MatchableFileContentCheck( }.map { (index, _) -> index } } - private fun isFileExempted(relativePath: String): Boolean = - relativePath in exemptedFileNames || exemptedFilePatterns.any { it.matches(relativePath) } + private fun isFileExempted(relativePath: String): Boolean { + return relativePath in exemptedFileNames + || exemptedFilePatterns.any { it.containsMatchIn(relativePath) } + } companion object { /** Returns a new [MatchableFileContentCheck] based on the specified [FileContentCheck]. */ diff --git a/scripts/src/javatests/org/oppia/android/scripts/regex/RegexPatternValidationCheckTest.kt b/scripts/src/javatests/org/oppia/android/scripts/regex/RegexPatternValidationCheckTest.kt index 3f576389c91..25688daeba1 100644 --- a/scripts/src/javatests/org/oppia/android/scripts/regex/RegexPatternValidationCheckTest.kt +++ b/scripts/src/javatests/org/oppia/android/scripts/regex/RegexPatternValidationCheckTest.kt @@ -119,6 +119,7 @@ class RegexPatternValidationCheckTest { "Don't use Delegates; use a lateinit var or nullable primitive var default-initialized to" + " null, instead. Delegates uses reflection internally, have a non-trivial initialization" + " cost, and can cause breakages on KitKat devices. See #3939 for more context." + private val doNotUseProtoLibrary = "Don't use proto_library. Use oppia_proto_library instead." private val wikiReferenceNote = "Refer to https://github.com/oppia/oppia-android/wiki/Static-Analysis-Checks" + "#regexpatternvalidation-check for more details on how to fix this." @@ -1544,6 +1545,48 @@ class RegexPatternValidationCheckTest { ) } + @Test + fun testFileContent_buildFileUsesProtoLibrary_fileContentIsNotCorrect() { + val prohibitedContent = "proto_library(" + tempFolder.newFolder("testfiles", "domain", "src", "main") + val stringFilePath = "domain/src/main/BUILD" + tempFolder.newFile("testfiles/$stringFilePath").writeText(prohibitedContent) + + val exception = assertThrows(Exception::class) { + runScript() + } + + assertThat(exception).hasMessageThat().contains(REGEX_CHECK_FAILED_OUTPUT_INDICATOR) + assertThat(outContent.toString().trim()) + .isEqualTo( + """ + $stringFilePath:1: $doNotUseProtoLibrary + $wikiReferenceNote + """.trimIndent() + ) + } + + @Test + fun testFileContent_buildBazelFileUsesProtoLibrary_fileContentIsNotCorrect() { + val prohibitedContent = "proto_library(" + tempFolder.newFolder("testfiles", "domain", "src", "main") + val stringFilePath = "domain/src/main/BUILD.bazel" + tempFolder.newFile("testfiles/$stringFilePath").writeText(prohibitedContent) + + val exception = assertThrows(Exception::class) { + runScript() + } + + assertThat(exception).hasMessageThat().contains(REGEX_CHECK_FAILED_OUTPUT_INDICATOR) + assertThat(outContent.toString().trim()) + .isEqualTo( + """ + $stringFilePath:1: $doNotUseProtoLibrary + $wikiReferenceNote + """.trimIndent() + ) + } + @Test fun testFilenameAndContent_useProhibitedFileName_useProhibitedFileContent_multipleFailures() { tempFolder.newFolder("testfiles", "data", "src", "main") From e50a50f14e13a8f36885021aa3caccf14d4d53ca Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 13 Jan 2022 16:14:26 -0800 Subject: [PATCH 042/162] Lint fix. --- .../android/scripts/regex/RegexPatternValidationCheck.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/src/java/org/oppia/android/scripts/regex/RegexPatternValidationCheck.kt b/scripts/src/java/org/oppia/android/scripts/regex/RegexPatternValidationCheck.kt index a910ac65a4a..422c915adef 100644 --- a/scripts/src/java/org/oppia/android/scripts/regex/RegexPatternValidationCheck.kt +++ b/scripts/src/java/org/oppia/android/scripts/regex/RegexPatternValidationCheck.kt @@ -232,8 +232,8 @@ private data class MatchableFileContentCheck( } private fun isFileExempted(relativePath: String): Boolean { - return relativePath in exemptedFileNames - || exemptedFilePatterns.any { it.containsMatchIn(relativePath) } + return relativePath in exemptedFileNames || + exemptedFilePatterns.any { it.containsMatchIn(relativePath) } } companion object { From ec575b74ccca5d47097f4eb6810547878f68cb56 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 13 Jan 2022 17:03:22 -0800 Subject: [PATCH 043/162] Fix failing static checks. --- scripts/assets/test_file_exemptions.textproto | 4 ++-- utility/src/main/java/org/oppia/android/util/math/BUILD.bazel | 2 +- utility/src/test/java/org/oppia/android/util/math/BUILD.bazel | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/assets/test_file_exemptions.textproto b/scripts/assets/test_file_exemptions.textproto index 8233c66c0d6..85d68bd005f 100644 --- a/scripts/assets/test_file_exemptions.textproto +++ b/scripts/assets/test_file_exemptions.textproto @@ -611,8 +611,6 @@ exempted_file_path: "domain/src/main/java/org/oppia/android/domain/topic/PrimeTo exempted_file_path: "domain/src/main/java/org/oppia/android/domain/topic/PrimeTopicAssetsControllerImpl.kt" exempted_file_path: "domain/src/main/java/org/oppia/android/domain/topic/PrimeTopicAssetsControllerModule.kt" exempted_file_path: "domain/src/main/java/org/oppia/android/domain/topic/RevisionCardRetriever.kt" -exempted_file_path: "domain/src/main/java/org/oppia/android/domain/util/FloatExtensions.kt" -exempted_file_path: "domain/src/main/java/org/oppia/android/domain/util/FractionExtensions.kt" exempted_file_path: "domain/src/main/java/org/oppia/android/domain/util/JsonAssetRetriever.kt" exempted_file_path: "domain/src/main/java/org/oppia/android/domain/util/JsonExtensions.kt" exempted_file_path: "domain/src/main/java/org/oppia/android/domain/util/WorkDataExtensions.kt" @@ -710,6 +708,8 @@ exempted_file_path: "utility/src/main/java/org/oppia/android/util/logging/fireba exempted_file_path: "utility/src/main/java/org/oppia/android/util/logging/firebase/FirebaseLogUploader.kt" exempted_file_path: "utility/src/main/java/org/oppia/android/util/logging/firebase/FirebaseLogUploaderModule.kt" exempted_file_path: "utility/src/main/java/org/oppia/android/util/logging/firebase/LogReportingModule.kt" +exempted_file_path: "utility/src/main/java/org/oppia/android/util/math/FloatExtensions.kt" +exempted_file_path: "utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt" exempted_file_path: "utility/src/main/java/org/oppia/android/util/networking/ConnectionStatus.kt" exempted_file_path: "utility/src/main/java/org/oppia/android/util/networking/NetworkConnectionDebugUtil.kt" exempted_file_path: "utility/src/main/java/org/oppia/android/util/networking/NetworkConnectionDebugUtilModule.kt" diff --git a/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel b/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel index 4b84961d297..4ecd3e58e47 100644 --- a/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel @@ -1,5 +1,5 @@ """ -TODO: document +General-purpose mathematics utilities, especially for supporting math-based interactions. """ load("@io_bazel_rules_kotlin//kotlin:kotlin.bzl", "kt_android_library") diff --git a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel index 493d89d66a0..313a5a1f751 100644 --- a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel @@ -1,5 +1,5 @@ """ -TODO: document +Tests for general-purpose mathematics utilities. """ load("//:oppia_android_test.bzl", "oppia_android_test") From 1268fc55bfabd268c9743f5efa36bd5752c41844 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 13 Jan 2022 19:27:47 -0800 Subject: [PATCH 044/162] Fix broken CI checks. Adds missing KDocs, test file exemptions, and fixes the Gradle build. --- model/src/main/proto/math.proto | 122 +++++- scripts/assets/test_file_exemptions.textproto | 4 + testing/build.gradle | 2 + .../oppia/android/testing/math/BUILD.bazel | 2 +- .../android/testing/math/FractionSubject.kt | 35 +- .../testing/math/MathEquationSubject.kt | 25 +- .../testing/math/MathExpressionSubject.kt | 369 ++++++++++++++++-- .../oppia/android/testing/math/RealSubject.kt | 26 +- 8 files changed, 532 insertions(+), 53 deletions(-) diff --git a/model/src/main/proto/math.proto b/model/src/main/proto/math.proto index 7dcbf780370..a8af5ba5bec 100644 --- a/model/src/main/proto/math.proto +++ b/model/src/main/proto/math.proto @@ -5,94 +5,186 @@ package model; option java_package = "org.oppia.android.app.model"; option java_multiple_files = true; -// Structure for a fraction object. +// Represents a fraction. +// +// Values of this proto can be analyzed using FractionSubject. message Fraction { + // Defines whether the fraction is negative. bool is_negative = 1; - int32 whole_number = 2; - int32 numerator = 3; - int32 denominator = 4; + + // Defines the whole number component of the fraction. + uint32 whole_number = 2; + + // Defines the numerator of the fraction. + uint32 numerator = 3; + + // Defines the denominator of the fraction. This should never be zero. + uint32 denominator = 4; } +// Represents a structured real value. +// +// Values of this proto can be analyzed using RealSubject. message Real { + // Defines type of real value. oneof real_type { + // Indicates that this real value is a fraction. Fraction rational = 1; - // Represents a decimal value. Technically these can sometimes be rational, but given IEEE-754 - // rounding errors we need to treat these values as irrational and non-factorable. + + // Indicates that this real value is a decimal. Technically these can sometimes be rational, but + // given IEEE-754 rounding errors and the difficulty of factoring fractions, many rational + // decimal values need to be treated as irrational and non-factorable. double irrational = 2; + + // Indicates that thi sreal value is an integer (as a special case of rational values since + // integers are easier to work with than fraction objects). Note that this isn't the only case + // where the real value can be an integer. It can also be an integer double value, or a fraction + // with only a whole number component. int32 integer = 3; } } -// Structure containing a ratio object for eg - [1,2,3] for 1:2:3. +// Represents a ratio, e.g. 1:2:3. message RatioExpression { - // List of components in a ratio. It's expected that list should have more than - // 1 element. + // List of components in a ratio. For example, the ratio 1:2:3 will have component values + // [1, 2, 3]. It's expected that list should have more than 1 element. repeated uint32 ratio_component = 1; } -// Represents a mathematical expression such as 1+2. The only expression currently supported is a -// binary operation. +// Represents a mathematical expression such as 1+2*7. Expressions are inherently recursive, so the +// overall expressiveness of this structure (& math expressions as a whole) is defined based on its +// constituent substructures. +// +// This structure is designed to represent both numeric and algebraic expressions. +// +// Values of this proto can be analyzed using MathExpressionSubject. message MathExpression { - // TODO: document inclusive - int32 parse_start_index = 1; - // TODO: document exclusive - int32 parse_end_index = 2; + // The index within the input text stream at which point the expression starts (it's an inclusive + // index). If both this and the end index are zero then no parsing information is included for + // this specific expression. + uint32 parse_start_index = 1; + + // The index within the input text stream at which point the expression ends, exclusively. If both + // this and the start index are zero then no parsing information is included for this specific + // expression. + uint32 parse_end_index = 2; + // The type of expression. oneof expression_type { + // Indicates that this expression is a real value. Real constant = 3; + + // Indicates that this expression is a variable (which is not valid for numeric-only + // expressions). string variable = 4; + + // Indicates that this expression is a binary operation between two sub-expressions. MathBinaryOperation binary_operation = 5; + + // Indicates that this expression is a unary operation that's operating on a sub-expression. MathUnaryOperation unary_operation = 6; + + // Indicates that this expression is a function call with a sub-expression argument. MathFunctionCall function_call = 7; + + // Indicates that this expression represents a nested group, e.g. 1+(2+3). MathExpression group = 8; } } +// Represents a binary operation like addition or multiplication. +// +// Values of this proto can be analyzed using MathExpressionSubject (within the context of a +// MathExpression). message MathBinaryOperation { + // Types of supported binary operations. enum Operator { + // Represents an unknown operator (which is never supported). OPERATOR_UNSPECIFIED = 0; + // Represents adding two values, e.g.: 1+x. ADD = 1; + // Represents subtracting two values, e.g.: x-2. SUBTRACT = 2; + // Represents multiplying two values, e.g.: x*y. MULTIPLY = 3; + // Represents dividing two values, e.g.: 1/x. DIVIDE = 4; + // Represents taking the exponentiation of one value by another, e.g.: x^2. EXPONENTIATE = 5; } + // The type of binary operation. Operator operator = 1; + + // The left-hand side of the operation, e.g. the '1' in 1+2. MathExpression left_operand = 2; + + // The right-hand side of the operation, e.g. the '2' in 1+2. MathExpression right_operand = 3; + + // Indicates whether this operation is implicit. This is currently only supported for + // multiplication, and helps represent expressions like '2x' (which should be treated as 2*x). bool is_implicit = 4; } +// Represents a unary operation like negation. +// +// Values of this proto can be analyzed using MathExpressionSubject (within the context of a +// MathExpression). message MathUnaryOperation { + // Types of supported unary operations. enum Operator { + // Represents an unknown operator (which is never supported). OPERATOR_UNSPECIFIED = 0; + // Represents negating a value, e.g.: -y. NEGATE = 1; + // Represents indicating a value as positive, e.g.: +y. POSITIVE = 2; } + // The type of unary operation, e.g. the '1' in -1. Operator operator = 1; + + // The operand being operated upon. MathExpression operand = 2; } +// Represents a function call, like square root. +// +// Values of this proto can be analyzed using MathExpressionSubject (within the context of a +// MathExpression). message MathFunctionCall { + // The types of supported function calls. enum FunctionType { + // Represents an unknown function (which is never supported). FUNCTION_UNSPECIFIED = 0; + + // Represents a square root operation, e.g. sqrt(4). SQUARE_ROOT = 1; } + // The type of function being called within this subexpression. FunctionType function_type = 1; + + // The subexpression being passed as an argument to the function. MathExpression argument = 2; } +// Represents a mathematical equation (generally algebraic) such as: 2x+3y=0. +// +// Values of this proto can be analyzed using MathEquationSubject. message MathEquation { + // The MathExpression representing the left-hand side of the equation, e.g. the '2x+3y' in + // 2x+3y=0. MathExpression left_side = 1; + + // The MathExpression representing the left-hand side of the equation, e.g. the '0' in 2x+3y=0. MathExpression right_side = 2; } diff --git a/scripts/assets/test_file_exemptions.textproto b/scripts/assets/test_file_exemptions.textproto index 85d68bd005f..6d52e0b504a 100644 --- a/scripts/assets/test_file_exemptions.textproto +++ b/scripts/assets/test_file_exemptions.textproto @@ -646,6 +646,10 @@ exempted_file_path: "testing/src/main/java/org/oppia/android/testing/espresso/Ge exempted_file_path: "testing/src/main/java/org/oppia/android/testing/espresso/ImageViewMatcher.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/espresso/KonfettiViewMatcher.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/junit/DefineAppLanguageLocaleContext.kt" +exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/FractionSubject.kt" +exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/RealSubject.kt" +exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt" +exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/MathEquationSubject.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/mockito/MockitoKotlinHelper.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/network/ApiMockLoader.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/network/MockClassroomService.kt" diff --git a/testing/build.gradle b/testing/build.gradle index 6be1df02f89..02ab02ffdab 100644 --- a/testing/build.gradle +++ b/testing/build.gradle @@ -76,6 +76,7 @@ dependencies { 'com.google.dagger:dagger:2.24', 'com.google.protobuf:protobuf-javalite:3.17.3', 'com.google.truth:truth:1.1.3', + 'com.google.truth.extensions:truth-liteproto-extension:1.1.3', 'nl.dionsegijn:konfetti:1.2.5', 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.2.2', 'org.robolectric:robolectric:4.4', @@ -83,6 +84,7 @@ dependencies { 'org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version', 'org.mockito:mockito-core:2.19.0', project(":domain"), + project(":model"), project(":utility"), ) compileOnly( diff --git a/testing/src/main/java/org/oppia/android/testing/math/BUILD.bazel b/testing/src/main/java/org/oppia/android/testing/math/BUILD.bazel index a1453dd4496..a178fd03604 100644 --- a/testing/src/main/java/org/oppia/android/testing/math/BUILD.bazel +++ b/testing/src/main/java/org/oppia/android/testing/math/BUILD.bazel @@ -1,5 +1,5 @@ """ -TODO: document +General testing utilities and truth subjects for math structures and utilities. """ load("@io_bazel_rules_kotlin//kotlin:kotlin.bzl", "kt_android_library") diff --git a/testing/src/main/java/org/oppia/android/testing/math/FractionSubject.kt b/testing/src/main/java/org/oppia/android/testing/math/FractionSubject.kt index b256d7fd555..03c6ed4ef78 100644 --- a/testing/src/main/java/org/oppia/android/testing/math/FractionSubject.kt +++ b/testing/src/main/java/org/oppia/android/testing/math/FractionSubject.kt @@ -10,21 +10,52 @@ import com.google.common.truth.extensions.proto.LiteProtoSubject import org.oppia.android.app.model.Fraction import org.oppia.android.util.math.toDouble -class FractionSubject( +// TODO(#4097): Add tests for this class. + +/** + * Truth subject for verifying properties of [Fraction]s. + * + * Note that this class is also a [LiteProtoSubject] so other aspects of the underlying [Fraction] + * proto can be verified through inherited methods. + * + * Call [assertThat] to create the subject. + */ +class FractionSubject private constructor( metadata: FailureMetadata, private val actual: Fraction ) : LiteProtoSubject(metadata, actual) { + /** + * Returns a [BooleanSubject] to test [Fraction.getIsNegative]. This method never fails since the + * underlying property defaults to false if it's not defined in the fraction. + */ fun hasNegativePropertyThat(): BooleanSubject = assertThat(actual.isNegative) + /** + * Returns an [IntegerSubject] to test [Fraction.getWholeNumber]. This method never fails since + * the underlying property defaults to 0 if it's not defined in the fraction. + */ fun hasWholeNumberThat(): IntegerSubject = assertThat(actual.wholeNumber) + /** + * Returns an [IntegerSubject] to test [Fraction.getNumerator]. This method never fails since the + * underlying property defaults to 0 if it's not defined in the fraction. + */ fun hasNumeratorThat(): IntegerSubject = assertThat(actual.numerator) + /** + * Returns an [IntegerSubject] to test [Fraction.getDenominator]. This method never fails since + * the underlying property defaults to 0 if it's not defined in the fraction. + */ fun hasDenominatorThat(): IntegerSubject = assertThat(actual.denominator) - fun evaluatesToRealThat(): DoubleSubject = assertThat(actual.toDouble()) + /** + * Returns a [DoubleSubject] to test the converted double version of the fraction being + * represented by this subject. + */ + fun evaluatesToDoubleThat(): DoubleSubject = assertThat(actual.toDouble()) companion object { + /** Returns a new [FractionSubject] to verify aspects of the specified [Fraction] value. */ fun assertThat(actual: Fraction): FractionSubject = assertAbout(::FractionSubject).that(actual) } } diff --git a/testing/src/main/java/org/oppia/android/testing/math/MathEquationSubject.kt b/testing/src/main/java/org/oppia/android/testing/math/MathEquationSubject.kt index ce24e1e08cc..b050a776c89 100644 --- a/testing/src/main/java/org/oppia/android/testing/math/MathEquationSubject.kt +++ b/testing/src/main/java/org/oppia/android/testing/math/MathEquationSubject.kt @@ -2,19 +2,42 @@ package org.oppia.android.testing.math import com.google.common.truth.FailureMetadata import com.google.common.truth.Truth.assertAbout +import com.google.common.truth.Truth.assertThat import com.google.common.truth.extensions.proto.LiteProtoSubject import org.oppia.android.app.model.MathEquation +import org.oppia.android.testing.math.MathEquationSubject.Companion.assertThat import org.oppia.android.testing.math.MathExpressionSubject.Companion.assertThat -class MathEquationSubject( +// TODO(#4097): Add tests for this class. + +/** + * Truth subject for verifying properties of [MathEquation]s. + * + * Note that this class is also a [LiteProtoSubject] so other aspects of the underlying + * [MathEquation] proto can be verified through inherited methods. + * + * Call [assertThat] to create the subject. + */ +class MathEquationSubject private constructor( metadata: FailureMetadata, private val actual: MathEquation ) : LiteProtoSubject(metadata, actual) { + /** + * Returns a [MathExpressionSubject] to test [MathEquation.getLeftSide]. This method never fails + * since the underlying property defaults to a default proto if it's not defined in the equation. + */ fun hasLeftHandSideThat(): MathExpressionSubject = assertThat(actual.leftSide) + /** + * Returns a [MathExpressionSubject] to test [MathEquation.getRightSide]. This method never fails + * since the underlying property defaults to a default proto if it's not defined in the equation. + */ fun hasRightHandSideThat(): MathExpressionSubject = assertThat(actual.rightSide) companion object { + /** + * Returns a new [MathEquationSubject] to verify aspects of the specified [MathEquation] value. + */ fun assertThat(actual: MathEquation): MathEquationSubject = assertAbout(::MathEquationSubject).that(actual) } diff --git a/testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt b/testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt index c9be134e209..d0394a0a3c1 100644 --- a/testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt +++ b/testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt @@ -16,102 +16,287 @@ import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.VARIABLE import org.oppia.android.app.model.MathFunctionCall import org.oppia.android.app.model.MathUnaryOperation import org.oppia.android.app.model.Real +import org.oppia.android.testing.math.MathExpressionSubject.Companion.assertThat import org.oppia.android.testing.math.RealSubject.Companion.assertThat -// See: https://kotlinlang.org/docs/type-safe-builders.html. -class MathExpressionSubject( +// TODO(#4097): Add tests for this class. + +/** + * Truth subject for verifying properties of [MathExpression]s. + * + * This subject makes use of a custom Kotlin DSL to test the structure of an expression. This + * structure allows for recursive verification of the structure since the structure itself is + * recursive. Further, unchecked parts of the structure are not verified. See the following example + * to get an idea of the DSL for verifying expressions (see specific methods the comparator for all + * syntactical options): + * + * ```kotlin + * assertThat(expression).hasStructureThatMatches { + * addition { + * leftOperand { + * constant { + * withValueThat().isIntegerThat().isEqualTo(3) + * } + * } + * rightOperand { + * multiplication { + * leftOperand { + * constant { + * withValueThat().isIntegerThat().isEqualTo(4) + * } + * } + * rightOperand { + * negation { + * operand { + * constant { + * withValueThat().isIntegerThat().isEqualTo(5) + * } + * } + * } + * } + * } + * } + * } + * } + * ``` + * + * The above verifies the following structure: + * ``` + * + + * / \ + * 3 * + * / \ + * 4 - + * | + * 5 + * ``` + * + * (which would correspond to the expression 3+4*-5). + * + * Note that this class is also a [LiteProtoSubject] so other aspects of the underlying + * [MathExpression] proto can be verified through inherited methods. + * + * Call [assertThat] to create the subject. + */ +class MathExpressionSubject private constructor( metadata: FailureMetadata, private val actual: MathExpression ) : LiteProtoSubject(metadata, actual) { + /** + * Begins the structure syntax matcher. + * + * See [ExpressionComparator] for syntax. + */ fun hasStructureThatMatches(init: ExpressionComparator.() -> Unit) { - // TODO: maybe verify that all aspects are verified? ExpressionComparator.createFromExpression(actual).also(init) } - // TODO: update DSL to not have return values (since it's unnecessary). + /** + * DSL syntax provider for verifying the structure of a [MathExpression]. + * + * Note that per the proto definition of [MathExpression], this comparator can only represent one + * of the expression substructures (e.g. constant, variable, binary operations, and others). See + * the member methods for the different substructures that can be verified. + * + * Example syntax for verifying a constant: + * + * ```kotlin + * { + * constant { + * ... + * } + * } + * ``` + * + * is either verifying the root (i.e. via [hasStructureThatMatches]) or is for verifying + * a nested expression (such as through groups). + */ @ExpressionComparatorMarker class ExpressionComparator private constructor(private val expression: MathExpression) { - // TODO: convert to constant comparator? - fun constant(init: ConstantComparator.() -> Unit): ConstantComparator = + /** + * Begins structure matching for this expression as a constant per [MathExpression.getConstant]. + * + * This method will fail if the expression corresponding to the subject is not a constant. See + * [ConstantComparator] for example syntax. + */ + fun constant(init: ConstantComparator.() -> Unit) { ConstantComparator.createFromExpression(expression).also(init) + } - fun variable(init: VariableComparator.() -> Unit): VariableComparator = + /** + * Begins structure matching for this expression as a variable per [MathExpression.getVariable]. + * + * This method will fail if the expression corresponding to the subject is not a variable. See + * [VariableComparator] for example syntax. + */ + fun variable(init: VariableComparator.() -> Unit) { VariableComparator.createFromExpression(expression).also(init) + } - fun addition(init: BinaryOperationComparator.() -> Unit): BinaryOperationComparator { - return BinaryOperationComparator.createFromExpression( + /** + * Begins structure matching for this expression as an addition operation per + * [MathExpression.getBinaryOperation]. + * + * This method will fail if the expression corresponding to the subject is not an addition + * operation. See [BinaryOperationComparator] for example syntax. + */ + fun addition(init: BinaryOperationComparator.() -> Unit) { + BinaryOperationComparator.createFromExpression( expression, expectedOperator = MathBinaryOperation.Operator.ADD ).also(init) } - fun subtraction(init: BinaryOperationComparator.() -> Unit): BinaryOperationComparator { - return BinaryOperationComparator.createFromExpression( + /** + * Begins structure matching for this expression as a subtraction operation per + * [MathExpression.getBinaryOperation]. + * + * This method will fail if the expression corresponding to the subject is not a subtraction + * operation. See [BinaryOperationComparator] for example syntax. + */ + fun subtraction(init: BinaryOperationComparator.() -> Unit) { + BinaryOperationComparator.createFromExpression( expression, expectedOperator = MathBinaryOperation.Operator.SUBTRACT ).also(init) } - fun multiplication(init: BinaryOperationComparator.() -> Unit): BinaryOperationComparator { - return BinaryOperationComparator.createFromExpression( + /** + * Begins structure matching for this expression as a multiplication operation per + * [MathExpression.getBinaryOperation]. + * + * This method will fail if the expression corresponding to the subject is not a multiplication + * operation. See [BinaryOperationComparator] for example syntax. + */ + fun multiplication(init: BinaryOperationComparator.() -> Unit) { + BinaryOperationComparator.createFromExpression( expression, expectedOperator = MathBinaryOperation.Operator.MULTIPLY ).also(init) } - fun division(init: BinaryOperationComparator.() -> Unit): BinaryOperationComparator { - return BinaryOperationComparator.createFromExpression( + /** + * Begins structure matching for this expression as a division operation per + * [MathExpression.getBinaryOperation]. + * + * This method will fail if the expression corresponding to the subject is not a division + * operation. See [BinaryOperationComparator] for example syntax. + */ + fun division(init: BinaryOperationComparator.() -> Unit) { + BinaryOperationComparator.createFromExpression( expression, expectedOperator = MathBinaryOperation.Operator.DIVIDE ).also(init) } - fun exponentiation(init: BinaryOperationComparator.() -> Unit): BinaryOperationComparator { - return BinaryOperationComparator.createFromExpression( + /** + * Begins structure matching for this expression as an exponentiation operation per + * [MathExpression.getBinaryOperation]. + * + * This method will fail if the expression corresponding to the subject is not an exponentiation + * operation. See [BinaryOperationComparator] for example syntax. + */ + fun exponentiation(init: BinaryOperationComparator.() -> Unit) { + BinaryOperationComparator.createFromExpression( expression, expectedOperator = MathBinaryOperation.Operator.EXPONENTIATE ).also(init) } - fun negation(init: UnaryOperationComparator.() -> Unit): UnaryOperationComparator { - return UnaryOperationComparator.createFromExpression( + /** + * Begins structure matching for this expression as a negation operation per + * [MathExpression.getUnaryOperation]. + * + * This method will fail if the expression corresponding to the subject is not a negation + * operation. See [UnaryOperationComparator] for example syntax. + */ + fun negation(init: UnaryOperationComparator.() -> Unit) { + UnaryOperationComparator.createFromExpression( expression, expectedOperator = MathUnaryOperation.Operator.NEGATE ).also(init) } - fun positive(init: UnaryOperationComparator.() -> Unit): UnaryOperationComparator { - return UnaryOperationComparator.createFromExpression( + /** + * Begins structure matching for this expression as a positive operation per + * [MathExpression.getUnaryOperation]. + * + * This method will fail if the expression corresponding to the subject is not a positive + * operation. See [UnaryOperationComparator] for example syntax. + */ + fun positive(init: UnaryOperationComparator.() -> Unit) { + UnaryOperationComparator.createFromExpression( expression, expectedOperator = MathUnaryOperation.Operator.POSITIVE ).also(init) } + /** + * Begins structure matching for this expression as a function call per + * [MathExpression.getFunctionCall]. + * + * This method will fail if the expression corresponding to the subject is not a function call. + * See [FunctionCallComparator] for example syntax. + */ fun functionCallTo( - type: MathFunctionCall.FunctionType, - init: FunctionCallComparator.() -> Unit - ): FunctionCallComparator { - return FunctionCallComparator.createFromExpression( - expression, - expectedFunctionType = type + type: MathFunctionCall.FunctionType, init: FunctionCallComparator.() -> Unit + ) { + FunctionCallComparator.createFromExpression( + expression, expectedFunctionType = type ).also(init) } - fun group(init: ExpressionComparator.() -> Unit): ExpressionComparator { - return createFromExpression(expression.group).also(init) + /** + * Begins structure matching for this expression as a group per [MathExpression.getGroup]. + * + * This method will fail if the expression corresponding to the subject is not a group. Example + * syntax: + * + * ```kotlin + * group { + * ... ... + * } + * ``` + * + * Groups refer to other expressions, so [ExpressionComparator] is used to verify constituent + * properties of the group. + */ + fun group(init: ExpressionComparator.() -> Unit) { + createFromExpression(expression.group).also(init) } internal companion object { + /** Returns a new [ExpressionComparator] corresponding to the specified [MathExpression]. */ fun createFromExpression(expression: MathExpression): ExpressionComparator = ExpressionComparator(expression) } } + /** + * DSL syntax provider for verifying constants. + * + * Example syntax: + * + * ```kotlin + * constant { + * withValueThat()... + * } + * ``` + * + * This comparator provides access to a [RealSubject] to verify the actual constant value. + */ @ExpressionComparatorMarker class ConstantComparator private constructor(private val constant: Real) { + /** + * Returns a [RealSubject] to verify the constant that's being represented by this comparator. + */ fun withValueThat(): RealSubject = assertThat(constant) internal companion object { + /** + * Returns a new [ConstantComparator] corresponding to the specified [MathExpression], + * verifying that it is, indeed, a constant. + */ fun createFromExpression(expression: MathExpression): ConstantComparator { assertThat(expression.expressionTypeCase).isEqualTo(CONSTANT) return ConstantComparator(expression.constant) @@ -119,11 +304,31 @@ class MathExpressionSubject( } } + /** + * DSL syntax provider for verifying variables. + * + * Example syntax: + * + * ```kotlin + * variable { + * withNameThat()... + * } + * ``` + * + * This comparator provides access to a [StringSubject] to verify the actual variable value. + */ @ExpressionComparatorMarker class VariableComparator private constructor(private val variableName: String) { + /** + * Returns a [StringSubject] to verify the variable that's being represented by this comparator. + */ fun withNameThat(): StringSubject = assertThat(variableName) internal companion object { + /** + * Returns a new [VariableComparator] corresponding to the specified [MathExpression], + * verifying that it is, indeed, a variable. + */ fun createFromExpression(expression: MathExpression): VariableComparator { assertThat(expression.expressionTypeCase).isEqualTo(VARIABLE) return VariableComparator(expression.variable) @@ -131,17 +336,56 @@ class MathExpressionSubject( } } + /** + * DSL syntax provider for verifying binary operations, like addition and multiplication. + * + * Example syntax: + * + * ```kotlin + * division { + * leftOperand { + * ... ... + * } + * + * rightOperand { + * ... ... + * } + * } + * ``` + * + * Both the left and right operands represent other [MathExpression]s. + */ @ExpressionComparatorMarker class BinaryOperationComparator private constructor( private val operation: MathBinaryOperation ) { - fun leftOperand(init: ExpressionComparator.() -> Unit): ExpressionComparator = + /** + * Begins structure matching this operation's left operand per + * [MathBinaryOperation.getLeftOperand] for the operation represented by this comparator. + * + * This method provides an [ExpressionComparator] to use to verify the constituent properties + * of the operand. + */ + fun leftOperand(init: ExpressionComparator.() -> Unit) { ExpressionComparator.createFromExpression(operation.leftOperand).also(init) + } - fun rightOperand(init: ExpressionComparator.() -> Unit): ExpressionComparator = + /** + * Begins structure matching this operation's right operand per + * [MathBinaryOperation.getRightOperand] for the operation represented by this comparator. + * + * This method provides an [ExpressionComparator] to use to verify the constituent properties + * of the operand. + */ + fun rightOperand(init: ExpressionComparator.() -> Unit) { ExpressionComparator.createFromExpression(operation.rightOperand).also(init) + } internal companion object { + /** + * Returns a new [BinaryOperationComparator] corresponding to the specified [MathExpression], + * verifying that it is, indeed, a binary operation with the specified operator. + */ fun createFromExpression( expression: MathExpression, expectedOperator: MathBinaryOperation.Operator @@ -155,14 +399,41 @@ class MathExpressionSubject( } } + /** + * DSL syntax provider for verifying unary operations, like negation. + * + * Example syntax: + * + * ```kotlin + * negation { + * operand { + * ... ... + * } + * } + * ``` + * + * The operation's operand represents another [MathExpression]. + */ @ExpressionComparatorMarker class UnaryOperationComparator private constructor( private val operation: MathUnaryOperation ) { - fun operand(init: ExpressionComparator.() -> Unit): ExpressionComparator = + /** + * Begins structure matching this operation's operand per [MathUnaryOperation.getOperand] for + * the operation represented by this comparator. + * + * This method provides an [ExpressionComparator] to use to verify the constituent properties + * of the operand. + */ + fun operand(init: ExpressionComparator.() -> Unit) { ExpressionComparator.createFromExpression(operation.operand).also(init) + } internal companion object { + /** + * Returns a new [UnaryOperationComparator] corresponding to the specified [MathExpression], + * verifying that it is, indeed, a unary operation with the specified operator. + */ fun createFromExpression( expression: MathExpression, expectedOperator: MathUnaryOperation.Operator @@ -176,14 +447,41 @@ class MathExpressionSubject( } } + /** + * DSL syntax provider for verifying function calls, like square root. + * + * Example syntax: + * + * ```kotlin + * functionCallTo(SQUARE_ROOT) { + * argument { + * ... ... + * } + * } + * ``` + * + * The function call's argument represents another [MathExpression]. + */ @ExpressionComparatorMarker class FunctionCallComparator private constructor( private val functionCall: MathFunctionCall ) { - fun argument(init: ExpressionComparator.() -> Unit): ExpressionComparator = + /** + * Begins structure matching the function call's argument per [MathFunctionCall.getArgument] for + * the operation represented by this comparator. + * + * This method provides an [ExpressionComparator] to use to verify the constituent properties + * of the function call's argument. + */ + fun argument(init: ExpressionComparator.() -> Unit) { ExpressionComparator.createFromExpression(functionCall.argument).also(init) + } internal companion object { + /** + * Returns a new [FunctionCallComparator] corresponding to the specified [MathExpression], + * verifying that it is, indeed, a function call with the function type. + */ fun createFromExpression( expression: MathExpression, expectedFunctionType: MathFunctionCall.FunctionType @@ -198,8 +496,13 @@ class MathExpressionSubject( } companion object { + // See: https://kotlinlang.org/docs/type-safe-builders.html for how the DSL definition works. @DslMarker private annotation class ExpressionComparatorMarker + /** + * Returns a new [MathExpressionSubject] to verify aspects of the specified [MathExpression] + * value. + */ fun assertThat(actual: MathExpression): MathExpressionSubject = assertAbout(::MathExpressionSubject).that(actual) } diff --git a/testing/src/main/java/org/oppia/android/testing/math/RealSubject.kt b/testing/src/main/java/org/oppia/android/testing/math/RealSubject.kt index 8f9edddda0b..cb541cb67cb 100644 --- a/testing/src/main/java/org/oppia/android/testing/math/RealSubject.kt +++ b/testing/src/main/java/org/oppia/android/testing/math/RealSubject.kt @@ -7,23 +7,46 @@ import com.google.common.truth.Truth.assertAbout import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage import com.google.common.truth.extensions.proto.LiteProtoSubject +import org.oppia.android.app.model.MathEquation import org.oppia.android.app.model.Real import org.oppia.android.testing.math.FractionSubject.Companion.assertThat -class RealSubject( +// TODO(#4097): Add tests for this class. + +/** + * Truth subject for verifying properties of [Real]s. + * + * Note that this class is also a [LiteProtoSubject] so other aspects of the underlying [Real] proto + * can be verified through inherited methods. + * + * Call [assertThat] to create the subject. + */ +class RealSubject private constructor( metadata: FailureMetadata, private val actual: Real ) : LiteProtoSubject(metadata, actual) { + /** + * Returns a [FractionSubject] to test [Real.getRational]. This will fail if the [Real] pertaining + * to this subject is not of type rational. + */ fun isRationalThat(): FractionSubject { verifyTypeToBe(Real.RealTypeCase.RATIONAL) return assertThat(actual.rational) } + /** + * Returns a [DoubleSubject] to test [Real.getIrrational]. This will fail if the [Real] pertaining + * to this subject is not of type irrational. + */ fun isIrrationalThat(): DoubleSubject { verifyTypeToBe(Real.RealTypeCase.IRRATIONAL) return assertThat(actual.irrational) } + /** + * Returns a [IntegerSubject] to test [Real.getInteger]. This will fail if the [Real] pertaining + * to this subject is not of type integer. + */ fun isIntegerThat(): IntegerSubject { verifyTypeToBe(Real.RealTypeCase.INTEGER) return assertThat(actual.integer) @@ -36,6 +59,7 @@ class RealSubject( } companion object { + /** Returns a new [RealSubject] to verify aspects of the specified [Real] value. */ fun assertThat(actual: Real): RealSubject = assertAbout(::RealSubject).that(actual) } } From 977eb9e077fcc43b45a4b5b17a6db92c5d24f3b9 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 13 Jan 2022 19:28:55 -0800 Subject: [PATCH 045/162] Lint fixes. --- .../org/oppia/android/testing/math/MathExpressionSubject.kt | 5 +++-- .../main/java/org/oppia/android/testing/math/RealSubject.kt | 1 - 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt b/testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt index d0394a0a3c1..c5eb730c868 100644 --- a/testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt +++ b/testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt @@ -23,7 +23,7 @@ import org.oppia.android.testing.math.RealSubject.Companion.assertThat /** * Truth subject for verifying properties of [MathExpression]s. - * + * * This subject makes use of a custom Kotlin DSL to test the structure of an expression. This * structure allows for recursive verification of the structure since the structure itself is * recursive. Further, unchecked parts of the structure are not verified. See the following example @@ -239,7 +239,8 @@ class MathExpressionSubject private constructor( * See [FunctionCallComparator] for example syntax. */ fun functionCallTo( - type: MathFunctionCall.FunctionType, init: FunctionCallComparator.() -> Unit + type: MathFunctionCall.FunctionType, + init: FunctionCallComparator.() -> Unit ) { FunctionCallComparator.createFromExpression( expression, expectedFunctionType = type diff --git a/testing/src/main/java/org/oppia/android/testing/math/RealSubject.kt b/testing/src/main/java/org/oppia/android/testing/math/RealSubject.kt index cb541cb67cb..bb8a180b306 100644 --- a/testing/src/main/java/org/oppia/android/testing/math/RealSubject.kt +++ b/testing/src/main/java/org/oppia/android/testing/math/RealSubject.kt @@ -7,7 +7,6 @@ import com.google.common.truth.Truth.assertAbout import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage import com.google.common.truth.extensions.proto.LiteProtoSubject -import org.oppia.android.app.model.MathEquation import org.oppia.android.app.model.Real import org.oppia.android.testing.math.FractionSubject.Companion.assertThat From eed21d53c0146aefec49d771b7935404d2554dfb Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 13 Jan 2022 21:27:59 -0800 Subject: [PATCH 046/162] Add docs & exempted tests. --- model/src/main/proto/math.proto | 71 +++- scripts/assets/test_file_exemptions.textproto | 1 + .../math/ComparableOperationListSubject.kt | 323 +++++++++++++++++- 3 files changed, 370 insertions(+), 25 deletions(-) diff --git a/model/src/main/proto/math.proto b/model/src/main/proto/math.proto index aa341f58161..dfe00605efe 100644 --- a/model/src/main/proto/math.proto +++ b/model/src/main/proto/math.proto @@ -189,55 +189,104 @@ message MathEquation { MathExpression right_side = 2; } -// Represents a list of comparable mathematics operations. 'Comparable' here means that this -// structure provides a trivial way to compare commutative operations (i.e. by extracting terms from -// multiple subsequent commutative operations into lists that can be deterministically sorted). This -// structure is meant to provide a means to compare two expressions without considering -// associativity or commutativity (though the latter requires the operation lists stored within this -// structure to be sorted before using standard proto equals checking). +// Represents a list of comparable mathematics operations. +// +// 'Comparable' here means that this structure provides a trivial way to compare commutative and +// associative operations (i.e. by extracting terms from multiple subsequent commutative & +// associative operations into lists that can be deterministically sorted). This structure is meant +// to provide a means to compare two expressions without considering associativity or commutativity +// (though the latter requires the operation lists stored within this structure to be sorted before +// using standard proto equals checking). Also note that care must be taken when performing equality +// checking since this structure can contain floating point values that require an epsilon check to +// approximate equality. message ComparableOperationList { + // An operation that can be compared in a way that does not change the value based on + // commutativity or associativity. message ComparableOperation { - // Treat this operation (e.g. x) as negated (e.g. -x). + // Indicates that this operation (e.g. x) should be treated as negated (e.g. -x). bool is_negated = 1; - // Treat this operation (e.g. x) as a multiplicative inverse (e.g. 1/x). + // Indicates that this operation (e.g. x) should be treated as a multiplicative inverse + // (e.g. 1/x). bool is_inverted = 2; + // The supported comparison types. oneof comparison_type { + // Indicates that this operation is a commutative accumulation (that is, a list of subsequent + // operations of the same type that are commutative, e.g. addition or multiplication). CommutativeAccumulation commutative_accumulation = 3; + + // Indicates that this operation is a non-commutative operation and thus cannot be + // accumulated (e.g. exponentiation). NonCommutativeOperation non_commutative_operation = 4; + + // Indicates that this operation represents a constant value. + Real constant_term = 5; + + // Indicates that this operation represents a variable. string variable_term = 6; } } - // Represents an accumulation of operations (such as a summation or product). This helps simplify - // comparison across commutative boundaries by collecting terms into sortable lists, such as the - // expression 1+2+3 becoming [1,2,3] and trivially comparable to [3,2,1] from 3+2+1. + + // Represents an accumulation of operations (such as a summation or product). + // + // This helps simplify comparison across commutative and associative boundaries by collecting + // terms into sortable lists, such as the expression 1+2+3 becoming [1,2,3] and trivially + // comparable to [3,2,1] from 3+2+1 (once sorted). // // Subsequent subtractions are treated as additions with each term arithmetically negated (i.e. // f(x)=-x). Similarly, divisions are considered multiplications with each divisor being // multiplicatively inverted (i.e. the reciprocal function: f(x)=1/x). message CommutativeAccumulation { + // The types of supported accumulations. enum AccumulationType { + // Represents an unsupported accumulation type (which is always invalid). ACCUMULATION_TYPE_UNSPECIFIED = 0; + + // Represents an accumulation of sums, e.g. 1+2+3. SUMMATION = 1; + + // Represents an accumulation of products, e.g. 2*3*4. PRODUCT = 2; } + // The type of this accumulation. AccumulationType accumulation_type = 1; + + // The list of operations being combined in this accumulation (which can be subsequent + // accumulations in more complex expressions). repeated ComparableOperation combined_operations = 2; } + + // Represents a non-commutative operation (such as exponentiation or square roots). + // + // Operations represented by this structure cannot be combined in an accumulation which means they + // can't be "internally" sorted (in order to maintain the commutativity and associativity of the + // operation). message NonCommutativeOperation { + // The types of supported non-commutative operations. oneof operation_type { + // Indicates that this is an exponentiation operation. BinaryOperation exponentiation = 1; + + // Indicates that this is a square root operation with this operation representing the + // argument passed to the square root function. ComparableOperation square_root = 2; } + // Represents a non-commutative binary operation, such as exponentiation. message BinaryOperation { + // The left-hand side of the operation (which itself is another operation), e.g. the '2' in + // 2^3. ComparableOperation left_operand = 1; + + // The right-hand side of the operation (which itself is another operation), e.g. the '3' in + // 2^3. ComparableOperation right_operand = 2; } } + // The root of the operation list (i.e. the lowest precedent operation of the expression). ComparableOperation root_operation = 1; } diff --git a/scripts/assets/test_file_exemptions.textproto b/scripts/assets/test_file_exemptions.textproto index 6d52e0b504a..fa79d219d56 100644 --- a/scripts/assets/test_file_exemptions.textproto +++ b/scripts/assets/test_file_exemptions.textproto @@ -646,6 +646,7 @@ exempted_file_path: "testing/src/main/java/org/oppia/android/testing/espresso/Ge exempted_file_path: "testing/src/main/java/org/oppia/android/testing/espresso/ImageViewMatcher.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/espresso/KonfettiViewMatcher.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/junit/DefineAppLanguageLocaleContext.kt" +exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/ComparableOperationListSubject.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/FractionSubject.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/RealSubject.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt" diff --git a/testing/src/main/java/org/oppia/android/testing/math/ComparableOperationListSubject.kt b/testing/src/main/java/org/oppia/android/testing/math/ComparableOperationListSubject.kt index ce32ec28d2b..3d71f819677 100644 --- a/testing/src/main/java/org/oppia/android/testing/math/ComparableOperationListSubject.kt +++ b/testing/src/main/java/org/oppia/android/testing/math/ComparableOperationListSubject.kt @@ -13,61 +13,226 @@ import org.oppia.android.app.model.ComparableOperationList.ComparableOperation.C import org.oppia.android.app.model.Real import org.oppia.android.testing.math.RealSubject.Companion.assertThat -class ComparableOperationListSubject( +// TODO(#4098): Add tests for this class. + +/** + * Truth subject for verifying properties of [ComparableOperationList]s. + * + * This subject makes use of a custom Kotlin DSL to test the structure of a comparable operation + * list. This structure allows for recursive verification of the structure since the structure + * itself is recursive. Further, unchecked parts of the structure are not verified. See the + * following example to get an idea of the DSL for verifying operations (see specific methods of the + * comparators for all syntactical options): + * + * ```kotlin + * assertThat(comparableOperationList).hasStructureThatMatches { + * hasNegatedPropertyThat().isFalse() + * hasInvertedPropertyThat().isFalse() + * commutativeAccumulationWithType(SUMMATION) { + * hasOperandCountThat().isEqualTo(3) + * index(0) { + * hasNegatedPropertyThat().isFalse() + * hasInvertedPropertyThat().isFalse() + * constantTerm { + * withValueThat().isIntegerThat().isEqualTo(1) + * } + * } + * index(1) { + * hasNegatedPropertyThat().isFalse() + * hasInvertedPropertyThat().isFalse() + * constantTerm { + * withValueThat().isIntegerThat().isEqualTo(3) + * } + * } + * index(2) { + * hasNegatedPropertyThat().isFalse() + * hasInvertedPropertyThat().isFalse() + * constantTerm { + * withValueThat().isIntegerThat().isEqualTo(4) + * } + * } + * } + * } + * ``` + * + * The above verifies the following structure corresponding to the expression 1+3+4. + * + * Note that this class is also a [LiteProtoSubject] so other aspects of the underlying + * [ComparableOperationList] proto can be verified through inherited methods. + * + * Call [assertThat] to create the subject. + */ +class ComparableOperationListSubject private constructor( metadata: FailureMetadata, private val actual: ComparableOperationList ) : LiteProtoSubject(metadata, actual) { + /** + * Begins the structure syntax matcher for the root of the [ComparableOperationList] corresponding + * to this subject (per [ComparableOperationList.getRootOperation]). + * + * See [ComparableOperationComparator] for syntax. + */ fun hasStructureThatMatches(init: ComparableOperationComparator.() -> Unit) { ComparableOperationComparator.createFrom(actual.rootOperation).also(init) } + /** + * DSL syntax provider for verifying the structure of a [ComparableOperation]. + * + * Note that per the proto definition of [ComparableOperation], this comparator can only represent + * one of the operation substructures (e.g. constant, variable, commutative accumulations, and + * others). See the member methods for the different substructures that can be verified. + * + * Example syntax for verifying a constant term: + * + * ```kotlin + * { + * constantTerm { + * ... + * } + * } + * ``` + * + * is either verifying the root (i.e. via [hasStructureThatMatches]) or is for verifying + * a nested operation (such as through a non-commutative operation). + */ @ComparableOperationComparatorMarker class ComparableOperationComparator private constructor( private val operation: ComparableOperation ) { + /** + * Returns a [BooleanSubject] to test [ComparableOperation.getIsNegated]. + * + * This method never fails since the underlying property defaults to false if it's not defined + * in the fraction. + */ fun hasNegatedPropertyThat(): BooleanSubject = assertThat(operation.isNegated) + /** + * Returns a [BooleanSubject] to test [ComparableOperation.getIsNegated]. + * + * This method never fails since the underlying property defaults to false if it's not defined + * in the fraction. + */ fun hasInvertedPropertyThat(): BooleanSubject = assertThat(operation.isInverted) + /** + * Begins structure matching for this operation as a commutative accumulation per + * [ComparableOperation.getCommutativeAccumulation]. + * + * This method will fail if the represented operation is not a commutative accumulation with the + * specified type. See [CommutativeAccumulationComparator] for example syntax. + */ fun commutativeAccumulationWithType( type: ComparableOperationList.CommutativeAccumulation.AccumulationType, init: CommutativeAccumulationComparator.() -> Unit - ): CommutativeAccumulationComparator = + ) { CommutativeAccumulationComparator.createFrom(type, operation).also(init) + } + /** + * Begins structure matching for this operation as a non-commutative operation per + * [ComparableOperation.getNonCommutativeOperation]. + * + * This method will fail if the represented operation is not a non-commutative operation. See + * [NonCommutativeOperationComparator] for example syntax. + */ fun nonCommutativeOperation( init: NonCommutativeOperationComparator.() -> Unit - ): NonCommutativeOperationComparator = + ) { NonCommutativeOperationComparator.createFrom(operation).also(init) + } - fun constantTerm(init: ConstantTermComparator.() -> Unit): ConstantTermComparator = + /** + * Begins structure matching for this operation as a constant term per + * [ComparableOperation.getConstantTerm]. + * + * This method will fail if the represented operation is not a constant term. See + * [ConstantTermComparator] for example syntax. + */ + fun constantTerm(init: ConstantTermComparator.() -> Unit) { ConstantTermComparator.createFrom(operation).also(init) + } - fun variableTerm(init: VariableTermComparator.() -> Unit): VariableTermComparator = + /** + * Begins structure matching for this operation as a variable term per + * [ComparableOperation.getVariableTerm]. + * + * This method will fail if the represented operation is not a variable term. See + * [VariableTermComparator] for example syntax. + */ + fun variableTerm(init: VariableTermComparator.() -> Unit) { VariableTermComparator.createFrom(operation).also(init) + } internal companion object { + /** + * Returns a new [ComparableOperationComparator] corresponding to the specified + * [ComparableOperation]. + */ fun createFrom(operation: ComparableOperation): ComparableOperationComparator = ComparableOperationComparator(operation) } } + /** + * DSL syntax provider for verifying commutative accumulations such as summations or products. + * + * Example syntax: + * + * ```kotlin + * commutativeAccumulationWithType(PRODUCT) { + * hasOperandCountThat().isEqualTo(2) + * index(0) { + * ... ... + * } + * index(1) { + * ... ... + * } + * } + * ``` + * + * As demonstrated, an accumulation represents a list of comparable operations which may be other + * accumulations (though it's guaranteed per the structure that nested accumulations will never be + * the same type), non-commutative operations, constants, or variables. List entries are also + * verified in order. + */ @ComparableOperationComparatorMarker class CommutativeAccumulationComparator private constructor( private val accumulation: ComparableOperationList.CommutativeAccumulation ) { + /** + * Returns a [IntegerSubject] to test + * [ComparableOperationList.CommutativeAccumulation.getCombinedOperationsCount]. + * + * This method never fails since the underlying property defaults to 0 if there are no + * operations in the accumulation. + */ fun hasOperandCountThat(): IntegerSubject = assertThat(accumulation.combinedOperationsCount) + /** + * Begins structure matching for the operation at the specified index within the outer operation + * represented by this comparator. + * + * This method will fail if the operation corresponding to the subject does not have a + * sub-operation at the specified index. See [ComparableOperationComparator] for available + * verification functionality for each indexed operation. + */ fun index( index: Int, init: ComparableOperationComparator.() -> Unit - ): ComparableOperationComparator { - return ComparableOperationComparator.createFrom( + ) { + ComparableOperationComparator.createFrom( accumulation.combinedOperationsList[index] ).also(init) } internal companion object { + /** + * Returns a new [CommutativeAccumulationComparator] corresponding to the specified + * [ComparableOperation], verifying that it is, indeed, a commutative accumulation of the + * specified type. + */ fun createFrom( type: ComparableOperationList.CommutativeAccumulation.AccumulationType, operation: ComparableOperation @@ -80,22 +245,62 @@ class ComparableOperationListSubject( } } + /** + * DSL syntax provider for verifying non-commutative operations such as exponentiation or square + * roots. + * + * Example syntax: + * + * ```kotlin + * nonCommutativeOperation { + * ... ... + * } + * ``` + */ @ComparableOperationComparatorMarker class NonCommutativeOperationComparator private constructor( private val operation: ComparableOperationList.NonCommutativeOperation ) { - fun exponentiation(init: BinaryOperationComparator.() -> Unit): BinaryOperationComparator { + /** + * Begins structure matching for this operation as an exponentiation per + * [ComparableOperationList.NonCommutativeOperation.getExponentiation]. + * + * This method will fail if the operation corresponding to the subject is not an exponentiation. + * See [BinaryOperationComparator] for specifics on the operation comparator used here. Example + * syntax: + * + * ```kotlin + * exponentiation { + * ... ... + * } + * ``` + */ + fun exponentiation(init: BinaryOperationComparator.() -> Unit) { verifyTypeAs( ComparableOperationList.NonCommutativeOperation.OperationTypeCase.EXPONENTIATION ) - return BinaryOperationComparator.createFrom(operation.exponentiation).also(init) + BinaryOperationComparator.createFrom(operation.exponentiation).also(init) } + /** + * Begins structure matching for this operation as a square root operation per + * [ComparableOperationList.NonCommutativeOperation.getSquareRoot]. + * + * This method will fail if the operation corresponding to the subject is not a square root. The + * argument is another [ComparableOperation] hence the utilization of + * [ComparableOperationComparator]. Example syntax: + * + * ```kotlin + * squareRootWithArgument { + * ... ... + * } + * ``` + */ fun squareRootWithArgument( init: ComparableOperationComparator.() -> Unit - ): ComparableOperationComparator { + ) { verifyTypeAs(ComparableOperationList.NonCommutativeOperation.OperationTypeCase.SQUARE_ROOT) - return ComparableOperationComparator.createFrom(operation.squareRoot).also(init) + ComparableOperationComparator.createFrom(operation.squareRoot).also(init) } private fun verifyTypeAs( @@ -105,6 +310,11 @@ class ComparableOperationListSubject( } internal companion object { + /** + * Returns a new [NonCommutativeOperationComparator] corresponding to the specified + * [ComparableOperation], verifying that it is, indeed, a non-commutative operation of the + * specified type. + */ fun createFrom(operation: ComparableOperation): NonCommutativeOperationComparator { assertThat(operation.comparisonTypeCase) .isEqualTo(ComparisonTypeCase.NON_COMMUTATIVE_OPERATION) @@ -113,34 +323,95 @@ class ComparableOperationListSubject( } } + /** + * DSL syntax provider for verifying non-commutative binary operations (e.g. exponentiation). + * + * Example syntax: + * + * ```kotlin + * { + * leftOperand { + * ... ... + * } + * rightOperand { + * ... ... + * } + * } + * ``` + * + * Both the left and right operands represent other [ComparableOperation]s. Further, this + * comparator is used in conjunction with [NonCommutativeOperationComparator]. + */ @ComparableOperationComparatorMarker class BinaryOperationComparator private constructor( private val operation: ComparableOperationList.NonCommutativeOperation.BinaryOperation ) { + /** + * Begins structure matching this operation's left operand per + * [ComparableOperationList.NonCommutativeOperation.BinaryOperation.getLeftOperand] for the + * operation represented by this comparator. + * + * This method provides an [ComparableOperationComparator] to use to verify the constituent + * properties of the operand. + */ fun leftOperand( init: ComparableOperationComparator.() -> Unit - ): ComparableOperationComparator = + ) { ComparableOperationComparator.createFrom(operation.leftOperand).also(init) + } + /** + * Begins structure matching this operation's right operand per + * [ComparableOperationList.NonCommutativeOperation.BinaryOperation.getRightOperand] for the + * operation represented by this comparator. + * + * This method provides an [ComparableOperationComparator] to use to verify the constituent + * properties of the operand. + */ fun rightOperand( init: ComparableOperationComparator.() -> Unit - ): ComparableOperationComparator = + ) { ComparableOperationComparator.createFrom(operation.rightOperand).also(init) + } internal companion object { + /** + * Returns a new [BinaryOperationComparator] corresponding to the specified non-commutative + * binary operation. + */ fun createFrom( operation: ComparableOperationList.NonCommutativeOperation.BinaryOperation ): BinaryOperationComparator = BinaryOperationComparator(operation) } } + /** + * DSL syntax provider for verifying constants. + * + * Example syntax: + * + * ```kotlin + * constantTerm { + * withValueThat()... + * } + * ``` + * + * This comparator provides access to a [RealSubject] to verify the actual constant value. + */ @ComparableOperationComparatorMarker class ConstantTermComparator private constructor( private val constant: Real ) { + /** + * Returns a [RealSubject] to verify the constant that's being represented by this comparator. + */ fun withValueThat(): RealSubject = assertThat(constant) internal companion object { + /** + * Returns a new [ConstantTermComparator] corresponding to the specified + * [ComparableOperation], verifying that it is, indeed, a constant term. + */ fun createFrom(operation: ComparableOperation): ConstantTermComparator { assertThat(operation.comparisonTypeCase).isEqualTo(ComparisonTypeCase.CONSTANT_TERM) return ConstantTermComparator(operation.constantTerm) @@ -148,13 +419,33 @@ class ComparableOperationListSubject( } } + /** + * DSL syntax provider for verifying variables. + * + * Example syntax: + * + * ```kotlin + * variableTerm { + * withNameThat()... + * } + * ``` + * + * This comparator provides access to a [StringSubject] to verify the actual variable value. + */ @ComparableOperationComparatorMarker class VariableTermComparator private constructor( private val variableName: String ) { + /** + * Returns a [StringSubject] to verify the variable that's being represented by this comparator. + */ fun withNameThat(): StringSubject = assertThat(variableName) internal companion object { + /** + * Returns a new [VariableTermComparator] corresponding to the specified + * [ComparableOperation], verifying that it is, indeed, a variable term. + */ fun createFrom(operation: ComparableOperation): VariableTermComparator { assertThat(operation.comparisonTypeCase).isEqualTo(ComparisonTypeCase.VARIABLE_TERM) return VariableTermComparator(operation.variableTerm) @@ -163,9 +454,13 @@ class ComparableOperationListSubject( } companion object { - // See: https://kotlinlang.org/docs/type-safe-builders.html. + // See: https://kotlinlang.org/docs/type-safe-builders.html for how the DSL definition works. @DslMarker private annotation class ComparableOperationComparatorMarker + /** + * Returns a new [ComparableOperationListSubject] to verify aspects of the specified + * [ComparableOperationList] value. + */ fun assertThat(actual: ComparableOperationList): ComparableOperationListSubject = assertAbout(::ComparableOperationListSubject).that(actual) } From 080e7dd048265b892d5a53451f3d02c3c892a9e0 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 13 Jan 2022 21:39:27 -0800 Subject: [PATCH 047/162] Remove blank line. --- model/src/main/proto/math.proto | 1 - 1 file changed, 1 deletion(-) diff --git a/model/src/main/proto/math.proto b/model/src/main/proto/math.proto index dfe00605efe..9a97c7821ad 100644 --- a/model/src/main/proto/math.proto +++ b/model/src/main/proto/math.proto @@ -221,7 +221,6 @@ message ComparableOperationList { NonCommutativeOperation non_commutative_operation = 4; // Indicates that this operation represents a constant value. - Real constant_term = 5; // Indicates that this operation represents a variable. From 904aad89216f975274c42367c4a57f848f32d9ab Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Tue, 18 Jan 2022 18:29:58 -0800 Subject: [PATCH 048/162] Add docs + tests. --- model/src/main/proto/math.proto | 12 + scripts/assets/test_file_exemptions.textproto | 1 + .../android/testing/math/PolynomialSubject.kt | 83 ++- .../android/util/math/FloatExtensions.kt | 15 +- .../android/util/math/FractionExtensions.kt | 9 +- .../android/util/math/PolynomialExtensions.kt | 6 + .../android/util/math/RatioExtensions.kt | 5 + .../oppia/android/util/math/RealExtensions.kt | 37 +- .../org/oppia/android/util/math/BUILD.bazel | 73 +++ .../android/util/math/FloatExtensionsTest.kt | 155 +++++ .../util/math/FractionExtensionsTest.kt | 530 ++++++++++++++++++ .../util/math/PolynomialExtensionsTest.kt | 390 +++++++++++++ .../android/util/math/RatioExtensionsTest.kt | 4 +- .../android/util/math/RealExtensionsTest.kt | 414 ++++++++++++++ 14 files changed, 1720 insertions(+), 14 deletions(-) create mode 100644 utility/src/test/java/org/oppia/android/util/math/FloatExtensionsTest.kt create mode 100644 utility/src/test/java/org/oppia/android/util/math/FractionExtensionsTest.kt create mode 100644 utility/src/test/java/org/oppia/android/util/math/PolynomialExtensionsTest.kt create mode 100644 utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt diff --git a/model/src/main/proto/math.proto b/model/src/main/proto/math.proto index 877a070b539..4adf75cb83e 100644 --- a/model/src/main/proto/math.proto +++ b/model/src/main/proto/math.proto @@ -291,15 +291,27 @@ message ComparableOperationList { ComparableOperation root_operation = 1; } +// Represents a polynomial, e.g.: 2x^3+3x-y+7. message Polynomial { + // The list of terms in this polynomial, e.g. the '2x^3', '3x', '-y', and '-7' in 2x^3+3x-y+7. repeated Term term = 1; + // Represents a polynomial term, i.e. a real coefficient multiplied by one or more variables, each + // of which may have a power >= 1. message Term { + // The coefficient of this term (which may be zero or negative), e.g. '2' in '2x^3'. Real coefficient = 1; + + // The variables of this term. This list can be zero or more variables long (where zero + // variables indicate a constant polynomial term). repeated Variable variable = 2; + // A variable within the term, that is, a variable with a positive power. message Variable { + // The name of the variable, e.g. 'x' in 'x^3'. string name = 1; + + // The power of the variable, e.g. '3' in 'x^3'. uint32 power = 2; } } diff --git a/scripts/assets/test_file_exemptions.textproto b/scripts/assets/test_file_exemptions.textproto index fa79d219d56..ae6a4f367b2 100644 --- a/scripts/assets/test_file_exemptions.textproto +++ b/scripts/assets/test_file_exemptions.textproto @@ -649,6 +649,7 @@ exempted_file_path: "testing/src/main/java/org/oppia/android/testing/junit/Defin exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/ComparableOperationListSubject.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/FractionSubject.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/RealSubject.kt" +exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/PolynomialSubject.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/MathEquationSubject.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/mockito/MockitoKotlinHelper.kt" diff --git a/testing/src/main/java/org/oppia/android/testing/math/PolynomialSubject.kt b/testing/src/main/java/org/oppia/android/testing/math/PolynomialSubject.kt index bb4a3d970d5..23dc2b479a7 100644 --- a/testing/src/main/java/org/oppia/android/testing/math/PolynomialSubject.kt +++ b/testing/src/main/java/org/oppia/android/testing/math/PolynomialSubject.kt @@ -8,11 +8,22 @@ import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage import com.google.common.truth.extensions.proto.LiteProtoSubject import org.oppia.android.app.model.Polynomial +import org.oppia.android.testing.math.PolynomialSubject.Companion.assertThat import org.oppia.android.testing.math.RealSubject.Companion.assertThat import org.oppia.android.util.math.getConstant import org.oppia.android.util.math.isConstant import org.oppia.android.util.math.toPlainText +// TODO(#4100): Add tests for this class. + +/** + * Truth subject for verifying properties of [Polynomial]s. + * + * Note that this class is also a [LiteProtoSubject] so other aspects of the underlying [Polynomial] + * proto can be verified through inherited methods. + * + * Call [assertThat] to create the subject. + */ class PolynomialSubject( metadata: FailureMetadata, private val actual: Polynomial? @@ -24,28 +35,48 @@ class PolynomialSubject( } } + /** Verifies that the represented [Polynomial] is null (i.e. not a valid polynomial). */ fun isNotValidPolynomial() { - // TODO: use toPlainText here. assertWithMessage( "Expected polynomial to be undefined, but was: ${actual?.toPlainText()}" ).that(actual).isNull() } + /** + * Verifies that the represented [Polynomial] is a constant (i.e. [Polynomial.isConstant] and + * returns a [RealSubject] to verify the value of the constant polynomial. + */ fun isConstantThat(): RealSubject { - // TODO: use toPlainText here. - assertWithMessage("Expected polynomial to be constant, but was: $nonNullActual") + assertWithMessage("Expected polynomial to be constant, but was: ${nonNullActual.toPlainText()}") .that(nonNullActual.isConstant()) .isTrue() return assertThat(nonNullActual.getConstant()) } + /** + * Returns an [IntegerSubject] to test [Polynomial.getTermCount]. + * + * This method never fails since the underlying property defaults to 0 if there are no terms + * defined in the polynomial (unless the polynomial is null). + */ fun hasTermCountThat(): IntegerSubject = assertThat(nonNullActual.termCount) + /** + * Returns a [PolynomialTermSubject] to test [Polynomial.getTerm] for the specified index. + * + * This method throws if the index doesn't correspond to a valid term. Callers should first verify + * the term count using [hasTermCountThat]. + */ fun term(index: Int): PolynomialTermSubject = assertThat(nonNullActual.termList[index]) + /** + * Returns a [StringSubject] to test the plain-text representation of the [Polynomial] (i.e. via + * [Polynomial.toPlainText]). + */ fun evaluatesToPlainTextThat(): StringSubject = assertThat(nonNullActual.toPlainText()) companion object { + /** Returns a new [PolynomialSubject] to verify aspects of the specified [Polynomial] value. */ fun assertThat(actual: Polynomial?): PolynomialSubject = assertAbout(::PolynomialSubject).that(actual) @@ -56,24 +87,70 @@ class PolynomialSubject( assertAbout(::PolynomialTermVariableSubject).that(actual) } + /** + * Truth subject for verifying properties of [Polynomial.Term]s. + * + * Note that this class is also a [LiteProtoSubject] so other aspects of the underlying + * [Polynomial.Term] proto can be verified through inherited methods. + */ class PolynomialTermSubject( metadata: FailureMetadata, private val actual: Polynomial.Term ) : LiteProtoSubject(metadata, actual) { + /** + * Returns a [RealSubject] to test [Polynomial.Term.getCoefficient] for the represented term. + * + * This method never fails since the underlying property defaults to a default instance if it's + * not defined in the term. + */ fun hasCoefficientThat(): RealSubject = assertThat(actual.coefficient) + /** + * Returns an [IntegerSubject] to test [Polynomial.Term.getVariableCount] for the represented + * term. + * + * This method never fails since the underlying property defaults to 0 if there are no variables + * in the represented term. + */ fun hasVariableCountThat(): IntegerSubject = assertThat(actual.variableCount) + /** + * Returns a [PolynomialTermVariableSubject] to test [Polynomial.Term.getVariable] for the + * specified index. + * + * This method throws if the index doesn't correspond to a valid variable. Callers should first + * verify the variable count using [hasVariableCountThat]. + */ fun variable(index: Int): PolynomialTermVariableSubject = assertThat(actual.variableList[index]) } + /** + * Truth subject for verifying properties of [Polynomial.Term.Variable]s. + * + * Note that this class is also a [LiteProtoSubject] so other aspects of the underlying + * [Polynomial.Term.Variable] proto can be verified through inherited methods. + */ class PolynomialTermVariableSubject( metadata: FailureMetadata, private val actual: Polynomial.Term.Variable ) : LiteProtoSubject(metadata, actual) { + /** + * Returns a [StringSubject] to test [Polynomial.Term.Variable.getName] for the represented + * variable. + * + * This method never fails since the underlying property defaults to empty string if it's not + * defined in the variable. + */ fun hasNameThat(): StringSubject = assertThat(actual.name) + /** + * Returns an [IntegerSubject] to test [Polynomial.Term.Variable.getPower] for the represented + * variable. + * + * This method never fails since the underlying property defaults to 0 if it's not defined in + * the variable. + */ fun hasPowerThat(): IntegerSubject = assertThat(actual.power) } } diff --git a/utility/src/main/java/org/oppia/android/util/math/FloatExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/FloatExtensions.kt index 2ca5ece9da3..27ac002c08c 100644 --- a/utility/src/main/java/org/oppia/android/util/math/FloatExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/FloatExtensions.kt @@ -2,17 +2,26 @@ package org.oppia.android.util.math import kotlin.math.abs -/** The error margin used for float equality by [Float.approximatelyEquals]. */ +/** The error margin used for approximately [Float] and [Double] equality checking. */ const val FLOAT_EQUALITY_INTERVAL = 1e-5 -/** Returns whether this float approximately equals another based on a consistent epsilon value. */ +/** + * Returns whether this float approximately equals another based on a consistent epsilon value + * ([FLOAT_EQUALITY_INTERVAL]). + */ fun Float.approximatelyEquals(other: Float): Boolean { return abs(this - other) < FLOAT_EQUALITY_INTERVAL } -/** Returns whether this double approximately equals another based on a consistent epsilon value. */ +/** Returns whether this double approximately equals another based on a consistent epsilon value + * ([FLOAT_EQUALITY_INTERVAL]). + */ fun Double.approximatelyEquals(other: Double): Boolean { return abs(this - other) < FLOAT_EQUALITY_INTERVAL } +/** + * Returns a string representation of this [Double] that keeps the double in pure decimal and never + * relies on scientific notation (unlike [Double.toString]). + */ fun Double.toPlainString(): String = toBigDecimal().toPlainString() diff --git a/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt index 1da9fef1857..d4881613346 100644 --- a/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt @@ -32,9 +32,10 @@ fun Fraction.toDouble(): Double { */ fun Fraction.toAnswerString(): String { return when { - isOnlyWholeNumber() -> { - // Fraction is only a whole number. - if (isNegative) "-$wholeNumber" else "$wholeNumber" + // Fraction is only a whole number. + isOnlyWholeNumber() -> when (wholeNumber) { + 0 -> "0" // 0 is always 0 regardless of its negative sign. + else -> if (isNegative) "-$wholeNumber" else "$wholeNumber" } wholeNumber == 0 -> { // Fraction contains just a fraction (no whole number). @@ -86,6 +87,6 @@ operator fun Fraction.unaryMinus(): Fraction { } /** Returns the greatest common divisor between two integers. */ -fun gcd(x: Int, y: Int): Int { +private fun gcd(x: Int, y: Int): Int { return if (y == 0) x else gcd(y, x % y) } diff --git a/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt index a4ba72213be..25bb5f2955d 100644 --- a/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt @@ -17,6 +17,12 @@ fun Polynomial.isConstant(): Boolean = termCount == 1 && getTerm(0).variableCoun */ fun Polynomial.getConstant(): Real = getTerm(0).coefficient +/** + * Returns a human-readable, plaintext representation of this [Polynomial]. + * + * The returned string is guaranteed to be a syntactically correct algebraic expression representing + * the polynomial, e.g. "1+x-7x^2"). + */ fun Polynomial.toPlainText(): String { return termList.map { it.toPlainText() }.reduce { acc, termAnswerStr -> if (termAnswerStr.startsWith("-")) { diff --git a/utility/src/main/java/org/oppia/android/util/math/RatioExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/RatioExtensions.kt index 83d85e9098c..4b2b559d579 100644 --- a/utility/src/main/java/org/oppia/android/util/math/RatioExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/RatioExtensions.kt @@ -21,3 +21,8 @@ fun RatioExpression.toSimplestForm(): List { fun RatioExpression.toAnswerString(): String { return ratioComponentList.joinToString(separator = ":") } + +/** Returns the greatest common divisor between two integers. */ +private fun gcd(x: Int, y: Int): Int { + return if (y == 0) x else gcd(y, x % y) +} diff --git a/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt index 6df36abd3b6..b4fb0a39dad 100644 --- a/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt @@ -6,24 +6,42 @@ import org.oppia.android.app.model.Real.RealTypeCase.IRRATIONAL import org.oppia.android.app.model.Real.RealTypeCase.RATIONAL import org.oppia.android.app.model.Real.RealTypeCase.REALTYPE_NOT_SET +/** + * Returns whether this [Real] is explicitly a rational type (i.e. a fraction). + * + * This returns false if the real is an integer despite that being mathematically rational. + */ fun Real.isRational(): Boolean = realTypeCase == RATIONAL +/** Returns whether this [Real] is negative. */ fun Real.isNegative(): Boolean = when (realTypeCase) { RATIONAL -> rational.isNegative IRRATIONAL -> irrational < 0 INTEGER -> integer < 0 - REALTYPE_NOT_SET, null -> throw Exception("Invalid real: $this.") + REALTYPE_NOT_SET, null -> throw IllegalStateException("Invalid real: $this.") } +/** + * Returns a [Double] representation of this [Real] that is approximately the same value (per + * [isApproximatelyEqualTo]). + */ fun Real.toDouble(): Double { return when (realTypeCase) { RATIONAL -> rational.toDouble() INTEGER -> integer.toDouble() IRRATIONAL -> irrational - REALTYPE_NOT_SET, null -> throw Exception("Invalid real: $this.") + REALTYPE_NOT_SET, null -> throw IllegalStateException("Invalid real: $this.") } } +/** + * Returns a human-readable, plaintext representation of this [Real]. + * + * Note that the returned value is guaranteed to be a self-contained numeric expression representing + * the real (which means proper fractions are converted to improper answer strings since fractions + * like '1 1/2' can't be written as a numeric expression without converting them to an improper + * form: '3/2'). + */ fun Real.toPlainText(): String = when (realTypeCase) { // Note that the rational part is first converted to an improper fraction since mixed fractions // can't be expressed as a single coefficient in typical polynomial syntax). @@ -33,19 +51,32 @@ fun Real.toPlainText(): String = when (realTypeCase) { REALTYPE_NOT_SET, null -> "" } +/** + * Returns whether this [Real] is approximately equal to the specified [Double] per + * [Double.approximatelyEquals]. + */ fun Real.isApproximatelyEqualTo(value: Double): Boolean { return toDouble().approximatelyEquals(value) } +/** + * Returns a negative version of this [Real] such that the original real plus the negative version + * would result in zero. + */ operator fun Real.unaryMinus(): Real { return when (realTypeCase) { RATIONAL -> recompute { it.setRational(-rational) } IRRATIONAL -> recompute { it.setIrrational(-irrational) } INTEGER -> recompute { it.setInteger(-integer) } - REALTYPE_NOT_SET, null -> throw Exception("Invalid real: $this.") + REALTYPE_NOT_SET, null -> throw IllegalStateException("Invalid real: $this.") } } +/** + * Returns an absolute value of this [Real] (that is, a non-negative [Real]). + * + * [isNegative] is guaranteed to return false for the returned value. + */ fun abs(real: Real): Real = if (real.isNegative()) -real else real private fun Real.recompute(transform: (Real.Builder) -> Real.Builder): Real { diff --git a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel index 313a5a1f751..f85a7664379 100644 --- a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel @@ -4,6 +4,60 @@ Tests for general-purpose mathematics utilities. load("//:oppia_android_test.bzl", "oppia_android_test") +oppia_android_test( + name = "FloatExtensionsTest", + srcs = ["FloatExtensionsTest.kt"], + custom_package = "org.oppia.android.util.math", + test_class = "org.oppia.android.util.math.FloatExtensionsTest", + test_manifest = "//utility:test_manifest", + deps = [ + "//model/src/main/proto:math_java_proto_lite", + "//third_party:androidx_test_ext_junit", + "//third_party:com_google_truth_truth", + "//third_party:junit_junit", + "//third_party:org_robolectric_robolectric", + "//third_party:robolectric_android-all", + "//utility/src/main/java/org/oppia/android/util/math:extensions", + ], +) + +oppia_android_test( + name = "FractionExtensionsTest", + srcs = ["FractionExtensionsTest.kt"], + custom_package = "org.oppia.android.util.math", + test_class = "org.oppia.android.util.math.FractionExtensionsTest", + test_manifest = "//utility:test_manifest", + deps = [ + "//model/src/main/proto:math_java_proto_lite", + "//testing", + "//testing/src/main/java/org/oppia/android/testing/math:fraction_subject", + "//third_party:androidx_test_ext_junit", + "//third_party:com_google_truth_truth", + "//third_party:junit_junit", + "//third_party:org_robolectric_robolectric", + "//third_party:robolectric_android-all", + "//utility/src/main/java/org/oppia/android/util/math:extensions", + ], +) + +oppia_android_test( + name = "PolynomialExtensionsTest", + srcs = ["PolynomialExtensionsTest.kt"], + custom_package = "org.oppia.android.util.math", + test_class = "org.oppia.android.util.math.PolynomialExtensionsTest", + test_manifest = "//utility:test_manifest", + deps = [ + "//model/src/main/proto:math_java_proto_lite", + "//testing/src/main/java/org/oppia/android/testing/math:polynomial_subject", + "//third_party:androidx_test_ext_junit", + "//third_party:com_google_truth_truth", + "//third_party:junit_junit", + "//third_party:org_robolectric_robolectric", + "//third_party:robolectric_android-all", + "//utility/src/main/java/org/oppia/android/util/math:extensions", + ], +) + oppia_android_test( name = "RatioExtensionsTest", srcs = ["RatioExtensionsTest.kt"], @@ -20,3 +74,22 @@ oppia_android_test( "//utility/src/main/java/org/oppia/android/util/math:extensions", ], ) + +oppia_android_test( + name = "RealExtensionsTest", + srcs = ["RealExtensionsTest.kt"], + custom_package = "org.oppia.android.util.math", + test_class = "org.oppia.android.util.math.RealExtensionsTest", + test_manifest = "//utility:test_manifest", + deps = [ + "//model/src/main/proto:math_java_proto_lite", + "//testing", + "//testing/src/main/java/org/oppia/android/testing/math:real_subject", + "//third_party:androidx_test_ext_junit", + "//third_party:com_google_truth_truth", + "//third_party:junit_junit", + "//third_party:org_robolectric_robolectric", + "//third_party:robolectric_android-all", + "//utility/src/main/java/org/oppia/android/util/math:extensions", + ], +) diff --git a/utility/src/test/java/org/oppia/android/util/math/FloatExtensionsTest.kt b/utility/src/test/java/org/oppia/android/util/math/FloatExtensionsTest.kt new file mode 100644 index 00000000000..9010828169e --- /dev/null +++ b/utility/src/test/java/org/oppia/android/util/math/FloatExtensionsTest.kt @@ -0,0 +1,155 @@ +package org.oppia.android.util.math + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.annotation.LooperMode + +/** Tests for [Float] and [Double] extensions. */ +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") +@RunWith(AndroidJUnit4::class) +@LooperMode(LooperMode.Mode.PAUSED) +class FloatExtensionsTest { + + @Test + fun testFloat_approximatelyEquals_bothZero_returnsTrue() { + val leftFloat = 0f + val rightFloat = 0f + + val result = leftFloat.approximatelyEquals(rightFloat) + + assertThat(result).isTrue() + } + + @Test + fun testFloat_approximatelyEquals_sameNonZeroValue_returnsTrue() { + val leftFloat = 1.2f + val rightFloat = 1.2f + + val result = leftFloat.approximatelyEquals(rightFloat) + + assertThat(result).isTrue() + } + + @Test + fun testFloat_approximatelyEquals_nonZeroValues_withinInterval_returnsTrue() { + val leftFloat = 1.2f + val rightFloat = leftFloat + FLOAT_EQUALITY_INTERVAL.toFloat() / 10f + + val result = leftFloat.approximatelyEquals(rightFloat) + + // Verify that they are approximately equal, but not actually the same float. + assertThat(result).isTrue() + assertThat(leftFloat).isNotEqualTo(rightFloat) + } + + @Test + fun testFloat_approximatelyEquals_nonZeroValues_outsideInterval_returnsFalse() { + val leftFloat = 1.2f + val rightFloat = leftFloat + FLOAT_EQUALITY_INTERVAL.toFloat() * 2f + + val result = leftFloat.approximatelyEquals(rightFloat) + + assertThat(result).isFalse() + } + + @Test + fun testFloat_approximatelyEquals_nonZeroValues_veryDifferent_returnsFalse() { + val leftFloat = 1.2f + val rightFloat = 7.3f + + val result = leftFloat.approximatelyEquals(rightFloat) + + assertThat(result).isFalse() + } + + @Test + fun testDouble_approximatelyEquals_bothZero_returnsTrue() { + val leftDouble = 0.0 + val rightDouble = 0.0 + + val result = leftDouble.approximatelyEquals(rightDouble) + + assertThat(result).isTrue() + } + + @Test + fun testDouble_approximatelyEquals_sameNonZeroValue_returnsTrue() { + val leftDouble = 1.2 + val rightDouble = 1.2 + + val result = leftDouble.approximatelyEquals(rightDouble) + + assertThat(result).isTrue() + } + + @Test + fun testDouble_approximatelyEquals_nonZeroValues_withinInterval_returnsTrue() { + val leftDouble = 1.2 + val rightDouble = leftDouble + FLOAT_EQUALITY_INTERVAL / 10.0 + + val result = leftDouble.approximatelyEquals(rightDouble) + + // Verify that they are approximately equal, but not actually the same double. + assertThat(result).isTrue() + assertThat(leftDouble).isNotEqualTo(rightDouble) + } + + @Test + fun testDouble_approximatelyEquals_nonZeroValues_outsideInterval_returnsFalse() { + val leftDouble = 1.2 + val rightDouble = leftDouble + FLOAT_EQUALITY_INTERVAL * 2 + + val result = leftDouble.approximatelyEquals(rightDouble) + + assertThat(result).isFalse() + } + + @Test + fun testDouble_approximatelyEquals_nonZeroValues_veryDifferent_returnsFalse() { + val leftDouble = 1.2 + val rightDouble = 7.3 + + val result = leftDouble.approximatelyEquals(rightDouble) + + assertThat(result).isFalse() + } + + @Test + fun testDouble_toPlainText_zero_returnsStringWithZero() { + val testDouble = 0.0 + + val plainText = testDouble.toPlainString() + + assertThat(plainText).isEqualTo("0.0") + } + + @Test + fun testDouble_toPlainText_nonZero_returnsStringForNonZero() { + val testDouble = 4.0 + + val plainText = testDouble.toPlainString() + + assertThat(plainText).isEqualTo("4.0") + } + + @Test + fun testDouble_toPlainText_negativeMultiDigitNumber_returnsCorrectString() { + val testDouble = -1.73 + + val plainText = testDouble.toPlainString() + + assertThat(plainText).isEqualTo("-1.73") + } + + @Test + fun testDouble_toPlainText_largeNumber_returnsNumberWithoutScientificNotation() { + val testDouble = 84758123.3213989 + + val plainText = testDouble.toPlainString() + + assertThat(plainText).isEqualTo("84758123.3213989") + } +} diff --git a/utility/src/test/java/org/oppia/android/util/math/FractionExtensionsTest.kt b/utility/src/test/java/org/oppia/android/util/math/FractionExtensionsTest.kt new file mode 100644 index 00000000000..48121da69d7 --- /dev/null +++ b/utility/src/test/java/org/oppia/android/util/math/FractionExtensionsTest.kt @@ -0,0 +1,530 @@ +package org.oppia.android.util.math + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.oppia.android.app.model.Fraction +import org.oppia.android.testing.assertThrows +import org.oppia.android.testing.math.FractionSubject.Companion.assertThat +import org.robolectric.annotation.LooperMode +import java.lang.ArithmeticException + +/** Tests for [Fraction] extensions. */ +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") +@RunWith(AndroidJUnit4::class) +@LooperMode(LooperMode.Mode.PAUSED) +class FractionExtensionsTest { + private companion object { + private val ZERO_FRACTION = Fraction.newBuilder().apply { + denominator = 1 + }.build() + + private val NEGATIVE_ZERO_FRACTION = Fraction.newBuilder().apply { + isNegative = true + denominator = 1 + }.build() + + private val ONE_HALF_FRACTION = Fraction.newBuilder().apply { + numerator = 1 + denominator = 2 + }.build() + + private val NEGATIVE_ONE_HALF_FRACTION = Fraction.newBuilder().apply { + isNegative = true + numerator = 1 + denominator = 2 + }.build() + + private val ONE_AND_ONE_HALF_FRACTION = Fraction.newBuilder().apply { + wholeNumber = 1 + numerator = 1 + denominator = 2 + }.build() + + private val NEGATIVE_ONE_AND_ONE_HALF_FRACTION = Fraction.newBuilder().apply { + isNegative = true + wholeNumber = 1 + numerator = 1 + denominator = 2 + }.build() + + private val THREE_HALVES_FRACTION = Fraction.newBuilder().apply { + numerator = 3 + denominator = 2 + }.build() + + private val NEGATIVE_THREE_HALVES_FRACTION = Fraction.newBuilder().apply { + isNegative = true + numerator = 3 + denominator = 2 + }.build() + + private val THREE_ONES_FRACTION = Fraction.newBuilder().apply { + numerator = 3 + denominator = 1 + }.build() + + private val NEGATIVE_THREE_ONES_FRACTION = Fraction.newBuilder().apply { + isNegative = true + numerator = 3 + denominator = 1 + }.build() + + private val TWO_FRACTION = Fraction.newBuilder().apply { + wholeNumber = 2 + denominator = 1 + }.build() + + private val NEGATIVE_TWO_FRACTION = Fraction.newBuilder().apply { + isNegative = true + wholeNumber = 2 + denominator = 1 + }.build() + } + + @Test + fun testHasFractionalPart_zeroFraction_returnsFalse() { + val result = ZERO_FRACTION.hasFractionalPart() + + assertThat(result).isFalse() + } + + @Test + fun testHasFractionalPart_oneHalf_returnsTrue() { + val result = ONE_HALF_FRACTION.hasFractionalPart() + + assertThat(result).isTrue() + } + + @Test + fun testHasFractionalPart_negativeOneHalf_returnsTrue() { + val result = NEGATIVE_ONE_HALF_FRACTION.hasFractionalPart() + + assertThat(result).isTrue() + } + + @Test + fun testHasFractionalPart_mixedFraction_returnsTrue() { + val result = ONE_AND_ONE_HALF_FRACTION.hasFractionalPart() + + assertThat(result).isTrue() + } + + @Test + fun testHasFractionalPart_improperFraction_returnsTrue() { + val result = THREE_HALVES_FRACTION.hasFractionalPart() + + assertThat(result).isTrue() + } + + @Test + fun testHasFractionalPart_threeOverOne_returnsTrue() { + val result = THREE_ONES_FRACTION.hasFractionalPart() + + assertThat(result).isTrue() + } + + @Test + fun testHasFractionalPart_onlyWholeNumber_returnsFalse() { + val result = TWO_FRACTION.hasFractionalPart() + + assertThat(result).isFalse() + } + + @Test + fun testIsOnlyWholeNumber_zeroFraction_returnsTrue() { + val result = ZERO_FRACTION.isOnlyWholeNumber() + + assertThat(result).isTrue() + } + + @Test + fun testIsOnlyWholeNumber_oneHalf_returnsFalse() { + val result = ONE_HALF_FRACTION.isOnlyWholeNumber() + + assertThat(result).isFalse() + } + + @Test + fun testIsOnlyWholeNumber_negativeOneHalf_returnsFalse() { + val result = NEGATIVE_ONE_HALF_FRACTION.isOnlyWholeNumber() + + assertThat(result).isFalse() + } + + @Test + fun testIsOnlyWholeNumber_mixedFraction_returnsFalse() { + val result = ONE_AND_ONE_HALF_FRACTION.isOnlyWholeNumber() + + assertThat(result).isFalse() + } + + @Test + fun testIsOnlyWholeNumber_improperFraction_returnsFalse() { + val result = THREE_HALVES_FRACTION.isOnlyWholeNumber() + + assertThat(result).isFalse() + } + + @Test + fun testIsOnlyWholeNumber_threeOverOne_returnsFalse() { + val result = THREE_ONES_FRACTION.isOnlyWholeNumber() + + // 3/1 is technically not a whole number since it's still in fractional form. + assertThat(result).isFalse() + } + + @Test + fun testIsOnlyWholeNumber_onlyWholeNumber_returnsTrue() { + val result = TWO_FRACTION.isOnlyWholeNumber() + + assertThat(result).isTrue() + } + + @Test + fun testToDouble_zeroFraction_returnsZero() { + val result = ZERO_FRACTION.toDouble() + + assertThat(result).isWithin(1e-5).of(0.0) + } + + @Test + fun testToDouble_oneHalf_returnsPointFive() { + val result = ONE_HALF_FRACTION.toDouble() + + assertThat(result).isWithin(1e-5).of(0.5) + } + + @Test + fun testToDouble_negativeOneHalf_returnsNegativePointFive() { + val result = NEGATIVE_ONE_HALF_FRACTION.toDouble() + + assertThat(result).isWithin(1e-5).of(-0.5) + } + + @Test + fun testToDouble_one_and_one_half_returnsOnePointFive() { + val result = ONE_AND_ONE_HALF_FRACTION.toDouble() + + assertThat(result).isWithin(1e-5).of(1.5) + } + + @Test + fun testToDouble_threeHalves_returnsOnePointFive() { + val result = THREE_HALVES_FRACTION.toDouble() + + assertThat(result).isWithin(1e-5).of(1.5) + } + + @Test + fun testToDouble_two_returnsTwo() { + val result = TWO_FRACTION.toDouble() + + assertThat(result).isWithin(1e-5).of(2.0) + } + + @Test + fun testToAnswerString_zero_returnsZeroString() { + val result = ZERO_FRACTION.toAnswerString() + + assertThat(result).isEqualTo("0") + } + + @Test + fun testToAnswerString_negativeZero_returnsZeroString() { + val result = NEGATIVE_ZERO_FRACTION.toAnswerString() + + assertThat(result).isEqualTo("0") + } + + @Test + fun testToAnswerString_two_returnsTwoString() { + val result = TWO_FRACTION.toAnswerString() + + assertThat(result).isEqualTo("2") + } + + @Test + fun testToAnswerString_negativeTwo_returnsMinusTwoString() { + val result = NEGATIVE_TWO_FRACTION.toAnswerString() + + assertThat(result).isEqualTo("-2") + } + + @Test + fun testToAnswerString_threeOverOne_returnsThreeString() { + val result = THREE_ONES_FRACTION.toAnswerString() + + assertThat(result).isEqualTo("3") + } + + @Test + fun testToAnswerString_negativeThreeOverOne_returnsMinusThreeString() { + val result = NEGATIVE_THREE_ONES_FRACTION.toAnswerString() + + assertThat(result).isEqualTo("-3") + } + + @Test + fun testToAnswerString_threeOverTwo_returnsThreeHalvesString() { + val result = THREE_HALVES_FRACTION.toAnswerString() + + assertThat(result).isEqualTo("3/2") + } + + @Test + fun testToAnswerString_negativeThreeOverTwo_returnsMinusThreeHalvesString() { + val result = NEGATIVE_THREE_HALVES_FRACTION.toAnswerString() + + assertThat(result).isEqualTo("-3/2") + } + + @Test + fun testToAnswerString_oneAndOneHalf_returnsMixedFractionString() { + val result = ONE_AND_ONE_HALF_FRACTION.toAnswerString() + + assertThat(result).isEqualTo("1 1/2") + } + + @Test + fun testToAnswerString_negativeOneAndOneHalf_returnsMinusMixedFractionString() { + val result = NEGATIVE_ONE_AND_ONE_HALF_FRACTION.toAnswerString() + + assertThat(result).isEqualTo("-1 1/2") + } + + @Test + fun testToSimplestForm_zero_returnsZeroFraction() { + val result = ZERO_FRACTION.toSimplestForm() + + assertThat(result).hasNegativePropertyThat().isFalse() + assertThat(result).hasNumeratorThat().isEqualTo(0) + assertThat(result).hasDenominatorThat().isEqualTo(1) + assertThat(result).hasWholeNumberThat().isEqualTo(0) + } + + @Test + fun testToSimplestForm_two_returnsTwoFraction() { + val result = TWO_FRACTION.toSimplestForm() + + assertThat(result).hasNegativePropertyThat().isFalse() + assertThat(result).hasNumeratorThat().isEqualTo(0) + assertThat(result).hasDenominatorThat().isEqualTo(1) + assertThat(result).hasWholeNumberThat().isEqualTo(2) + } + + @Test + fun testToSimplestForm_oneHalf_returnsOneHalfFraction() { + val result = ONE_HALF_FRACTION.toSimplestForm() + + assertThat(result).hasNegativePropertyThat().isFalse() + assertThat(result).hasNumeratorThat().isEqualTo(1) + assertThat(result).hasDenominatorThat().isEqualTo(2) + assertThat(result).hasWholeNumberThat().isEqualTo(0) + } + + @Test + fun testToSimplestForm_oneAndOneHalf_returnsOneAndOneHalfFraction() { + val result = ONE_AND_ONE_HALF_FRACTION.toSimplestForm() + + assertThat(result).hasNegativePropertyThat().isFalse() + assertThat(result).hasNumeratorThat().isEqualTo(1) + assertThat(result).hasDenominatorThat().isEqualTo(2) + assertThat(result).hasWholeNumberThat().isEqualTo(1) + } + + @Test + fun testToSimplestForm_sixFourths_returnsThreeHalvesFraction() { + val sixHalvesFraction = Fraction.newBuilder().apply { + numerator = 6 + denominator = 4 + }.build() + + val result = sixHalvesFraction.toSimplestForm() + + assertThat(result).hasNegativePropertyThat().isFalse() + assertThat(result).hasNumeratorThat().isEqualTo(3) + assertThat(result).hasDenominatorThat().isEqualTo(2) + assertThat(result).hasWholeNumberThat().isEqualTo(0) + } + + @Test + fun testToSimplestForm_largeNegativeImproperFraction_reducesToSimplestImproperFraction() { + val largeImproperFraction = Fraction.newBuilder().apply { + isNegative = true + numerator = 1650 + denominator = 209 + }.build() + + val result = largeImproperFraction.toSimplestForm() + + assertThat(result).hasNegativePropertyThat().isTrue() + assertThat(result).hasNumeratorThat().isEqualTo(150) + assertThat(result).hasDenominatorThat().isEqualTo(19) + assertThat(result).hasWholeNumberThat().isEqualTo(0) + } + + @Test + fun testToSimplestForm_zeroDenominator_throwsException() { + val zeroDenominatorFraction = Fraction.getDefaultInstance() + + // Converting to simplest form results in a divide by zero in this case. + assertThrows(ArithmeticException::class) { zeroDenominatorFraction.toSimplestForm() } + } + + @Test + fun testToImproperForm_zero_returnsZeroFraction() { + val result = ZERO_FRACTION.toImproperForm() + + assertThat(result).hasNegativePropertyThat().isFalse() + assertThat(result).hasNumeratorThat().isEqualTo(0) + assertThat(result).hasDenominatorThat().isEqualTo(1) + assertThat(result).hasWholeNumberThat().isEqualTo(0) + } + + @Test + fun testToImproperForm_two_returnsTwoOnesFraction() { + val result = TWO_FRACTION.toImproperForm() + + assertThat(result).hasNegativePropertyThat().isFalse() + assertThat(result).hasNumeratorThat().isEqualTo(2) + assertThat(result).hasDenominatorThat().isEqualTo(1) + assertThat(result).hasWholeNumberThat().isEqualTo(0) + } + + @Test + fun testToImproperForm_oneHalf_returnsOneHalfFraction() { + val result = ONE_HALF_FRACTION.toImproperForm() + + assertThat(result).hasNegativePropertyThat().isFalse() + assertThat(result).hasNumeratorThat().isEqualTo(1) + assertThat(result).hasDenominatorThat().isEqualTo(2) + assertThat(result).hasWholeNumberThat().isEqualTo(0) + } + + @Test + fun testToImproperForm_oneAndOneHalf_returnsThreeHalvesFraction() { + val result = ONE_AND_ONE_HALF_FRACTION.toImproperForm() + + assertThat(result).hasNegativePropertyThat().isFalse() + assertThat(result).hasNumeratorThat().isEqualTo(3) + assertThat(result).hasDenominatorThat().isEqualTo(2) + assertThat(result).hasWholeNumberThat().isEqualTo(0) + } + + @Test + fun testToImproperForm_threeHalves_returnsThreeHalvesFraction() { + val result = THREE_HALVES_FRACTION.toImproperForm() + + assertThat(result).hasNegativePropertyThat().isFalse() + assertThat(result).hasNumeratorThat().isEqualTo(3) + assertThat(result).hasDenominatorThat().isEqualTo(2) + assertThat(result).hasWholeNumberThat().isEqualTo(0) + } + + @Test + fun testToImproperForm_negativeOneAndTwoThirds_returnsNegativeFiveThirdsFraction() { + val negativeOneAndTwoThirds = Fraction.newBuilder().apply { + isNegative = true + numerator = 2 + denominator = 3 + wholeNumber = 1 + }.build() + + val result = negativeOneAndTwoThirds.toImproperForm() + + assertThat(result).hasNegativePropertyThat().isTrue() + assertThat(result).hasNumeratorThat().isEqualTo(5) + assertThat(result).hasDenominatorThat().isEqualTo(3) + assertThat(result).hasWholeNumberThat().isEqualTo(0) + } + + @Test + fun testToImproperForm_largeSimpleFormFraction_returnsLargeImproperFraction() { + val negativeOneAndTwoThirds = Fraction.newBuilder().apply { + isNegative = true + numerator = 17 + denominator = 19 + wholeNumber = 7 + }.build() + + val result = negativeOneAndTwoThirds.toImproperForm() + + assertThat(result).hasNegativePropertyThat().isTrue() + assertThat(result).hasNumeratorThat().isEqualTo(150) + assertThat(result).hasDenominatorThat().isEqualTo(19) + assertThat(result).hasWholeNumberThat().isEqualTo(0) + } + + @Test + fun testUnaryMinus_zero_returnsNegativeZeroFraction() { + val result = -ZERO_FRACTION + + assertThat(result).hasNegativePropertyThat().isTrue() + assertThat(result).hasNumeratorThat().isEqualTo(0) + assertThat(result).hasDenominatorThat().isEqualTo(1) + assertThat(result).hasWholeNumberThat().isEqualTo(0) + } + + @Test + fun testUnaryMinus_two_returnsNegativeTwoFraction() { + val result = -TWO_FRACTION + + assertThat(result).hasNegativePropertyThat().isTrue() + assertThat(result).hasNumeratorThat().isEqualTo(0) + assertThat(result).hasDenominatorThat().isEqualTo(1) + assertThat(result).hasWholeNumberThat().isEqualTo(2) + } + + @Test + fun testUnaryMinus_negativeTwo_returnsTwoFraction() { + val result = -NEGATIVE_TWO_FRACTION + + assertThat(result).hasNegativePropertyThat().isFalse() + assertThat(result).hasNumeratorThat().isEqualTo(0) + assertThat(result).hasDenominatorThat().isEqualTo(1) + assertThat(result).hasWholeNumberThat().isEqualTo(2) + } + + @Test + fun testUnaryMinus_oneHalf_returnsNegativeOneHalfFraction() { + val result = -ONE_HALF_FRACTION + + assertThat(result).hasNegativePropertyThat().isTrue() + assertThat(result).hasNumeratorThat().isEqualTo(1) + assertThat(result).hasDenominatorThat().isEqualTo(2) + assertThat(result).hasWholeNumberThat().isEqualTo(0) + } + + @Test + fun testUnaryMinus_negativeOneHalf_returnsOneHalfFraction() { + val result = -NEGATIVE_ONE_HALF_FRACTION + + assertThat(result).hasNegativePropertyThat().isFalse() + assertThat(result).hasNumeratorThat().isEqualTo(1) + assertThat(result).hasDenominatorThat().isEqualTo(2) + assertThat(result).hasWholeNumberThat().isEqualTo(0) + } + + @Test + fun testUnaryMinus_oneAndOneHalf_returnsNegativeOneAndOneHalfFraction() { + val result = -ONE_AND_ONE_HALF_FRACTION + + assertThat(result).hasNegativePropertyThat().isTrue() + assertThat(result).hasNumeratorThat().isEqualTo(1) + assertThat(result).hasDenominatorThat().isEqualTo(2) + assertThat(result).hasWholeNumberThat().isEqualTo(1) + } + + @Test + fun testUnaryMinus_negativeOneAndOneHalf_returnsOneAndOneHalfFraction() { + val result = -NEGATIVE_ONE_AND_ONE_HALF_FRACTION + + assertThat(result).hasNegativePropertyThat().isFalse() + assertThat(result).hasNumeratorThat().isEqualTo(1) + assertThat(result).hasDenominatorThat().isEqualTo(2) + assertThat(result).hasWholeNumberThat().isEqualTo(1) + } +} diff --git a/utility/src/test/java/org/oppia/android/util/math/PolynomialExtensionsTest.kt b/utility/src/test/java/org/oppia/android/util/math/PolynomialExtensionsTest.kt new file mode 100644 index 00000000000..4fac2ae26b0 --- /dev/null +++ b/utility/src/test/java/org/oppia/android/util/math/PolynomialExtensionsTest.kt @@ -0,0 +1,390 @@ +package org.oppia.android.util.math + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.oppia.android.app.model.Fraction +import org.oppia.android.app.model.Polynomial +import org.oppia.android.app.model.Polynomial.Term +import org.oppia.android.app.model.Polynomial.Term.Variable +import org.oppia.android.app.model.Real +import org.oppia.android.testing.math.RealSubject.Companion.assertThat +import org.robolectric.annotation.LooperMode + +/** Tests for [Polynomial] extensions. */ +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") +@RunWith(AndroidJUnit4::class) +@LooperMode(LooperMode.Mode.PAUSED) +class PolynomialExtensionsTest { + private companion object { + private const val PI = 3.1415 + + private val ONE_HALF_FRACTION = Fraction.newBuilder().apply { + numerator = 1 + denominator = 2 + }.build() + + private val ONE_AND_ONE_HALF_FRACTION = Fraction.newBuilder().apply { + numerator = 1 + denominator = 2 + wholeNumber = 1 + }.build() + + private val ZERO_REAL = Real.newBuilder().apply { + integer = 0 + }.build() + + private val ONE_REAL = Real.newBuilder().apply { + integer = 1 + }.build() + + private val TWO_REAL = Real.newBuilder().apply { + integer = 2 + }.build() + + private val ONE_HALF_REAL = Real.newBuilder().apply { + rational = ONE_HALF_FRACTION + }.build() + + private val ONE_AND_ONE_HALF_REAL = Real.newBuilder().apply { + rational = ONE_AND_ONE_HALF_FRACTION + }.build() + + private val PI_REAL = Real.newBuilder().apply { + irrational = PI + }.build() + + private val ZERO_POLYNOMIAL = createPolynomial(createTerm(coefficient = ZERO_REAL)) + + private val TWO_POLYNOMIAL = createPolynomial(createTerm(coefficient = TWO_REAL)) + + private val NEGATIVE_TWO_POLYNOMIAL = createPolynomial(createTerm(coefficient = -TWO_REAL)) + + private val ONE_HALF_POLYNOMIAL = createPolynomial(createTerm(coefficient = ONE_HALF_REAL)) + + private val NEGATIVE_ONE_HALF_POLYNOMIAL = + createPolynomial(createTerm(coefficient = -ONE_HALF_REAL)) + + private val ONE_AND_ONE_HALF_POLYNOMIAL = + createPolynomial(createTerm(coefficient = ONE_AND_ONE_HALF_REAL)) + + private val NEGATIVE_ONE_AND_ONE_HALF_POLYNOMIAL = + createPolynomial(createTerm(coefficient = -ONE_AND_ONE_HALF_REAL)) + + private val PI_POLYNOMIAL = createPolynomial(createTerm(coefficient = PI_REAL)) + + private val NEGATIVE_PI_POLYNOMIAL = createPolynomial(createTerm(coefficient = -PI_REAL)) + + private val ONE_X_POLYNOMIAL = + createPolynomial(createTerm(coefficient = ONE_REAL, createVariable(name = "x", power = 1))) + + private val NEGATIVE_ONE_X_POLYNOMIAL = + createPolynomial(createTerm(coefficient = -ONE_REAL, createVariable(name = "x", power = 1))) + + private val TWO_X_POLYNOMIAL = + createPolynomial(createTerm(coefficient = TWO_REAL, createVariable(name = "x", power = 1))) + + private val ONE_PLUS_X_POLYNOMIAL = + createPolynomial( + createTerm(coefficient = ONE_REAL), + createTerm(coefficient = ONE_REAL, createVariable(name = "x", power = 1)) + ) + } + + @Test + fun testIsConstant_default_returnsFalse() { + val defaultPolynomial = Polynomial.getDefaultInstance() + + val result = defaultPolynomial.isConstant() + + assertThat(result).isFalse() + } + + @Test + fun testIsConstant_zero_returnsTrue() { + val result = ZERO_POLYNOMIAL.isConstant() + + assertThat(result).isTrue() + } + + @Test + fun testIsConstant_two_returnsTrue() { + val result = TWO_POLYNOMIAL.isConstant() + + assertThat(result).isTrue() + } + + @Test + fun testIsConstant_negativeTwo_returnsTrue() { + val result = NEGATIVE_TWO_POLYNOMIAL.isConstant() + + assertThat(result).isTrue() + } + + @Test + fun testIsConstant_oneHalf_returnsTrue() { + val result = ONE_HALF_POLYNOMIAL.isConstant() + + assertThat(result).isTrue() + } + + @Test + fun testIsConstant_negativeOneHalf_returnsTrue() { + val result = NEGATIVE_ONE_HALF_POLYNOMIAL.isConstant() + + assertThat(result).isTrue() + } + + @Test + fun testIsConstant_pi_returnsTrue() { + val result = PI_POLYNOMIAL.isConstant() + + assertThat(result).isTrue() + } + + @Test + fun testIsConstant_negativePi_returnsTrue() { + val result = NEGATIVE_PI_POLYNOMIAL.isConstant() + + assertThat(result).isTrue() + } + + @Test + fun testIsConstant_x_returnsFalse() { + val result = ONE_X_POLYNOMIAL.isConstant() + + assertThat(result).isFalse() + } + + @Test + fun testIsConstant_2x_returnsFalse() { + val result = TWO_X_POLYNOMIAL.isConstant() + + assertThat(result).isFalse() + } + + @Test + fun testIsConstant_one_and_x_returnsFalse() { + val result = ONE_PLUS_X_POLYNOMIAL.isConstant() + + assertThat(result).isFalse() + } + + @Test + fun testIsConstant_one_and_two_returnsFalse() { + val onePlusTwoPolynomial = + createPolynomial(createTerm(coefficient = ONE_REAL), createTerm(coefficient = TWO_REAL)) + + val result = onePlusTwoPolynomial.isConstant() + + // While 1+2 is effectively a constant polynomial, it's not actually simplified and thus isn't + // considered a constant polynomial. + assertThat(result).isFalse() + } + + @Test + fun testGetConstant_zero_returnsZero() { + val result = ZERO_POLYNOMIAL.getConstant() + + assertThat(result).isIntegerThat().isEqualTo(0) + } + + @Test + fun testGetConstant_two_returnsTwo() { + val result = TWO_POLYNOMIAL.getConstant() + + assertThat(result).isIntegerThat().isEqualTo(2) + } + + @Test + fun testGetConstant_negativeTwo_returnsNegativeTwo() { + val result = NEGATIVE_TWO_POLYNOMIAL.getConstant() + + assertThat(result).isIntegerThat().isEqualTo(-2) + } + + @Test + fun testGetConstant_oneHalf_returnsOneHalf() { + val result = ONE_HALF_POLYNOMIAL.getConstant() + + assertThat(result).isRationalThat().evaluatesToDoubleThat().isWithin(1e-5).of(0.5) + } + + @Test + fun testGetConstant_negativeOneHalf_returnsNegativeOneHalf() { + val result = NEGATIVE_ONE_HALF_POLYNOMIAL.getConstant() + + assertThat(result).isRationalThat().evaluatesToDoubleThat().isWithin(1e-5).of(-0.5) + } + + @Test + fun testGetConstant_pi_returnsPi() { + val result = PI_POLYNOMIAL.getConstant() + + assertThat(result).isIrrationalThat().isWithin(1e-5).of(PI) + } + + @Test + fun testGetConstant_negativePi_returnsNegativePi() { + val result = NEGATIVE_PI_POLYNOMIAL.getConstant() + + assertThat(result).isIrrationalThat().isWithin(1e-5).of(-PI) + } + + @Test + fun testToPlainText_zero_returnsZeroString() { + val result = ZERO_POLYNOMIAL.toPlainText() + + assertThat(result).isEqualTo("0") + } + + @Test + fun testToPlainText_two_returnsTwoString() { + val result = TWO_POLYNOMIAL.toPlainText() + + assertThat(result).isEqualTo("2") + } + + @Test + fun testToPlainText_negativeTwo_returnsMinusTwoString() { + val result = NEGATIVE_TWO_POLYNOMIAL.toPlainText() + + assertThat(result).isEqualTo("-2") + } + + @Test + fun testToPlainText_oneAndOneHalf_returnsThreeHalvesString() { + val result = ONE_AND_ONE_HALF_POLYNOMIAL.toPlainText() + + assertThat(result).isEqualTo("3/2") + } + + @Test + fun testToPlainText_negativeOneAndOneHalf_returnsMinusThreeHalvesString() { + val result = NEGATIVE_ONE_AND_ONE_HALF_POLYNOMIAL.toPlainText() + + assertThat(result).isEqualTo("-3/2") + } + + @Test + fun testToPlainText_pi_returnsPiString() { + val result = PI_POLYNOMIAL.toPlainText() + + assertThat(result).isEqualTo("3.1415") + } + + @Test + fun testToPlainText_negativePi_returnsMinusPiString() { + val result = NEGATIVE_PI_POLYNOMIAL.toPlainText() + + assertThat(result).isEqualTo("-3.1415") + } + + @Test + fun testToPlainText_2x_returns2XString() { + val result = TWO_X_POLYNOMIAL.toPlainText() + + assertThat(result).isEqualTo("2x") + } + + @Test + fun testToPlainText_1x_returnsXString() { + val result = ONE_X_POLYNOMIAL.toPlainText() + + assertThat(result).isEqualTo("x") + } + + @Test + fun testToPlainText_negativeX_returnsMinusXString() { + val result = NEGATIVE_ONE_X_POLYNOMIAL.toPlainText() + + assertThat(result).isEqualTo("-x") + } + + @Test + fun testToPlainText_oneAndX_returnsOnePlusXString() { + val result = ONE_PLUS_X_POLYNOMIAL.toPlainText() + + assertThat(result).isEqualTo("1 + x") + } + + @Test + fun testToPlainText_oneAndNegativeX_returnsOneMinusXString() { + val oneMinusXPolynomial = createPolynomial( + createTerm(coefficient = ONE_REAL), + createTerm(coefficient = -ONE_REAL, createVariable(name = "x", power = 1)) + ) + + val result = oneMinusXPolynomial.toPlainText() + + assertThat(result).isEqualTo("1 - x") + } + + @Test + fun testToPlainText_oneAndOneHalfXAndY_returnsThreeHalvesXPlusYString() { + val oneMinusXPolynomial = createPolynomial( + createTerm(coefficient = ONE_AND_ONE_HALF_REAL, createVariable(name = "x", power = 1)), + createTerm(coefficient = ONE_REAL, createVariable(name = "y", power = 1)) + ) + + val result = oneMinusXPolynomial.toPlainText() + + assertThat(result).isEqualTo("(3/2)x + y") + } + + @Test + fun testToPlainText_oneAndXAndXSquared_returnsOnePlusXPlusXSquaredString() { + val oneMinusXPolynomial = createPolynomial( + createTerm(coefficient = ONE_REAL), + createTerm(coefficient = ONE_REAL, createVariable(name = "x", power = 1)), + createTerm(coefficient = ONE_REAL, createVariable(name = "x", power = 2)) + ) + + val result = oneMinusXPolynomial.toPlainText() + + assertThat(result).isEqualTo("1 + x + x^2") + } + + @Test + fun testToPlainText_xSquaredAndXAndOne_returnsXSquaredPlusXPlusOneString() { + val oneMinusXPolynomial = createPolynomial( + createTerm(coefficient = ONE_REAL, createVariable(name = "x", power = 2)), + createTerm(coefficient = ONE_REAL, createVariable(name = "x", power = 1)), + createTerm(coefficient = ONE_REAL) + ) + + val result = oneMinusXPolynomial.toPlainText() + + // Compared with the test above, this shows that term order matters for string conversion. + assertThat(result).isEqualTo("x^2 + x + 1") + } + + @Test + fun testToPlainText_xSquaredYCubedAndOne_returnsXSquaredYCubedPlusOneString() { + val oneMinusXPolynomial = createPolynomial( + createTerm(coefficient = ONE_REAL, createVariable(name = "x", power = 2)), + createTerm(coefficient = ONE_REAL, createVariable(name = "y", power = 3)), + createTerm(coefficient = ONE_REAL) + ) + + val result = oneMinusXPolynomial.toPlainText() + + assertThat(result).isEqualTo("x^2 + y^3 + 1") + } +} + +private fun createVariable(name: String, power: Int) = Variable.newBuilder().apply { + this.name = name + this.power = power +}.build() + +private fun createTerm(coefficient: Real, vararg variables: Variable) = Term.newBuilder().apply { + this.coefficient = coefficient + addAllVariable(variables.toList()) +}.build() + +private fun createPolynomial(vararg terms: Term) = Polynomial.newBuilder().apply { + addAllTerm(terms.toList()) +}.build() diff --git a/utility/src/test/java/org/oppia/android/util/math/RatioExtensionsTest.kt b/utility/src/test/java/org/oppia/android/util/math/RatioExtensionsTest.kt index ca380220b0b..2b9bea5df06 100644 --- a/utility/src/test/java/org/oppia/android/util/math/RatioExtensionsTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/RatioExtensionsTest.kt @@ -7,7 +7,9 @@ import org.junit.runner.RunWith import org.oppia.android.app.model.RatioExpression import org.robolectric.annotation.LooperMode -/** Tests for [RatioExtensions]. */ +/** Tests for [RatioExpression] extensions. */ +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) class RatioExtensionsTest { diff --git a/utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt b/utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt new file mode 100644 index 00000000000..2e13da959aa --- /dev/null +++ b/utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt @@ -0,0 +1,414 @@ +package org.oppia.android.util.math + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.oppia.android.app.model.Fraction +import org.oppia.android.app.model.Real +import org.oppia.android.testing.assertThrows +import org.oppia.android.testing.math.RealSubject.Companion.assertThat +import org.robolectric.annotation.LooperMode + +/** Tests for [Real] extensions. */ +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") +@RunWith(AndroidJUnit4::class) +@LooperMode(LooperMode.Mode.PAUSED) +class RealExtensionsTest { + private companion object { + private const val PI = 3.1415 + + private val ONE_HALF_FRACTION = Fraction.newBuilder().apply { + numerator = 1 + denominator = 2 + }.build() + + private val ONE_AND_ONE_HALF_FRACTION = Fraction.newBuilder().apply { + numerator = 1 + denominator = 2 + wholeNumber = 1 + }.build() + + private val ZERO_REAL = createIntegerReal(0) + private val TWO_REAL = createIntegerReal(2) + private val NEGATIVE_TWO_REAL = createIntegerReal(-2) + + private val ONE_HALF_REAL = createRationalReal(ONE_HALF_FRACTION) + private val NEGATIVE_ONE_HALF_REAL = createRationalReal(-ONE_HALF_FRACTION) + private val ONE_AND_ONE_HALF_REAL = createRationalReal(ONE_AND_ONE_HALF_FRACTION) + private val NEGATIVE_ONE_AND_ONE_HALF_REAL = createRationalReal(-ONE_AND_ONE_HALF_FRACTION) + + private val PI_REAL = createIrrationalReal(PI) + private val NEGATIVE_PI_REAL = createIrrationalReal(-PI) + } + + @Test + fun testIsRational_default_returnsFalse() { + val defaultReal = Real.getDefaultInstance() + + val result = defaultReal.isRational() + + assertThat(result).isFalse() + } + + @Test + fun testIsRational_twoInteger_returnsFalse() { + val result = TWO_REAL.isRational() + + assertThat(result).isFalse() + } + + @Test + fun testIsRational_oneHalfFraction_returnsTrue() { + val result = ONE_HALF_REAL.isRational() + + assertThat(result).isTrue() + } + + @Test + fun testIsRational_piIrrational_returnsFalse() { + val result = PI_REAL.isRational() + + assertThat(result).isFalse() + } + + @Test + fun testIsNegative_default_throwsException() { + val defaultReal = Real.getDefaultInstance() + + val exception = assertThrows(IllegalStateException::class) { defaultReal.isNegative() } + + assertThat(exception).hasMessageThat().contains("Invalid real") + } + + @Test + fun testIsNegative_twoInteger_returnsFalse() { + val result = TWO_REAL.isNegative() + + assertThat(result).isFalse() + } + + @Test + fun testIsNegative_negativeTwoInteger_returnsTrue() { + val result = NEGATIVE_TWO_REAL.isNegative() + + assertThat(result).isTrue() + } + + @Test + fun testIsNegative_oneHalfFraction_returnsFalse() { + val result = ONE_HALF_REAL.isNegative() + + assertThat(result).isFalse() + } + + @Test + fun testIsNegative_negativeOneHalfFraction_returnsTrue() { + val result = NEGATIVE_ONE_HALF_REAL.isNegative() + + assertThat(result).isTrue() + } + + @Test + fun testIsNegative_piIrrational_returnsFalse() { + val result = PI_REAL.isNegative() + + assertThat(result).isFalse() + } + + @Test + fun testIsNegative_negativePiIrrational_returnsTrue() { + val result = NEGATIVE_PI_REAL.isNegative() + + assertThat(result).isTrue() + } + + @Test + fun testToDouble_default_returnsZeroDouble() { + val defaultReal = Real.getDefaultInstance() + + val exception = assertThrows(IllegalStateException::class) { defaultReal.toDouble() } + + assertThat(exception).hasMessageThat().contains("Invalid real") + } + + @Test + fun testToDouble_twoInteger_returnsTwoDouble() { + val result = TWO_REAL.toDouble() + + assertThat(result).isWithin(1e-5).of(2.0) + } + + @Test + fun testToDouble_negativeTwoInteger_returnsNegativeTwoDouble() { + val result = NEGATIVE_TWO_REAL.toDouble() + + assertThat(result).isWithin(1e-5).of(-2.0) + } + + @Test + fun testToDouble_oneHalfFraction_returnsPointFive() { + val result = ONE_HALF_REAL.toDouble() + + assertThat(result).isWithin(1e-5).of(0.5) + } + + @Test + fun testToDouble_negativeOneHalfFraction_returnsNegativePointFive() { + val result = NEGATIVE_ONE_HALF_REAL.toDouble() + + assertThat(result).isWithin(1e-5).of(-0.5) + } + + @Test + fun testToDouble_piIrrational_returnsPi() { + val result = PI_REAL.toDouble() + + assertThat(result).isWithin(1e-5).of(PI) + } + + @Test + fun testToDouble_negativePiIrrational_returnsNegativePi() { + val result = NEGATIVE_PI_REAL.toDouble() + + assertThat(result).isWithin(1e-5).of(-PI) + } + + @Test + fun testToPlainText_default_returnsEmptyString() { + val defaultReal = Real.getDefaultInstance() + + val result = defaultReal.toPlainText() + + assertThat(result).isEmpty() + } + + @Test + fun testToPlainText_twoInteger_returnsTwoString() { + val result = TWO_REAL.toPlainText() + + assertThat(result).isEqualTo("2") + } + + @Test + fun testToPlainText_negativeTwoInteger_returnsMinusTwoString() { + val result = NEGATIVE_TWO_REAL.toPlainText() + + assertThat(result).isEqualTo("-2") + } + + @Test + fun testToPlainText_oneHalfFraction_returnsOneHalfString() { + val result = ONE_HALF_REAL.toPlainText() + + assertThat(result).isEqualTo("1/2") + } + + @Test + fun testToPlainText_negativeOneHalfFraction_returnsMinusOneHalfString() { + val result = NEGATIVE_ONE_HALF_REAL.toPlainText() + + assertThat(result).isEqualTo("-1/2") + } + + @Test + fun testToPlainText_oneAndOneHalfFraction_returnsThreeHalvesString() { + val result = ONE_AND_ONE_HALF_REAL.toPlainText() + + assertThat(result).isEqualTo("3/2") + } + + @Test + fun testToPlainText_negativeOneAndOneHalfFraction_returnsMinusThreeHalvesString() { + val result = NEGATIVE_ONE_AND_ONE_HALF_REAL.toPlainText() + + assertThat(result).isEqualTo("-3/2") + } + + @Test + fun testToPlainText_piIrrational_returnsPiString() { + val result = PI_REAL.toPlainText() + + assertThat(result).isEqualTo("3.1415") + } + + @Test + fun testToPlainText_negativePiIrrational_returnsMinusPiString() { + val result = NEGATIVE_PI_REAL.toPlainText() + + assertThat(result).isEqualTo("-3.1415") + } + + @Test + fun testIsApproximatelyEqualTo_zeroIntegerAndZero_returnsTrue() { + val result = ZERO_REAL.isApproximatelyEqualTo(0.0) + + assertThat(result).isTrue() + } + + @Test + fun testIsApproximatelyEqualTo_zeroAndOne_returnsFalse() { + val result = ZERO_REAL.isApproximatelyEqualTo(1.0) + + assertThat(result).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_twoAndTwoWithinThreshold_returnsTrue() { + val result = TWO_REAL.isApproximatelyEqualTo(2.0 + FLOAT_EQUALITY_INTERVAL / 2.0) + + assertThat(result).isTrue() + } + + @Test + fun testIsApproximatelyEqualTo_twoAndTwoOutsideThreshold_returnsFalse() { + val result = TWO_REAL.isApproximatelyEqualTo(2.0 + FLOAT_EQUALITY_INTERVAL * 2.0) + + assertThat(result).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_oneHalfAndOne_returnsFalse() { + val result = ONE_HALF_REAL.isApproximatelyEqualTo(1.0) + + assertThat(result).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_oneHalfAndPointFive_returnsTrue() { + val result = ONE_HALF_REAL.isApproximatelyEqualTo(0.5) + + assertThat(result).isTrue() + } + + @Test + fun testIsApproximatelyEqualTo_oneHalfAndPointSix_returnsFalse() { + val result = ONE_HALF_REAL.isApproximatelyEqualTo(0.6) + + assertThat(result).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_pointFiveAndPointFive_returnsTrue() { + val pointFiveReal = createIrrationalReal(0.5) + + val result = pointFiveReal.isApproximatelyEqualTo(0.5) + + assertThat(result).isTrue() + } + + @Test + fun testIsApproximatelyEqualTo_pointFiveAndPointSix_returnsFalse() { + val pointFiveReal = createIrrationalReal(0.5) + + val result = pointFiveReal.isApproximatelyEqualTo(0.6) + + assertThat(result).isFalse() + } + + @Test + fun testUnaryMinus_default_throwsException() { + val defaultReal = Real.getDefaultInstance() + + val exception = assertThrows(IllegalStateException::class) { -defaultReal } + + assertThat(exception).hasMessageThat().contains("Invalid real") + } + + @Test + fun testUnaryMinus_twoInteger_returnsNegativeTwoInteger() { + val result = -TWO_REAL + + assertThat(result).isIntegerThat().isEqualTo(-2) + } + + @Test + fun testUnaryMinus_negativeTwoInteger_returnsTwoInteger() { + val result = -NEGATIVE_TWO_REAL + + assertThat(result).isIntegerThat().isEqualTo(2) + } + + @Test + fun testUnaryMinus_twoOneHalf_returnsNegativeOneHalf() { + val result = -ONE_HALF_REAL + + assertThat(result).isRationalThat().evaluatesToDoubleThat().isWithin(1e-5).of(-0.5) + } + + @Test + fun testUnaryMinus_negativeOneHalf_returnsOneHalf() { + val result = -NEGATIVE_ONE_HALF_REAL + + assertThat(result).isRationalThat().evaluatesToDoubleThat().isWithin(1e-5).of(0.5) + } + + @Test + fun testUnaryMinus_pi_returnsNegativePi() { + val result = -PI_REAL + + assertThat(result).isIrrationalThat().isWithin(1e-5).of(-PI) + } + + @Test + fun testUnaryMinus_negativePi_returnsPi() { + val result = -NEGATIVE_PI_REAL + + assertThat(result).isIrrationalThat().isWithin(1e-5).of(PI) + } + + @Test + fun testAbs_twoInteger_returnsTwoInteger() { + val result = abs(TWO_REAL) + + assertThat(result).isIntegerThat().isEqualTo(2) + } + + @Test + fun testAbs_negativeTwoInteger_returnsTwoInteger() { + val result = abs(NEGATIVE_TWO_REAL) + + assertThat(result).isIntegerThat().isEqualTo(2) + } + + @Test + fun testAbs_oneHalf_returnsOneHalf() { + val result = abs(ONE_HALF_REAL) + + assertThat(result).isRationalThat().evaluatesToDoubleThat().isWithin(1e-5).of(0.5) + } + + @Test + fun testAbs_negativeOneHalf_returnsOneHalf() { + val result = abs(NEGATIVE_ONE_HALF_REAL) + + assertThat(result).isRationalThat().evaluatesToDoubleThat().isWithin(1e-5).of(0.5) + } + + @Test + fun testAbs_pi_returnsPi() { + val result = abs(PI_REAL) + + assertThat(result).isIrrationalThat().isWithin(1e-5).of(PI) + } + + @Test + fun testAbs_negativePi_returnsPi() { + val result = abs(NEGATIVE_PI_REAL) + + assertThat(result).isIrrationalThat().isWithin(1e-5).of(PI) + } +} + +private fun createIntegerReal(value: Int) = Real.newBuilder().apply { + integer = value +}.build() + +private fun createRationalReal(value: Fraction) = Real.newBuilder().apply { + rational = value +}.build() + +private fun createIrrationalReal(value: Double) = Real.newBuilder().apply { + irrational = value +}.build() From fca0cd9ae3d5904812ed215d6816113f6888b06b Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 20 Jan 2022 22:22:44 -0800 Subject: [PATCH 049/162] Add parameterized test runner. This commit introduces a new parameterized test runner that allows proper combinations of parameterized & non-parameterized tests in the same suite, and in a way that should work on both Robolectric & Espresso (though the latter isn't currently verified). Further, this commit also introduces a TokenSubject that will be used more explicitly by the follow-up commit for verifying MathTokenizer. --- scripts/assets/test_file_exemptions.textproto | 8 + .../oppia/android/testing/junit/BUILD.bazel | 58 ++++ .../junit/OppiaParameterizedTestRunner.kt | 302 ++++++++++++++++++ .../android/testing/junit/ParameterValue.kt | 143 +++++++++ .../ParameterizedAndroidJUnit4ClassRunner.kt | 36 +++ .../testing/junit/ParameterizedMethod.kt | 44 +++ .../ParameterizedRobolectricTestRunner.kt | 49 +++ .../junit/ParameterizedRunnerDelegate.kt | 67 ++++ .../ParameterizedRunnerOverrideMethods.kt | 18 ++ .../oppia/android/testing/math/BUILD.bazel | 15 + .../android/testing/math/TokenSubject.kt | 173 ++++++++++ 11 files changed, 913 insertions(+) create mode 100644 testing/src/main/java/org/oppia/android/testing/junit/OppiaParameterizedTestRunner.kt create mode 100644 testing/src/main/java/org/oppia/android/testing/junit/ParameterValue.kt create mode 100644 testing/src/main/java/org/oppia/android/testing/junit/ParameterizedAndroidJUnit4ClassRunner.kt create mode 100644 testing/src/main/java/org/oppia/android/testing/junit/ParameterizedMethod.kt create mode 100644 testing/src/main/java/org/oppia/android/testing/junit/ParameterizedRobolectricTestRunner.kt create mode 100644 testing/src/main/java/org/oppia/android/testing/junit/ParameterizedRunnerDelegate.kt create mode 100644 testing/src/main/java/org/oppia/android/testing/junit/ParameterizedRunnerOverrideMethods.kt create mode 100644 testing/src/main/java/org/oppia/android/testing/math/TokenSubject.kt diff --git a/scripts/assets/test_file_exemptions.textproto b/scripts/assets/test_file_exemptions.textproto index ae6a4f367b2..e7b076af277 100644 --- a/scripts/assets/test_file_exemptions.textproto +++ b/scripts/assets/test_file_exemptions.textproto @@ -646,12 +646,20 @@ exempted_file_path: "testing/src/main/java/org/oppia/android/testing/espresso/Ge exempted_file_path: "testing/src/main/java/org/oppia/android/testing/espresso/ImageViewMatcher.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/espresso/KonfettiViewMatcher.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/junit/DefineAppLanguageLocaleContext.kt" +exempted_file_path: "testing/src/main/java/org/oppia/android/testing/junit/OppiaParameterizedTestRunner.kt" +exempted_file_path: "testing/src/main/java/org/oppia/android/testing/junit/ParameterizedAndroidJUnit4ClassRunner.kt" +exempted_file_path: "testing/src/main/java/org/oppia/android/testing/junit/ParameterizedMethod.kt" +exempted_file_path: "testing/src/main/java/org/oppia/android/testing/junit/ParameterizedRobolectricTestRunner.kt" +exempted_file_path: "testing/src/main/java/org/oppia/android/testing/junit/ParameterizedRunnerDelegate.kt" +exempted_file_path: "testing/src/main/java/org/oppia/android/testing/junit/ParameterizedRunnerOverrideMethods.kt" +exempted_file_path: "testing/src/main/java/org/oppia/android/testing/junit/ParameterValue.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/ComparableOperationListSubject.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/FractionSubject.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/RealSubject.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/PolynomialSubject.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/MathEquationSubject.kt" +exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/TokenSubject.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/mockito/MockitoKotlinHelper.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/network/ApiMockLoader.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/network/MockClassroomService.kt" diff --git a/testing/src/main/java/org/oppia/android/testing/junit/BUILD.bazel b/testing/src/main/java/org/oppia/android/testing/junit/BUILD.bazel index e8d62702d99..d7e6d99dd25 100644 --- a/testing/src/main/java/org/oppia/android/testing/junit/BUILD.bazel +++ b/testing/src/main/java/org/oppia/android/testing/junit/BUILD.bazel @@ -30,3 +30,61 @@ kt_android_library( "//third_party:junit_junit", ], ) + +kt_android_library( + name = "oppia_parameterized_test_runner", + testonly = True, + srcs = [ + "OppiaParameterizedTestRunner.kt", + ], + visibility = ["//:oppia_testing_visibility"], + deps = [ + ":parameterized_runner_delegate_impl", + "//third_party:androidx_test_runner", + "//third_party:junit_junit", + "//third_party:org_robolectric_robolectric", + "//third_party:robolectric_android-all", + ], +) + +kt_android_library( + name = "parameterized_android_junit4_class_runner", + testonly = True, + srcs = [ + "ParameterizedAndroidJUnit4ClassRunner.kt", + ], + visibility = ["//:oppia_testing_visibility"], + deps = [ + ":parameterized_runner_delegate_impl", + "//third_party:androidx_test_runner", + "//third_party:junit_junit", + ], +) + +kt_android_library( + name = "parameterized_robolectric_test_runner", + testonly = True, + srcs = [ + "ParameterizedRobolectricTestRunner.kt", + ], + visibility = ["//:oppia_testing_visibility"], + deps = [ + ":parameterized_runner_delegate_impl", + "//third_party:junit_junit", + "//third_party:org_robolectric_robolectric", + ], +) + +kt_android_library( + name = "parameterized_runner_delegate_impl", + testonly = True, + srcs = [ + "ParameterValue.kt", + "ParameterizedMethod.kt", + "ParameterizedRunnerDelegate.kt", + "ParameterizedRunnerOverrideMethods.kt", + ], + deps = [ + "//third_party:junit_junit", + ], +) diff --git a/testing/src/main/java/org/oppia/android/testing/junit/OppiaParameterizedTestRunner.kt b/testing/src/main/java/org/oppia/android/testing/junit/OppiaParameterizedTestRunner.kt new file mode 100644 index 00000000000..3579bc95d5b --- /dev/null +++ b/testing/src/main/java/org/oppia/android/testing/junit/OppiaParameterizedTestRunner.kt @@ -0,0 +1,302 @@ +package org.oppia.android.testing.junit + +import java.lang.annotation.Repeatable +import java.lang.reflect.Field +import java.lang.reflect.Method +import org.junit.runner.Description +import org.junit.runner.Runner +import org.junit.runner.manipulation.Filter +import org.junit.runner.manipulation.Filterable +import org.junit.runner.manipulation.Sortable +import org.junit.runner.manipulation.Sorter +import org.junit.runner.notification.RunNotifier +import org.junit.runners.Suite + +/** + * JUnit test runner that enables support for parameterization, that is, running a single test + * multiple times with different data values. + * + * From a testing correctness perspective, this should only be used to test scenarios of behaviors + * that are very similar to one another (i.e. only differ in one or two conditions that can be + * data-driven), and that have a large number (i.e. >5) conditions to test. For other situations, + * use regular explicit tests, instead (since parameterized tests can hurt test maintainability and + * readability). + * + * This runner behaves like AndroidJUnit4 in that it should work both locally (i.e. via Robolectric) + * and on a device (i.e. with Espresso), though the correct Bazel dependency needs to be added based + * on the environment in which the test is running. + * + * To introduce parameterized tests, add this runner along with one or more [Parameter]-annotated + * fields and one or more [RunParameterized]-annotated methods (where each method should have + * multiple [Iteration]s defined to describe each test iteration). Here's a simple example: + * + * ```kotlin + * @RunWith(OppiaParameterizedTestRunner::class) + * class ExampleParameterizedTest { + * @Parameter lateinit var parameter: String + * + * @Test + * @RunParameterized( + * Iteration("first", "parameter=first value"), + * Iteration("second", "parameter=second value"), + * Iteration("third", "parameter=third value") + * ) + * fun testParams_multipleVals_isConsistent() { + * val result = performOperation(parameter) + * assertThat(result).isEqualTo(consistentExpectedValue) + * } + * } + * ``` + * + * The test testParams_multipleVals_isConsistent will be run three times, and before each time the + * specified parameter value corresponding to each iteration will be injected into the parameter + * field for use by the test. + * + * Note that with Bazel specific iterations can be run by utilizing the test and iteration name, + * e.g.: + * + * ```bash + * bazel run //...:ExampleParameterizedTest --test_filter=testParams_multipleVals_isConsistent-first + * ``` + * + * Or, all of the iterations for that test can be run: + * + * ```bash + * bazel run //...:ExampleParameterizedTest --test_filter=testParams_multipleVals_isConsistent + * ``` + * + * Finally, regular tests can be added by simply using the JUnit ``Test`` annotation without also + * annotating with [RunParameterized]. Such tests should not ever read from the + * [Parameter]-annotated fields since there's no way to ensure what values those fields will + * contain (thus they should be treated as undefined outside of tests that specific define their + * value via [Iteration]). + */ +class OppiaParameterizedTestRunner(private val testClass: Class<*>): Suite(testClass, listOf()) { + private val parameterizedMethods = computeParameterizedMethods() + private val childrenRunners by lazy { + // Collect all parameterized methods (for each iteration they support) plus one test runner for + // all non-parameterized methods. + parameterizedMethods.flatMap { (methodName, method) -> + method.iterationNames.map { iterationName -> + ProxyParameterizedTestRunner(testClass, parameterizedMethods, methodName, iterationName) + } + } + ProxyParameterizedTestRunner(testClass, parameterizedMethods, methodName = null) + } + + override fun getChildren(): MutableList = childrenRunners.toMutableList() + + private fun computeParameterizedMethods(): Map { + val fieldsAndParsers = fetchParameterizedFields().map { field -> + val valueParser = ParameterValue.createParserForField(field) + checkNotNull(valueParser) { + "Unsupported field type: ${field.type} for parameterized field ${field.name}" + } + return@map field to valueParser + }.associateBy { (field, _) -> field.name } + + val fields = fieldsAndParsers.map { (_, fieldAndParser) -> fieldAndParser.first } + val methodDeclarations = fetchParameterizedMethodDeclarations() + return methodDeclarations.map { (method, rawValues) -> + val allValues = rawValues.mapValues { (_, values) -> + values.map { rawValuePair -> + check('=' in rawValuePair) { + "Expect all parameter values to be of the form propertyField=value (encountered:" + + " $rawValuePair)" + } + + val (fieldName, rawValue) = rawValuePair.split('=') + check(fieldName in fieldsAndParsers) { + "Property key does not correspond to any class fields: $fieldName (available:" + + " ${fieldsAndParsers.keys})" + } + + val (field, parser) = fieldsAndParsers.getValue(fieldName) + val value = parser.parseParameter(fieldName, rawValue) + checkNotNull(value) { + "Parameterized field ${field.name}'s type is incompatible with raw parameter value:" + + " $rawValue" + } + } + }.also { allValues -> + // Validate no duplicate keys. + allValues.forEach { (iterationName, values) -> + val allKeys = values.map { it.key } + val uniqueKeys = allKeys.toSet() + check(allKeys.size == uniqueKeys.size) { + val duplicateKeys = allKeys.toMutableList() + uniqueKeys.forEach { duplicateKeys.remove(it) } + return@check "Encountered duplicate keys in iteration $iterationName for method" + + " ${method.name}: ${duplicateKeys.toSet()}" + } + } + + // Validate key consistency. + val allKeys = allValues.values.flatten().map(ParameterValue::key).toSet() + allValues.forEach { (iterationName, values) -> + val iterationKeys = values.map { it.key }.toSet() + check(iterationKeys == allKeys) { + "Iteration $iterationName in method ${method.name} has missing keys compared with" + + " other iterations: ${allKeys - iterationKeys}" + } + } + + // Validate value ordering. + val iterationKeys = allValues.mapValues { (_, values) -> values.map { it.key } } + val expectedOrder = iterationKeys.values.first() + iterationKeys.forEach { (iterationName, keys) -> + check(keys == expectedOrder) { + "Iteration $iterationName in method ${method.name} lists its keys in the order: $keys" + + " whereas $expectedOrder (for the first iteration) is expected for consistency." + + " Please pick an order and ensure all iterations are consistent." + } + } + + // Validate that all value sets are unique (to detect redundant iterations). + allValues.entries.forEach { (outerIterationName, outerValues) -> + allValues.entries.forEach { (innerIterationName, innerValues) -> + if (outerIterationName != innerIterationName) { + // Order & counts have been verified above, so the values can be checked in order. + val differentValues = outerValues.zip(innerValues).any { (outerValue, innerValue) -> + outerValue.value != innerValue.value + } + check(differentValues) { + "Iterations $outerIterationName and $innerIterationName in method ${method.name}" + + " have the same values and are thus redundant. Please remove one of them or" + + " update the values." + } + } + } + } + } + return@map ParameterizedMethod(method.name, allValues, fields) + }.associateBy { it.methodName } + } + + private fun fetchParameterizedFields(): List { + return testClass.declaredFields.mapNotNull { field -> + field.getDeclaredAnnotation(Parameter::class.java)?.let { field } + } + } + + private fun fetchParameterizedMethodDeclarations(): List { + return testClass.declaredMethods.mapNotNull { method -> + method.getDeclaredAnnotationsByType(Iteration::class.java).map { parameters -> + parameters.name to parameters.keyValuePairs.toList() + }.takeIf { it.isNotEmpty() }?.let { rawValues -> + val groupedValues = rawValues.groupBy({ it.first }, { it.second }) + // Verify there are no duplicate iteration names. + groupedValues.forEach { (iterationName, iterations) -> + check(iterations.size == 1) { + "Encountered duplicate iteration name: $iterationName in method ${method.name}" + } + } + val mappedValues = groupedValues.mapValues { (_, iterations) -> iterations.first() } + ParameterizedMethodDeclaration(method, mappedValues) + } + } + } + + /** + * Defines a parameter that may have an injected value that comes from per-test [Iteration] + * definitions. + * + * It's recommended to make such fields 'lateinit var', and they must be public. + * + * The type of the parameter field will define how [Iteration]-defined values are parsed. The + * current list of supported types: + * - [String]s + * - [Boolean]s + * - [Int]s + * - [Long]s + * - [Float]s + * - [Double]s + */ + @Target(AnnotationTarget.FIELD) annotation class Parameter + + /** + * Specifies that a method in a test that uses a [OppiaParameterizedTestRunner] runner should be + * run multiple times for each [Iteration] specified in the [value] iterations list. + * + * See the KDoc for the runner for example code. + */ + @Target(AnnotationTarget.FUNCTION) annotation class RunParameterized(vararg val value: Iteration) + + // TODO(#4120): Migrate to Kotlin @Repeatable once Kotlin 1.6 is used (see: + // https://youtrack.jetbrains.com/issue/KT-12794). + /** + * Defines an iteration to run as part of a [RunParameterized]-annotated test method. + * + * See the KDoc for the runner for example code. + * + * @property name the name of the iteration (this should be short, but meaningful since it's + * appended to the test name) + * @property keyValuePairs an array of strings of the form "key=value" where 'key' is the name of + * a [Parameter]-annotated field and 'value' is a stringified conforming value based on the + * type of that field (incompatible values will result in test failures) + */ + @Repeatable(RunParameterized::class) + @Target(AnnotationTarget.FUNCTION) + annotation class Iteration(val name: String, vararg val keyValuePairs: String) + + private data class ParameterizedMethodDeclaration( + val method: Method, val rawValues: Map> + ) + + private class ProxyParameterizedTestRunner( + private val testClass: Class<*>, + private val parameterizedMethods: Map, + private val methodName: String?, + private val iterationName: String? = null + ): Runner(), Filterable, Sortable { + private val delegate by lazy { constructDelegate() } + private val delegateRunner by lazy { + checkNotNull(delegate as? Runner) { "Delegate runner isn't a JUnit runner: $delegate" } + } + private val delegateFilter by lazy { + checkNotNull(delegate as? Filterable) { "Delegate runner isn't filterable: $delegate" } + } + private val delegateSortable by lazy { + checkNotNull(delegate as? Sortable) { "Delegate runner isn't sortable: $delegate" } + } + + override fun getDescription(): Description = delegateRunner.description + + override fun run(notifier: RunNotifier?) = delegateRunner.run(notifier) + + override fun filter(filter: Filter?) = delegateFilter.filter(filter) + + override fun sort(sorter: Sorter?) = delegateSortable.sort(sorter) + + private fun constructDelegate(): Any { + System.getProperty("android.junit.runner").also { customRunner -> + check(customRunner == null) { + "Detected a custom runner ($customRunner) in a parameterized test. This isn't yet" + + " supported." + } + } + val runningOnAndroid = + System.getProperty("java.runtime.name")?.contains("android", ignoreCase = true) ?: false + + // Load the runner class using reflection since the Robolectric implementation relies on + // Robolectric (which can't be pulled into Espresso builds of shared tests). + val runnerClass = try { + if (runningOnAndroid) { + Class.forName("org.oppia.android.testing.junit.ParameterizedAndroidJUnit4ClassRunner") + } else Class.forName("org.oppia.android.testing.junit.ParameterizedRobolectricTestRunner") + } catch (e: Exception) { + throw IllegalStateException( + "Failed to load delegate test runner class. Did you forget to add either" + + " parameterized_android_junit4_class_runner or parameterized_robolectric_test_runner" + + " as a dependency?", + e + ) + } + + val constructor = + runnerClass.getConstructor( + Class::class.java, Map::class.java, String::class.java, String::class.java + ) + return constructor.newInstance(testClass, parameterizedMethods, methodName, iterationName) + } + } +} diff --git a/testing/src/main/java/org/oppia/android/testing/junit/ParameterValue.kt b/testing/src/main/java/org/oppia/android/testing/junit/ParameterValue.kt new file mode 100644 index 00000000000..360a44649e0 --- /dev/null +++ b/testing/src/main/java/org/oppia/android/testing/junit/ParameterValue.kt @@ -0,0 +1,143 @@ +package org.oppia.android.testing.junit + +import java.lang.reflect.Field + +/** + * Represents a single parameterized value for one parameterized field defined by a single test + * method iteration. + * + * @property key the name of the field to which this value is associated + * @property value the type-correct value to assign to the field prior to executing the iteration + * corresponding to this value + */ +internal sealed class ParameterValue(val key: String, val value: Any) { + private class BooleanParameterValue private constructor( + key: String, value: Boolean + ): ParameterValue(key, value) { + companion object { + /** + * Returns a new [ParameterValue] for the specified [key] and a [Boolean] parsed + * representation of [rawValue], or null if the value isn't a valid stringified [Boolean]. + */ + internal fun createParameter(key: String, rawValue: String): ParameterValue? { + return rawValue.toBooleanStrictOrNull()?.let { BooleanParameterValue(key, it) } + } + + // This can be replaced with Kotlin's version once the codebase uses 1.5+. + private fun String.toBooleanStrictOrNull(): Boolean? { + return when (this) { + "true" -> true + "false" -> false + else -> null + } + } + } + } + + private class IntParameterValue private constructor( + key: String, value: Int + ): ParameterValue(key, value) { + companion object { + /** + * Returns a new [ParameterValue] for the specified [key] and an [Int] parsed representation + * of [rawValue], or null if the value isn't a valid stringified [Int]. + */ + internal fun createParameter(key: String, rawValue: String): ParameterValue? { + return rawValue.toIntOrNull()?.let { IntParameterValue(key, it) } + } + } + } + + private class LongParameterValue private constructor( + key: String, value: Long + ): ParameterValue(key, value) { + companion object { + /** + * Returns a new [ParameterValue] for the specified [key] and a [Long] parsed representation + * of [rawValue], or null if the value isn't a valid stringified [Long]. + */ + internal fun createParameter(key: String, rawValue: String): ParameterValue? { + return rawValue.toLongOrNull()?.let { LongParameterValue(key, it) } + } + } + } + + private class FloatParameterValue private constructor( + key: String, value: Float + ): ParameterValue(key, value) { + companion object { + /** + * Returns a new [ParameterValue] for the specified [key] and a [Float] parsed representation + * of [rawValue], or null if the value isn't a valid stringified [Float]. + */ + internal fun createParameter(key: String, rawValue: String): ParameterValue? { + return rawValue.toFloatOrNull()?.let { FloatParameterValue(key, it) } + } + } + } + + private class DoubleParameterValue private constructor( + key: String, value: Double + ): ParameterValue(key, value) { + companion object { + /** + * Returns a new [ParameterValue] for the specified [key] and a [Double] parsed representation + * of [rawValue], or null if the value isn't a valid stringified [Double]. + */ + internal fun createParameter(key: String, rawValue: String): ParameterValue? { + return rawValue.toDoubleOrNull()?.let { DoubleParameterValue(key, it) } + } + } + } + + private class StringParameterValue private constructor( + key: String, value: String + ): ParameterValue(key, value) { + companion object { + /** + * Returns a new [ParameterValue] for the specified [key] and a [String] parsed representation + * of [rawValue], or null if the value isn't a valid stringified [String]. + */ + internal fun createParameter(key: String, rawValue: String): ParameterValue = + StringParameterValue(key, rawValue) + } + } + + internal companion object { + private val booleanValueParser = createParser(BooleanParameterValue::createParameter) + private val intValueParser = createParser(IntParameterValue::createParameter) + private val longValueParser = createParser(LongParameterValue::createParameter) + private val floatValueParser = createParser(FloatParameterValue::createParameter) + private val doubleValueParser = createParser(DoubleParameterValue::createParameter) + private val stringValueParser = createParser(StringParameterValue::createParameter) + + /** + * Returns a new [ParameterValueParser] corresponding to the type of the specified [field], or + * null if the field's type is unsupported. + */ + fun createParserForField(field: Field): ParameterValueParser? { + return when (field.type) { + Boolean::class.java -> booleanValueParser + Int::class.java -> intValueParser + Long::class.java -> longValueParser + Float::class.java -> floatValueParser + Double::class.java -> doubleValueParser + String::class.java -> stringValueParser + else -> null + } + } + + /** A string parser for a specific [ParameterValue] type. */ + fun interface ParameterValueParser { + /** + * Returns a [ParameterValue] corresponding to the specified [key], and with a type-safe + * parsing of [rawValue], or null if the string value is invalid. + */ + fun parseParameter(key: String, rawValue: String): ParameterValue? + } + + // A hack to work around the fact that Kotlin doesn't support assignment conversion from + // references to a functional interface. + private fun createParser(parser: ParameterValueParser) = parser + } +} diff --git a/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedAndroidJUnit4ClassRunner.kt b/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedAndroidJUnit4ClassRunner.kt new file mode 100644 index 00000000000..fcb642a437c --- /dev/null +++ b/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedAndroidJUnit4ClassRunner.kt @@ -0,0 +1,36 @@ +package org.oppia.android.testing.junit + +import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner +import org.junit.runners.model.FrameworkMethod +import org.junit.runners.model.Statement + +/** + * A [AndroidJUnit4ClassRunner] which supports [OppiaParameterizedTestRunner] when running on an + * Espresso-driven platform. + */ +@Suppress("unused") // This class is constructed using reflection. +internal class ParameterizedAndroidJUnit4ClassRunner( + testClass: Class<*>, + private val parameterizedMethods: Map, + private val methodName: String?, + private val iterationName: String? +): AndroidJUnit4ClassRunner(testClass), ParameterizedRunnerOverrideMethods { + private val delegate by lazy { + ParameterizedRunnerDelegate( + parameterizedMethods, + methodName, + iterationName + ).also { delegate -> + delegate.fetchChildrenFromParent = { super.getChildren() } + delegate.fetchTestNameFromParent = { method -> super.testName(method) } + delegate.fetchMethodInvokerFromParent = { method, test -> super.methodInvoker(method, test) } + } + } + + override fun getChildren(): MutableList = delegate.getChildren() + + override fun testName(method: FrameworkMethod?): String = delegate.testName(method) + + override fun methodInvoker(method: FrameworkMethod?, test: Any?): Statement = + delegate.methodInvoker(method, test) +} diff --git a/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedMethod.kt b/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedMethod.kt new file mode 100644 index 00000000000..d56cb4e3e87 --- /dev/null +++ b/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedMethod.kt @@ -0,0 +1,44 @@ +package org.oppia.android.testing.junit + +import java.lang.reflect.Field +import java.util.Locale + +/** + * A parameterized method used by [OppiaParameterizedTestRunner] when defining sub-tests that are + * run as part of the test suite. + * + * @property methodName the name of the test method that's been parameterized + */ +internal class ParameterizedMethod( + val methodName: String, + private val values: Map>, + private val parameterFields: List +) { + /** The names of all iterations run for this method. */ + val iterationNames: Collection by lazy { values.keys } + + /** + * Updates the specified [testClassInstance]'s parameter-injected fields to the values + * corresponding to the specified [iterationName] iteration. + * + * This should always be called before the test's execution begins. It's also expected that this + * method is called for each iteration (since the test method should be executed multiples, once + * for each of its iteration). + */ + internal fun initializeForTest(testClassInstance: Any, iterationName: String) { + // Retrieve the setters for the fields (since these are expected to be used instead of direct + // property access in Kotlin). Note that these need to be re-fetched since the instance class + // may change (due to Robolectric instrumentation including custom class loading & bytecode + // changes). + val baseClass = testClassInstance.javaClass + val fieldSetters = parameterFields.map { field -> + val setterMethod = + baseClass.getDeclaredMethod("set${field.name.capitalize(Locale.US)}", field.type) + field.name to setterMethod + }.toMap() + values.getValue(iterationName).forEach { parameterValue -> + val fieldSetter = fieldSetters.getValue(parameterValue.key) + fieldSetter.invoke(testClassInstance, parameterValue.value) + } + } +} diff --git a/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedRobolectricTestRunner.kt b/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedRobolectricTestRunner.kt new file mode 100644 index 00000000000..1a6df490125 --- /dev/null +++ b/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedRobolectricTestRunner.kt @@ -0,0 +1,49 @@ +package org.oppia.android.testing.junit + +import org.junit.runners.model.FrameworkMethod +import org.junit.runners.model.Statement +import org.robolectric.RobolectricTestRunner + +/** + * A [RobolectricTestRunner] which supports [OppiaParameterizedTestRunner] when running on a local + * JVM using Robolectric. + */ +@Suppress("unused") // This class is constructed using reflection. +internal class ParameterizedRobolectricTestRunner( + testClass: Class<*>, + private val parameterizedMethods: Map, + private val methodName: String?, + private val iterationName: String? +): RobolectricTestRunner(testClass), ParameterizedRunnerOverrideMethods { + private val delegate by lazy { + ParameterizedRunnerDelegate( + parameterizedMethods, + methodName, + iterationName + ).also { delegate -> + delegate.fetchChildrenFromParent = { super.getChildren() } + delegate.fetchTestNameFromParent = { method -> super.testName(method) } + } + } + + override fun getChildren(): MutableList = delegate.getChildren() + + override fun testName(method: FrameworkMethod?): String = delegate.testName(method) + + override fun methodInvoker(method: FrameworkMethod?, test: Any?): Nothing { + throw Exception("Expected this to never be executed in the Robolectric environment.") + } + + override fun getHelperTestRunner( + bootstrappedTestClass: Class<*>? + ): HelperTestRunner { + return object: HelperTestRunner(bootstrappedTestClass) { + override fun methodInvoker(method: FrameworkMethod?, test: Any?): Statement { + delegate.fetchMethodInvokerFromParent = { innerMethod, innerParent -> + super.methodInvoker(innerMethod, innerParent) + } + return delegate.methodInvoker(method, test) + } + } + } +} diff --git a/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedRunnerDelegate.kt b/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedRunnerDelegate.kt new file mode 100644 index 00000000000..02d90dbd5d8 --- /dev/null +++ b/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedRunnerDelegate.kt @@ -0,0 +1,67 @@ +package org.oppia.android.testing.junit + +import org.junit.runners.model.FrameworkMethod +import org.junit.runners.model.Statement + +/** + * A common helper for platform-specific helper runners. + * + * This class performs the actual field injection and execution delegation for running each + * parameterized test method. + */ +internal class ParameterizedRunnerDelegate( + private val parameterizedMethods: Map, + private val methodName: String?, + private val iterationName: String? +): ParameterizedRunnerOverrideMethods { + /** + * A lambda used to call into the parent runner's [getChildren] method. This should be set by + * helper parameterized test runners. + */ + lateinit var fetchChildrenFromParent: () -> MutableList + + /** + * A lambda used to call into the parent runner's [testName] method. This should be set by helper + * parameterized test runners. + */ + lateinit var fetchTestNameFromParent: (FrameworkMethod?) -> String + + /** + * A lambda used to call into the parent runner's [methodInvoker] method. This should be set by + * helper parameterized test runners. + */ + lateinit var fetchMethodInvokerFromParent: (FrameworkMethod?, Any?) -> Statement + + override fun getChildren(): MutableList { + return fetchChildrenFromParent().filter { + // Either only match to the specific method, or no parameterized methods. + if (methodName != null) { + it.method.name == methodName + } else it.method.name !in parameterizedMethods.keys + }.toMutableList() + } + + override fun testName(method: FrameworkMethod?): String { + return if (methodName != null) { + "${fetchTestNameFromParent(method)}-$iterationName" + } else fetchTestNameFromParent(method) + } + + override fun methodInvoker(method: FrameworkMethod?, test: Any?): Statement { + val invoker = fetchMethodInvokerFromParent(method, test) + checkNotNull(test) { "Expected test to be initialized." } + return if (methodName != null && iterationName != null) { + val parameterizedMethod = checkNotNull(parameterizedMethods[method?.name]) { + "Expected to find registered parameterized method: ${method?.name}, available:" + + " ${parameterizedMethods.keys}" + } + object : Statement() { + override fun evaluate() { + // Initialize the test prior to execution. + parameterizedMethod.initializeForTest(test, iterationName) + invoker.evaluate() + } + } + } else invoker + } +} diff --git a/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedRunnerOverrideMethods.kt b/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedRunnerOverrideMethods.kt new file mode 100644 index 00000000000..6d591ec274f --- /dev/null +++ b/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedRunnerOverrideMethods.kt @@ -0,0 +1,18 @@ +package org.oppia.android.testing.junit + +import org.junit.runners.model.FrameworkMethod +import org.junit.runners.model.Statement + +/** + * Specifies methods that the helper parameterized runners should override from JUnit's test runner. + */ +internal interface ParameterizedRunnerOverrideMethods { + /** See [org.junit.runners.BlockJUnit4ClassRunner.getChildren]. */ + fun getChildren(): MutableList + + /** See [org.junit.runners.BlockJUnit4ClassRunner.testName]. */ + fun testName(method: FrameworkMethod?): String + + /** See [org.junit.runners.BlockJUnit4ClassRunner.methodInvoker]. */ + fun methodInvoker(method: FrameworkMethod?, test: Any?): Statement +} diff --git a/testing/src/main/java/org/oppia/android/testing/math/BUILD.bazel b/testing/src/main/java/org/oppia/android/testing/math/BUILD.bazel index 7599a2c3008..ea0b8ba6b54 100644 --- a/testing/src/main/java/org/oppia/android/testing/math/BUILD.bazel +++ b/testing/src/main/java/org/oppia/android/testing/math/BUILD.bazel @@ -108,3 +108,18 @@ kt_android_library( "//third_party:com_google_truth_truth", ], ) + +kt_android_library( + name = "token_subject", + testonly = True, + srcs = [ + "TokenSubject.kt", + ], + visibility = [ + "//:oppia_testing_visibility", + ], + deps = [ + "//third_party:com_google_truth_truth", + "//utility/src/main/java/org/oppia/android/util/math:tokenizer", + ], +) diff --git a/testing/src/main/java/org/oppia/android/testing/math/TokenSubject.kt b/testing/src/main/java/org/oppia/android/testing/math/TokenSubject.kt new file mode 100644 index 00000000000..a623c59c7b6 --- /dev/null +++ b/testing/src/main/java/org/oppia/android/testing/math/TokenSubject.kt @@ -0,0 +1,173 @@ +package org.oppia.android.testing.math + +import com.google.common.truth.BooleanSubject +import com.google.common.truth.DoubleSubject +import com.google.common.truth.FailureMetadata +import com.google.common.truth.IntegerSubject +import com.google.common.truth.StringSubject +import com.google.common.truth.Subject +import com.google.common.truth.Truth.assertAbout +import com.google.common.truth.Truth.assertThat +import org.oppia.android.testing.math.TokenSubject.Companion.assertThat +import org.oppia.android.util.math.MathTokenizer.Companion.Token +import org.oppia.android.util.math.MathTokenizer.Companion.Token.DivideSymbol +import org.oppia.android.util.math.MathTokenizer.Companion.Token.EqualsSymbol +import org.oppia.android.util.math.MathTokenizer.Companion.Token.ExponentiationSymbol +import org.oppia.android.util.math.MathTokenizer.Companion.Token.FunctionName +import org.oppia.android.util.math.MathTokenizer.Companion.Token.IncompleteFunctionName +import org.oppia.android.util.math.MathTokenizer.Companion.Token.InvalidToken +import org.oppia.android.util.math.MathTokenizer.Companion.Token.LeftParenthesisSymbol +import org.oppia.android.util.math.MathTokenizer.Companion.Token.MinusSymbol +import org.oppia.android.util.math.MathTokenizer.Companion.Token.MultiplySymbol +import org.oppia.android.util.math.MathTokenizer.Companion.Token.PlusSymbol +import org.oppia.android.util.math.MathTokenizer.Companion.Token.PositiveInteger +import org.oppia.android.util.math.MathTokenizer.Companion.Token.PositiveRealNumber +import org.oppia.android.util.math.MathTokenizer.Companion.Token.RightParenthesisSymbol +import org.oppia.android.util.math.MathTokenizer.Companion.Token.SquareRootSymbol +import org.oppia.android.util.math.MathTokenizer.Companion.Token.VariableName + +// TODO(#4121): Add tests for this class. + +/** + * Truth subject for verifying properties of [Token]s. + * + * Call [assertThat] to create the subject. + */ +class TokenSubject( + metadata: FailureMetadata, + private val actual: Token +) : Subject(metadata, actual) { + /** Returns an [IntegerSubject] to test [Token.startIndex]. */ + fun hasStartIndexThat(): IntegerSubject = assertThat(actual.startIndex) + + /** Returns an [IntegerSubject] to test [Token.endIndex]. */ + fun hasEndIndexThat(): IntegerSubject = assertThat(actual.endIndex) + + /** + * Verifies that the [Token] being tested is a [PositiveInteger], and returns an [IntegerSubject] + * to test its [PositiveInteger.parsedValue]. + */ + fun isPositiveIntegerWhoseValue(): IntegerSubject { + return assertThat(actual.asVerifiedType().parsedValue) + } + + /** + * Verifies that the [Token] being tested is a [PositiveRealNumber], and returns a [DoubleSubject] + * to test its [PositiveRealNumber.parsedValue]. + */ + fun isPositiveRealNumberWhoseValue(): DoubleSubject { + return assertThat(actual.asVerifiedType().parsedValue) + } + + /** + * Verifies that the [Token] being tested is a [VariableName], and returns a [StringSubject] to + * test its [VariableName.parsedName]. + */ + fun isVariableWhoseName(): StringSubject { + return assertThat(actual.asVerifiedType().parsedName) + } + + /** + * Verifies that the [Token] being tested is a [FunctionName], and returns a [FunctionNameSubject] + * to test specific attributes of the function name. + */ + fun isFunctionNameThat(): FunctionNameSubject { + return FunctionNameSubject.assertThat(actual.asVerifiedType()) + } + + /** Verifies that the [Token] being tested is a [MinusSymbol]. */ + fun isMinusSymbol() { + actual.asVerifiedType() + } + + /** Verifies that the [Token] being tested is a [SquareRootSymbol]. */ + fun isSquareRootSymbol() { + actual.asVerifiedType() + } + + /** Verifies that the [Token] being tested is a [PlusSymbol]. */ + fun isPlusSymbol() { + actual.asVerifiedType() + } + + /** Verifies that the [Token] being tested is a [MultiplySymbol]. */ + fun isMultiplySymbol() { + actual.asVerifiedType() + } + + /** Verifies that the [Token] being tested is a [DivideSymbol]. */ + fun isDivideSymbol() { + actual.asVerifiedType() + } + + /** Verifies that the [Token] being tested is an [ExponentiationSymbol]. */ + fun isExponentiationSymbol() { + actual.asVerifiedType() + } + + /** Verifies that the [Token] being tested is an [EqualsSymbol]. */ + fun isEqualsSymbol() { + actual.asVerifiedType() + } + + /** Verifies that the [Token] being tested is a [LeftParenthesisSymbol]. */ + fun isLeftParenthesisSymbol() { + actual.asVerifiedType() + } + + /** Verifies that the [Token] being tested is a [RightParenthesisSymbol]. */ + fun isRightParenthesisSymbol() { + actual.asVerifiedType() + } + + /** Verifies that the [Token] being tested is an [InvalidToken]. */ + fun isInvalidToken() { + actual.asVerifiedType() + } + + /** Verifies that the [Token] being tested is an [IncompleteFunctionName]. */ + fun isIncompleteFunctionName() { + actual.asVerifiedType() + } + + /** + * Truth subject for verifying properties of [Token]FunctionName. + * + * Call [assertThat] to create the subject. + */ + class FunctionNameSubject( + metadata: FailureMetadata, + private val actual: FunctionName + ) : Subject(metadata, actual) { + /** + * Returns a [StringSubject] to test the value of [FunctionName.parsedName] for the function + * name being tested by this subject. + */ + fun hasNameThat(): StringSubject = assertThat(actual.parsedName) + + /** + * Returns a [BooleanSubject] to test the value of [FunctionName.isAllowedFunction] for the + * function name being tested by this subject. + */ + fun hasIsAllowedPropertyThat(): BooleanSubject = assertThat(actual.isAllowedFunction) + + companion object { + /** + * Returns a new [FunctionNameSubject] to verify aspects of the specified [FunctionName] + * value. + */ + internal fun assertThat(actual: FunctionName): FunctionNameSubject = + assertAbout(::FunctionNameSubject).that(actual) + } + } + + companion object { + /** Returns a new [TokenSubject] to verify aspects of the specified [Token] value. */ + fun assertThat(actual: Token): TokenSubject = assertAbout(::TokenSubject).that(actual) + + private inline fun Token.asVerifiedType(): T { + assertThat(this).isInstanceOf(T::class.java) + return this as T + } + } +} From 83e800cdb2252fd167f3597e076c5854569d59fe Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 20 Jan 2022 22:24:11 -0800 Subject: [PATCH 050/162] Add & update tests. This introduces tests for PeekableIterator, and reimplements all of MathTokenizer's tests to be more structured, thorough, and a bit more maintainable (i.e. by leveraging parameterized tests). --- .../org/oppia/android/util/math/BUILD.bazel | 3 + .../oppia/android/util/math/MathTokenizer.kt | 94 +- .../android/util/math/PeekableIterator.kt | 66 +- .../org/oppia/android/util/math/BUILD.bazel | 22 +- .../android/util/math/MathTokenizerTest.kt | 849 ++++++++++++++---- .../android/util/math/PeekableIteratorTest.kt | 600 +++++++++++++ 6 files changed, 1446 insertions(+), 188 deletions(-) create mode 100644 utility/src/test/java/org/oppia/android/util/math/PeekableIteratorTest.kt diff --git a/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel b/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel index 3d3925115ee..9250d3ffba1 100644 --- a/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel @@ -40,4 +40,7 @@ kt_android_library( srcs = [ "PeekableIterator.kt", ], + visibility = [ + "//:oppia_testing_visibility", + ], ) diff --git a/utility/src/main/java/org/oppia/android/util/math/MathTokenizer.kt b/utility/src/main/java/org/oppia/android/util/math/MathTokenizer.kt index 37ca0410cd0..b710ae7908b 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathTokenizer.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathTokenizer.kt @@ -10,19 +10,32 @@ import org.oppia.android.app.model.MathUnaryOperation import org.oppia.android.app.model.MathUnaryOperation.Operator.NEGATE import org.oppia.android.app.model.MathUnaryOperation.Operator.POSITIVE import java.lang.StringBuilder - -// TODO: rename to MathTokenizer & add documentation. -// TODO: consider a more efficient implementation that uses 1 underlying buffer (which could still -// be sequence-backed) with a forced lookahead-of-1 API, to also avoid rebuffering when parsing -// sequences of characters like for integers. - -// TODO: add customization to omit certain symbols (such as variables for numeric expressions?) +import org.oppia.android.util.math.PeekableIterator.Companion.toPeekableIterator + +/** + * Input tokenizer for math (numeric & algebraic) expressions and equations. + * + * See https://docs.google.com/document/d/1JMpbjqRqdEpye67HvDoqBo_rtScY9oEaB7SwKBBspss/edit for the + * grammar specification supported by this tokenizer. + * + * This class implements an LL(1) single-pass tokenizer with no caching. Use [tokenize] to produce a + * sequence of [Token]s from the given input stream. + */ class MathTokenizer private constructor() { companion object { + /** + * Returns a [Sequence] of [Token]s for the specified input string. + * + * Note that this tokenizer will attempt to recover if an invalid token is encountered (i.e. + * tokenization will continue). Further, tokenization occurs lazily (i.e. as the sequence is + * traversed), so calling this method is essentially zero-cost until tokens are actually needed. + * The sequence should be converted to a [List] if they need to be retained after initial + * tokenization since the sequence retains no memory. + */ fun tokenize(input: String): Sequence = tokenize(input.toCharArray().asSequence()) - fun tokenize(input: Sequence): Sequence { - val chars = PeekableIterator.fromSequence(input) + private fun tokenize(input: Sequence): Sequence { + val chars = input.toPeekableIterator() return generateSequence { // Consume any whitespace that might precede a valid token. chars.consumeWhitespace() @@ -37,7 +50,6 @@ class MathTokenizer private constructor() { '+' -> tokenizeSymbol(chars) { startIndex, endIndex -> Token.PlusSymbol(startIndex, endIndex) } - // TODO: add tests for different subtraction/minus symbols. '-', '−' -> tokenizeSymbol(chars) { startIndex, endIndex -> Token.MinusSymbol(startIndex, endIndex) } @@ -73,6 +85,7 @@ class MathTokenizer private constructor() { val integerPart1 = parseInteger(chars) ?: return Token.InvalidToken(startIndex, endIndex = chars.getRetrievalCount()) + val integerEndIndex = chars.getRetrievalCount() // The end index for integers. chars.consumeWhitespace() // Whitespace is allowed between digits and the '.'. return if (chars.peek() == '.') { chars.next() // Parse the "." since it will be re-added later. @@ -82,8 +95,7 @@ class MathTokenizer private constructor() { val integerPart2 = parseInteger(chars) ?: return Token.InvalidToken(startIndex, endIndex = chars.getRetrievalCount()) - // TODO: validate that the result isn't NaN or INF. - val doubleValue = "$integerPart1.$integerPart2".toDoubleOrNull() + val doubleValue = "$integerPart1.$integerPart2".toValidDoubleOrNull() ?: return Token.InvalidToken(startIndex, endIndex = chars.getRetrievalCount()) Token.PositiveRealNumber(doubleValue, startIndex, endIndex = chars.getRetrievalCount()) } else { @@ -91,7 +103,7 @@ class MathTokenizer private constructor() { integerPart1.toIntOrNull() ?: return Token.InvalidToken(startIndex, endIndex = chars.getRetrievalCount()), startIndex, - endIndex = chars.getRetrievalCount() + integerEndIndex ) } } @@ -250,39 +262,77 @@ class MathTokenizer private constructor() { } else null // Failed to parse; no digits. } + private fun String.toValidDoubleOrNull(): Double? { + return toDoubleOrNull()?.takeIf { it.isFinite() } + } + + /** Represents a token that may act as a unary operator. */ interface UnaryOperatorToken { + /** + * Returns the [MathUnaryOperation.Operator] that would be associated with this token if it's + * treated as a unary operator. + */ fun getUnaryOperator(): MathUnaryOperation.Operator } + /** Represents a token that may act as a binary operator. */ interface BinaryOperatorToken { + /** + * Returns the [MathBinaryOperation.Operator] that would be associated with this token if it's + * treated as a binary operator. + */ fun getBinaryOperator(): MathBinaryOperation.Operator } + /** Represents a token that may be encountered during tokenization. */ sealed class Token { - /** The index in the input stream at which point this token begins. */ + /** The (inclusive) index in the input stream at which point this token begins. */ abstract val startIndex: Int /** The (exclusive) index in the input stream at which point this token ends. */ abstract val endIndex: Int + /** + * Represents a positive integer (i.e. no decimal point, and no negative sign). + * + * @property parsedValue the parsed value of the integer + */ class PositiveInteger( val parsedValue: Int, override val startIndex: Int, override val endIndex: Int ) : Token() + /** + * Represents a positive real number (i.e. contains a decimal point, but no negative sign). + * + * @property parsedValue the parsed value of the real number as a [Double] + */ class PositiveRealNumber( val parsedValue: Double, override val startIndex: Int, override val endIndex: Int ) : Token() + /** + * Represents a variable. + * + * @property parsedName the name of the variable + */ class VariableName( val parsedName: String, override val startIndex: Int, override val endIndex: Int ) : Token() + /** + * Represents a recognized function name (otherwise sequential letters are treated as + * variables), e.g.: sqrt. + * + * @property parsedName the name of the function + * @property isAllowedFunction whether the function is supported by the parser. This helps + * with error detection & management while parsing. + */ class FunctionName( val parsedName: String, val isAllowedFunction: Boolean, @@ -290,6 +340,10 @@ class MathTokenizer private constructor() { override val endIndex: Int ) : Token() + /** Represents a square root sign, i.e. '√'. */ + class SquareRootSymbol(override val startIndex: Int, override val endIndex: Int) : Token() + + /** Represents a minus sign, e.g. '-'. */ class MinusSymbol( override val startIndex: Int, override val endIndex: Int @@ -299,8 +353,7 @@ class MathTokenizer private constructor() { override fun getBinaryOperator(): MathBinaryOperation.Operator = SUBTRACT } - class SquareRootSymbol(override val startIndex: Int, override val endIndex: Int) : Token() - + /** Represents a plus sign, e.g. '+'. */ class PlusSymbol( override val startIndex: Int, override val endIndex: Int @@ -310,6 +363,7 @@ class MathTokenizer private constructor() { override fun getBinaryOperator(): MathBinaryOperation.Operator = ADD } + /** Represents a multiply sign, e.g. '*'. */ class MultiplySymbol( override val startIndex: Int, override val endIndex: Int @@ -317,6 +371,7 @@ class MathTokenizer private constructor() { override fun getBinaryOperator(): MathBinaryOperation.Operator = MULTIPLY } + /** Represents a divide sign, e.g. '/'. */ class DivideSymbol( override val startIndex: Int, override val endIndex: Int @@ -324,6 +379,7 @@ class MathTokenizer private constructor() { override fun getBinaryOperator(): MathBinaryOperation.Operator = DIVIDE } + /** Represents an exponent sign, i.e. '^'. */ class ExponentiationSymbol( override val startIndex: Int, override val endIndex: Int @@ -331,27 +387,31 @@ class MathTokenizer private constructor() { override fun getBinaryOperator(): MathBinaryOperation.Operator = EXPONENTIATE } + /** Represents an equals sign, i.e. '='. */ class EqualsSymbol(override val startIndex: Int, override val endIndex: Int) : Token() + /** Represents a left parenthesis symbol, i.e. '('. */ class LeftParenthesisSymbol( override val startIndex: Int, override val endIndex: Int ) : Token() + /** Represents a right parenthesis symbol, i.e. ')'. */ class RightParenthesisSymbol( override val startIndex: Int, override val endIndex: Int ) : Token() + /** Represents an incomplete function name, e.g. 'sqr'. */ class IncompleteFunctionName( override val startIndex: Int, override val endIndex: Int ) : Token() + /** Represents an invalid character that doesn't fit any of the other [Token] types. */ class InvalidToken(override val startIndex: Int, override val endIndex: Int) : Token() } - // TODO: consider whether to use the more correct & expensive Java Char.isWhitespace(). private fun Char.isWhitespace(): Boolean = when (this) { ' ', '\t', '\n', '\r' -> true else -> false diff --git a/utility/src/main/java/org/oppia/android/util/math/PeekableIterator.kt b/utility/src/main/java/org/oppia/android/util/math/PeekableIterator.kt index 1a7abacc061..4d752fdc2f1 100644 --- a/utility/src/main/java/org/oppia/android/util/math/PeekableIterator.kt +++ b/utility/src/main/java/org/oppia/android/util/math/PeekableIterator.kt @@ -1,16 +1,36 @@ package org.oppia.android.util.math -class PeekableIterator(private val backingIterator: Iterator) : Iterator { +/** + * An [Iterator] for [Sequence]s that may have exactly up to 1 element lookahead. + * + * This iterator is intended to be used by tokenizers and parsers to force an LL(1) implementation + * of both (since only one token may be observed before retrieval). Further, this implementation is + * intentionally limited to sequences for potential performance: since no more than one element + * requires lookahead, having a backed array is unnecessary which means a potentially expensive and + * fully dynamic sequence could back the iterator. While especially large inputs aren't expected, + * they are inherently supported by the implementation with no additional overhead. + * + * New implementations can be created using [toPeekableIterator]. + * + * This class is not safe to use across multiple threads and requires synchronization. + */ +class PeekableIterator private constructor( + private val backingIterator: Iterator +) : Iterator { private var next: T? = null private var count: Int = 0 override fun hasNext(): Boolean = next != null || backingIterator.hasNext() - override fun next(): T = next?.also { - next = null - count++ - } ?: retrieveNext() + override fun next(): T = (next?.also { next = null } ?: retrieveNext()).also { count++ } + /** + * Returns the next element to be returned by [next] without actually consuming it, or ``null`` if + * there isn't one (i.e. [hasNext] returns false). + * + * It's safe to call this both at the end of the iterator, and multiple times (at any point in the + * iteration). + */ fun peek(): T? { return when { next != null -> next @@ -19,20 +39,52 @@ class PeekableIterator(private val backingIterator: Iterator) : Iter } } + /** + * Consumes and returns the next token if it matches the value provided by [expected]. + * + * This method essentially behaves the same way as [expectNextMatches]. + */ fun expectNextValue(expected: () -> T): T? = expectNextMatches { it == expected() } + /** + * Consumes and returns the next token (if it's available--see [peek]) if it passes the specified + * [predicate], otherwise ``null`` is returned. + * + * Note that a ``nul`` return value isn't sufficient to determine the iterator has ended + * ([hasNext] or [peek] should be used for that, instead). + * + * Note also that [predicate] will only be called once, but no assumption should be made as to + * when it will be called. + */ fun expectNextMatches(predicate: (T) -> Boolean): T? { // Only call the predicate if not at the end of the stream, and only call next() if the next // value matches. return peek()?.takeIf(predicate)?.also { next() } } + /** + * Returns the number of elements consumed by this iterator so far via [next]. + * + * At the beginning of iteration, this will return 0. At the end (i.e. when [hasNext] returns + * false), this will return the size of the underlying sequence/container (depending on if + * iteration began at the beginning of the sequence--see the caveat in [toPeekableIterator] for + * specifics). + */ fun getRetrievalCount(): Int = count private fun retrieveNext(): T = backingIterator.next() companion object { - fun fromSequence(sequence: Sequence): PeekableIterator = - PeekableIterator(sequence.iterator()) + /** + * Returns a new [PeekableIterator] for this [Sequence]. + * + * Note that iteration begins at the current point of the [Sequence] (since sequences may not + * retain state and can't be rewinded), so care should be taken when using multiple iterators + * for the same sequence (including when converting the sequence to another structure, like a + * [List]). Some sequences do support multiple iteration, so the exact behavior of the returned + * iterator will be sequence implementation dependent. + */ + fun Sequence.toPeekableIterator(): PeekableIterator = + PeekableIterator(iterator()) } } diff --git a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel index cd67c41492f..27c60ff1941 100644 --- a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel @@ -49,7 +49,9 @@ oppia_android_test( deps = [ "//model/src/main/proto:math_java_proto_lite", "//testing:assertion_helpers", - "//third_party:androidx_test_ext_junit", + "//testing/src/main/java/org/oppia/android/testing/junit:oppia_parameterized_test_runner", + "//testing/src/main/java/org/oppia/android/testing/junit:parameterized_robolectric_test_runner", + "//testing/src/main/java/org/oppia/android/testing/math:token_subject", "//third_party:com_google_truth_truth", "//third_party:junit_junit", "//third_party:org_robolectric_robolectric", @@ -58,6 +60,24 @@ oppia_android_test( ], ) +oppia_android_test( + name = "PeekableIteratorTest", + srcs = ["PeekableIteratorTest.kt"], + custom_package = "org.oppia.android.util.math", + test_class = "org.oppia.android.util.math.PeekableIteratorTest", + test_manifest = "//utility:test_manifest", + deps = [ + "//testing", + "//third_party:androidx_test_ext_junit", + "//third_party:com_google_truth_truth", + "//third_party:junit_junit", + "//third_party:org_mockito_mockito-core", + "//third_party:org_robolectric_robolectric", + "//third_party:robolectric_android-all", + "//utility/src/main/java/org/oppia/android/util/math:peekable_iterator", + ], +) + oppia_android_test( name = "PolynomialExtensionsTest", srcs = ["PolynomialExtensionsTest.kt"], diff --git a/utility/src/test/java/org/oppia/android/util/math/MathTokenizerTest.kt b/utility/src/test/java/org/oppia/android/util/math/MathTokenizerTest.kt index ac7e6556b9c..276911cb62a 100644 --- a/utility/src/test/java/org/oppia/android/util/math/MathTokenizerTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/MathTokenizerTest.kt @@ -1,195 +1,718 @@ package org.oppia.android.util.math -import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.google.common.truth.DoubleSubject -import com.google.common.truth.FailureMetadata -import com.google.common.truth.IntegerSubject -import com.google.common.truth.StringSubject -import com.google.common.truth.Subject -import com.google.common.truth.Truth.assertAbout import com.google.common.truth.Truth.assertThat import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.util.math.MathTokenizer.Companion.Token +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Iteration +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Parameter +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.RunParameterized +import org.oppia.android.testing.math.TokenSubject.Companion.assertThat import org.robolectric.annotation.LooperMode /** Tests for [MathTokenizer]. */ -@RunWith(AndroidJUnit4::class) +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") +@RunWith(OppiaParameterizedTestRunner::class) @LooperMode(LooperMode.Mode.PAUSED) class MathTokenizerTest { + @Parameter lateinit var variableName: String + @Parameter lateinit var funcName: String + @Parameter lateinit var token: String + @Test - fun testLotsOfCases() { - // TODO: split this up - // testTokenize_emptyString_producesNoTokens - val tokens1 = MathTokenizer.tokenize(" ").toList() - assertThat(tokens1).isEmpty() - - val tokens2 = MathTokenizer.tokenize(" 2 ").toList() - assertThat(tokens2).hasSize(1) - assertThat(tokens2.first()).isPositiveIntegerWhoseValue().isEqualTo(2) - - val tokens3 = MathTokenizer.tokenize(" 2.5 ").toList() - assertThat(tokens3).hasSize(1) - assertThat(tokens3.first()).isPositiveRealNumberWhoseValue().isWithin(1e-5).of(2.5) - - val tokens4 = MathTokenizer.tokenize(" x ").toList() - assertThat(tokens4).hasSize(1) - assertThat(tokens4.first()).isVariableWhoseName().isEqualTo("x") - - val tokens5 = MathTokenizer.tokenize(" z x ").toList() - assertThat(tokens5).hasSize(2) - assertThat(tokens5[0]).isVariableWhoseName().isEqualTo("z") - assertThat(tokens5[1]).isVariableWhoseName().isEqualTo("x") - - val tokens6 = MathTokenizer.tokenize("2^3^2").toList() - assertThat(tokens6).hasSize(5) - assertThat(tokens6[0]).isPositiveIntegerWhoseValue().isEqualTo(2) - assertThat(tokens6[1]).isExponentiationSymbol() - assertThat(tokens6[2]).isPositiveIntegerWhoseValue().isEqualTo(3) - assertThat(tokens6[3]).isExponentiationSymbol() - assertThat(tokens6[4]).isPositiveIntegerWhoseValue().isEqualTo(2) - - val tokens7 = MathTokenizer.tokenize("sqrt(2)").toList() - assertThat(tokens7).hasSize(4) - assertThat(tokens7[0]).isFunctionWhoseName().isEqualTo("sqrt") - assertThat(tokens7[1]).isLeftParenthesisSymbol() - assertThat(tokens7[2]).isPositiveIntegerWhoseValue().isEqualTo(2) - assertThat(tokens7[3]).isRightParenthesisSymbol() - - val tokens8 = MathTokenizer.tokenize("sqr(2)").toList() - assertThat(tokens8).hasSize(4) - assertThat(tokens8[0]).isIncompleteFunctionName() - assertThat(tokens8[1]).isLeftParenthesisSymbol() - assertThat(tokens8[2]).isPositiveIntegerWhoseValue().isEqualTo(2) - assertThat(tokens8[3]).isRightParenthesisSymbol() - - val tokens9 = MathTokenizer.tokenize("xyz(2)").toList() - assertThat(tokens9).hasSize(6) - assertThat(tokens9[0]).isVariableWhoseName().isEqualTo("x") - assertThat(tokens9[1]).isVariableWhoseName().isEqualTo("y") - assertThat(tokens9[2]).isVariableWhoseName().isEqualTo("z") - assertThat(tokens9[3]).isLeftParenthesisSymbol() - assertThat(tokens9[4]).isPositiveIntegerWhoseValue().isEqualTo(2) - assertThat(tokens9[5]).isRightParenthesisSymbol() - - val tokens10 = MathTokenizer.tokenize("732").toList() - assertThat(tokens10).hasSize(1) - assertThat(tokens10.first()).isPositiveIntegerWhoseValue().isEqualTo(732) - - val tokens11 = MathTokenizer.tokenize("73 2").toList() - assertThat(tokens11).hasSize(2) - assertThat(tokens11[0]).isPositiveIntegerWhoseValue().isEqualTo(73) - assertThat(tokens11[1]).isPositiveIntegerWhoseValue().isEqualTo(2) - - val tokens12 = MathTokenizer.tokenize("1*2-3+4^7-8/3*2+7").toList() - assertThat(tokens12).hasSize(17) - assertThat(tokens12[0]).isPositiveIntegerWhoseValue().isEqualTo(1) - assertThat(tokens12[1]).isMultiplySymbol() - assertThat(tokens12[2]).isPositiveIntegerWhoseValue().isEqualTo(2) - assertThat(tokens12[3]).isMinusSymbol() - assertThat(tokens12[4]).isPositiveIntegerWhoseValue().isEqualTo(3) - assertThat(tokens12[5]).isPlusSymbol() - assertThat(tokens12[6]).isPositiveIntegerWhoseValue().isEqualTo(4) - assertThat(tokens12[7]).isExponentiationSymbol() - assertThat(tokens12[8]).isPositiveIntegerWhoseValue().isEqualTo(7) - assertThat(tokens12[9]).isMinusSymbol() - assertThat(tokens12[10]).isPositiveIntegerWhoseValue().isEqualTo(8) - assertThat(tokens12[11]).isDivideSymbol() - assertThat(tokens12[12]).isPositiveIntegerWhoseValue().isEqualTo(3) - assertThat(tokens12[13]).isMultiplySymbol() - assertThat(tokens12[14]).isPositiveIntegerWhoseValue().isEqualTo(2) - assertThat(tokens12[15]).isPlusSymbol() - assertThat(tokens12[16]).isPositiveIntegerWhoseValue().isEqualTo(7) - - val tokens13 = MathTokenizer.tokenize("x = √2 × 7 ÷ 4").toList() - assertThat(tokens13).hasSize(8) - assertThat(tokens13[0]).isVariableWhoseName().isEqualTo("x") - assertThat(tokens13[1]).isEqualsSymbol() - assertThat(tokens13[2]).isSquareRootSymbol() - assertThat(tokens13[3]).isPositiveIntegerWhoseValue().isEqualTo(2) - assertThat(tokens13[4]).isMultiplySymbol() - assertThat(tokens13[5]).isPositiveIntegerWhoseValue().isEqualTo(7) - assertThat(tokens13[6]).isDivideSymbol() - assertThat(tokens13[7]).isPositiveIntegerWhoseValue().isEqualTo(4) - } - - private class TokenSubject( - metadata: FailureMetadata, - private val actual: T - ) : Subject(metadata, actual) { - fun isPositiveIntegerWhoseValue(): IntegerSubject { - return assertThat(actual.asVerifiedType().parsedValue) - } + fun testTokenize_emptyString_producesNoTokens() { + val tokens = MathTokenizer.tokenize("").toList() - fun isPositiveRealNumberWhoseValue(): DoubleSubject { - return assertThat(actual.asVerifiedType().parsedValue) - } + assertThat(tokens).isEmpty() + } - fun isVariableWhoseName(): StringSubject { - return assertThat(actual.asVerifiedType().parsedName) - } + @Test + fun testTokenize_onlyWhitespace_producesNoTokens() { + val tokens = MathTokenizer.tokenize(" ").toList() - fun isFunctionWhoseName(): StringSubject { - return assertThat(actual.asVerifiedType().parsedName) - } + assertThat(tokens).isEmpty() + } - fun isMinusSymbol() { - actual.asVerifiedType() - } + @Test + fun testTokenize_singleDigit_producesPositiveIntegerToken() { + val tokens = MathTokenizer.tokenize("1").toList() - fun isSquareRootSymbol() { - actual.asVerifiedType() - } + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).isPositiveIntegerWhoseValue().isEqualTo(1) + } - fun isPlusSymbol() { - actual.asVerifiedType() - } + @Test + fun testTokenize_digits_producesPositiveIntegerToken() { + val tokens = MathTokenizer.tokenize("927").toList() - fun isMultiplySymbol() { - actual.asVerifiedType() - } + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).isPositiveIntegerWhoseValue().isEqualTo(927) + } - fun isDivideSymbol() { - actual.asVerifiedType() - } + @Test + fun testTokenize_digits_withSpaces_spacesAreIgnored() { + val tokens = MathTokenizer.tokenize(" 927 ").toList() - fun isExponentiationSymbol() { - actual.asVerifiedType() - } + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).isPositiveIntegerWhoseValue().isEqualTo(927) + } - fun isEqualsSymbol() { - actual.asVerifiedType() - } + @Test + fun testTokenize_positiveInteger_withSpaces_tokenHasCorrectIndices() { + val tokens = MathTokenizer.tokenize(" 927 ").toList() - fun isLeftParenthesisSymbol() { - actual.asVerifiedType() - } + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).hasStartIndexThat().isEqualTo(2) + assertThat(tokens[0]).hasEndIndexThat().isEqualTo(5) + } - fun isRightParenthesisSymbol() { - actual.asVerifiedType() - } + @Test + fun testTokenize_positiveInteger_veryLargeNumber_producesInvalidToken() { + val tokens = MathTokenizer.tokenize("9823190830924801923845").toList() - fun isInvalidToken() { - actual.asVerifiedType() - } + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).isInvalidToken() + } - fun isIncompleteFunctionName() { - actual.asVerifiedType() - } + @Test + fun testTokenize_decimal_producesInvalidToken() { + val tokens = MathTokenizer.tokenize(".").toList() + + // A decimal by itself is invalid. + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).isInvalidToken() + } + + @Test + fun testTokenize_digitsWithDecimal_producesInvalidToken() { + val tokens = MathTokenizer.tokenize("12.").toList() + + // The decimal is incomplete. Note that this is one token since the '12.' is considered a single + // invalid unit. + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).isInvalidToken() + } + + @Test + fun testTokenize_decimalWithDigits_producesInvalidToken() { + val tokens = MathTokenizer.tokenize(".34").toList() + + // The decimal is incomplete. Note that this results in 2 tokens since the '.' is encountered as + // an isolated and unexpected symbol. + assertThat(tokens).hasSize(2) + assertThat(tokens[0]).isInvalidToken() + } + + @Test + fun testTokenize_digitsWithDecimalWithDigits_producesPositiveRealNumberToken() { + val tokens = MathTokenizer.tokenize("12.34").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).isPositiveRealNumberWhoseValue().isWithin(1e-5).of(12.34) + } + + @Test + fun testTokenize_positiveRealNumber_withSpaces_tokenHasCorrectIndices() { + val tokens = MathTokenizer.tokenize(" 12.34 ").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).hasStartIndexThat().isEqualTo(2) + assertThat(tokens[0]).hasEndIndexThat().isEqualTo(7) + } + + @Test + fun testTokenize_positiveRealNumber_veryLargeNumber_producesInvalidToken() { + val tokens = MathTokenizer.tokenize("1${"0".repeat(400)}.12345").toList() + + // The number is too large to represent as a double (so it's treated as infinity). + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).isInvalidToken() + } + + @Test + fun testTokenize_variable_singleLetter_producesVariableToken() { + val tokens = MathTokenizer.tokenize("x").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).isVariableWhoseName().isEqualTo("x") + } + + @Test + fun testTokenize_variable_twoLetters_producesMultipleVariableTokens() { + val tokens = MathTokenizer.tokenize("xy").toList() + + assertThat(tokens).hasSize(2) + assertThat(tokens[0]).isVariableWhoseName().isEqualTo("x") + assertThat(tokens[1]).isVariableWhoseName().isEqualTo("y") + } + + @Test + fun testTokenize_variable_withSpaces_tokenHasCorrectIndices() { + val tokens = MathTokenizer.tokenize(" x ").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).hasStartIndexThat().isEqualTo(2) + assertThat(tokens[0]).hasEndIndexThat().isEqualTo(3) + } + + @Test + @RunParameterized( + Iteration("a", "variableName=a"), Iteration("A", "variableName=A"), + Iteration("b", "variableName=b"), Iteration("B", "variableName=B"), + Iteration("c", "variableName=c"), Iteration("C", "variableName=C"), + Iteration("d", "variableName=d"), Iteration("D", "variableName=D"), + Iteration("e", "variableName=e"), Iteration("E", "variableName=E"), + Iteration("f", "variableName=f"), Iteration("F", "variableName=F"), + Iteration("g", "variableName=g"), Iteration("G", "variableName=G"), + Iteration("h", "variableName=h"), Iteration("H", "variableName=H"), + Iteration("i", "variableName=i"), Iteration("I", "variableName=I"), + Iteration("j", "variableName=j"), Iteration("J", "variableName=J"), + Iteration("k", "variableName=k"), Iteration("K", "variableName=K"), + Iteration("l", "variableName=l"), Iteration("L", "variableName=L"), + Iteration("m", "variableName=m"), Iteration("M", "variableName=M"), + Iteration("n", "variableName=n"), Iteration("N", "variableName=N"), + Iteration("o", "variableName=o"), Iteration("O", "variableName=O"), + Iteration("p", "variableName=p"), Iteration("P", "variableName=P"), + Iteration("q", "variableName=q"), Iteration("Q", "variableName=Q"), + Iteration("r", "variableName=r"), Iteration("R", "variableName=R"), + Iteration("s", "variableName=s"), Iteration("S", "variableName=S"), + Iteration("t", "variableName=t"), Iteration("T", "variableName=T"), + Iteration("u", "variableName=u"), Iteration("U", "variableName=U"), + Iteration("v", "variableName=v"), Iteration("V", "variableName=V"), + Iteration("w", "variableName=w"), Iteration("W", "variableName=W"), + Iteration("x", "variableName=x"), Iteration("X", "variableName=X"), + Iteration("y", "variableName=y"), Iteration("Y", "variableName=Y"), + Iteration("z", "variableName=z"), Iteration("Z", "variableName=Z") + ) + fun testTokenize_variable_allLatinAlphabetCharactersAreValidVariables() { + val tokens = MathTokenizer.tokenize(variableName).toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).isVariableWhoseName().isEqualTo(variableName) + } + + @Test + fun testTokenize_sqrtFunction_producesAllowedFunctionNameToken() { + val tokens = MathTokenizer.tokenize("sqrt").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).isFunctionNameThat().hasNameThat().isEqualTo("sqrt") + assertThat(tokens[0]).isFunctionNameThat().hasIsAllowedPropertyThat().isTrue() + } + + @Test + fun testTokenize_sqrtFunction_withSpaces_tokenHasCorrectIndices() { + val tokens = MathTokenizer.tokenize(" sqrt ").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).hasStartIndexThat().isEqualTo(2) + assertThat(tokens[0]).hasEndIndexThat().isEqualTo(6) + } + + @Test + fun testTokenize_expFunction_producesDisallowedFunctionNameToken() { + val tokens = MathTokenizer.tokenize("exp").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).isFunctionNameThat().hasNameThat().isEqualTo("exp") + assertThat(tokens[0]).isFunctionNameThat().hasIsAllowedPropertyThat().isFalse() + } + + @Test + fun testTokenize_logFunction_producesDisallowedFunctionNameToken() { + val tokens = MathTokenizer.tokenize("log").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).isFunctionNameThat().hasNameThat().isEqualTo("log") + assertThat(tokens[0]).isFunctionNameThat().hasIsAllowedPropertyThat().isFalse() + } + + @Test + fun testTokenize_log10Function_producesDisallowedFunctionNameToken() { + val tokens = MathTokenizer.tokenize("log10").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).isFunctionNameThat().hasNameThat().isEqualTo("log10") + assertThat(tokens[0]).isFunctionNameThat().hasIsAllowedPropertyThat().isFalse() + } + + @Test + fun testTokenize_lnFunction_producesDisallowedFunctionNameToken() { + val tokens = MathTokenizer.tokenize("ln").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).isFunctionNameThat().hasNameThat().isEqualTo("ln") + assertThat(tokens[0]).isFunctionNameThat().hasIsAllowedPropertyThat().isFalse() + } + + @Test + fun testTokenize_sinFunction_producesDisallowedFunctionNameToken() { + val tokens = MathTokenizer.tokenize("sin").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).isFunctionNameThat().hasNameThat().isEqualTo("sin") + assertThat(tokens[0]).isFunctionNameThat().hasIsAllowedPropertyThat().isFalse() + } + + @Test + fun testTokenize_cosFunction_producesDisallowedFunctionNameToken() { + val tokens = MathTokenizer.tokenize("cos").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).isFunctionNameThat().hasNameThat().isEqualTo("cos") + assertThat(tokens[0]).isFunctionNameThat().hasIsAllowedPropertyThat().isFalse() + } + + @Test + fun testTokenize_tanFunction_producesDisallowedFunctionNameToken() { + val tokens = MathTokenizer.tokenize("tan").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).isFunctionNameThat().hasNameThat().isEqualTo("tan") + assertThat(tokens[0]).isFunctionNameThat().hasIsAllowedPropertyThat().isFalse() + } + + @Test + fun testTokenize_cotFunction_producesDisallowedFunctionNameToken() { + val tokens = MathTokenizer.tokenize("cot").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).isFunctionNameThat().hasNameThat().isEqualTo("cot") + assertThat(tokens[0]).isFunctionNameThat().hasIsAllowedPropertyThat().isFalse() + } + + @Test + fun testTokenize_cscFunction_producesDisallowedFunctionNameToken() { + val tokens = MathTokenizer.tokenize("csc").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).isFunctionNameThat().hasNameThat().isEqualTo("csc") + assertThat(tokens[0]).isFunctionNameThat().hasIsAllowedPropertyThat().isFalse() + } + + @Test + fun testTokenize_secFunction_producesDisallowedFunctionNameToken() { + val tokens = MathTokenizer.tokenize("sec").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).isFunctionNameThat().hasNameThat().isEqualTo("sec") + assertThat(tokens[0]).isFunctionNameThat().hasIsAllowedPropertyThat().isFalse() + } + + @Test + fun testTokenize_atanFunction_producesDisallowedFunctionNameToken() { + val tokens = MathTokenizer.tokenize("atan").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).isFunctionNameThat().hasNameThat().isEqualTo("atan") + assertThat(tokens[0]).isFunctionNameThat().hasIsAllowedPropertyThat().isFalse() + } + + @Test + fun testTokenize_asinFunction_producesDisallowedFunctionNameToken() { + val tokens = MathTokenizer.tokenize("asin").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).isFunctionNameThat().hasNameThat().isEqualTo("asin") + assertThat(tokens[0]).isFunctionNameThat().hasIsAllowedPropertyThat().isFalse() + } + + @Test + fun testTokenize_acosFunction_producesDisallowedFunctionNameToken() { + val tokens = MathTokenizer.tokenize("acos").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).isFunctionNameThat().hasNameThat().isEqualTo("acos") + assertThat(tokens[0]).isFunctionNameThat().hasIsAllowedPropertyThat().isFalse() + } + + @Test + fun testTokenize_absFunction_producesDisallowedFunctionNameToken() { + val tokens = MathTokenizer.tokenize("abs").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).isFunctionNameThat().hasNameThat().isEqualTo("abs") + assertThat(tokens[0]).isFunctionNameThat().hasIsAllowedPropertyThat().isFalse() + } + + @Test + fun testTokenize_squareRoot_producesSquareRootSymbol() { + val tokens = MathTokenizer.tokenize("√").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).isSquareRootSymbol() + } + + @Test + fun testTokenize_squareRootSymbol_withSpaces_tokenHasCorrectIndices() { + val tokens = MathTokenizer.tokenize(" √ ").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).hasStartIndexThat().isEqualTo(2) + assertThat(tokens[0]).hasEndIndexThat().isEqualTo(3) + } + + @Test + fun testTokenize_hyphen_producesMinusSymbol() { + val tokens = MathTokenizer.tokenize("-").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).isMinusSymbol() + } + + @Test + fun testTokenize_mathMinusSymbol_producesMinusSymbol() { + val tokens = MathTokenizer.tokenize("−").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).isMinusSymbol() + } + + @Test + fun testTokenize_minusSymbol_withSpaces_tokenHasCorrectIndices() { + val tokens = MathTokenizer.tokenize(" − ").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).hasStartIndexThat().isEqualTo(2) + assertThat(tokens[0]).hasEndIndexThat().isEqualTo(3) + } + + @Test + fun testTokenize_plus_producesPlusSymbol() { + val tokens = MathTokenizer.tokenize("+").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).isPlusSymbol() + } + + @Test + fun testTokenize_plusSymbol_withSpaces_tokenHasCorrectIndices() { + val tokens = MathTokenizer.tokenize(" + ").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).hasStartIndexThat().isEqualTo(2) + assertThat(tokens[0]).hasEndIndexThat().isEqualTo(3) + } + + @Test + fun testTokenize_asterisk_producesMultiplySymbol() { + val tokens = MathTokenizer.tokenize("*").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).isMultiplySymbol() + } + + @Test + fun testTokenize_mathTimesSymbol_producesMultiplySymbol() { + val tokens = MathTokenizer.tokenize("×").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).isMultiplySymbol() + } + + @Test + fun testTokenize_multiplySymbol_withSpaces_tokenHasCorrectIndices() { + val tokens = MathTokenizer.tokenize(" × ").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).hasStartIndexThat().isEqualTo(2) + assertThat(tokens[0]).hasEndIndexThat().isEqualTo(3) + } + + @Test + fun testTokenize_forwardSlash_producesDivideSymbol() { + val tokens = MathTokenizer.tokenize("/").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).isDivideSymbol() + } + + @Test + fun testTokenize_mathDivideSymbol_producesDivideSymbol() { + val tokens = MathTokenizer.tokenize("÷").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).isDivideSymbol() + } + + @Test + fun testTokenize_divideSymbol_withSpaces_tokenHasCorrectIndices() { + val tokens = MathTokenizer.tokenize(" ÷ ").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).hasStartIndexThat().isEqualTo(2) + assertThat(tokens[0]).hasEndIndexThat().isEqualTo(3) + } + + @Test + fun testTokenize_caret_producesExponentiationSymbol() { + val tokens = MathTokenizer.tokenize("^").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).isExponentiationSymbol() + } + + @Test + fun testTokenize_exponentiationSymbol_withSpaces_tokenHasCorrectIndices() { + val tokens = MathTokenizer.tokenize(" ^ ").toList() - private companion object { - private inline fun Token.asVerifiedType(): T { - assertThat(this).isInstanceOf(T::class.java) - return this as T - } + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).hasStartIndexThat().isEqualTo(2) + assertThat(tokens[0]).hasEndIndexThat().isEqualTo(3) + } + + @Test + fun testTokenize_equals_producesEqualsSymbol() { + val tokens = MathTokenizer.tokenize("=").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).isEqualsSymbol() + } + + @Test + fun testTokenize_equalsSymbol_withSpaces_tokenHasCorrectIndices() { + val tokens = MathTokenizer.tokenize(" = ").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).hasStartIndexThat().isEqualTo(2) + assertThat(tokens[0]).hasEndIndexThat().isEqualTo(3) + } + + @Test + fun testTokenize_leftParenthesis_producesLeftParenthesisSymbol() { + val tokens = MathTokenizer.tokenize("(").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).isLeftParenthesisSymbol() + } + + @Test + fun testTokenize_leftParenthesisSymbol_withSpaces_tokenHasCorrectIndices() { + val tokens = MathTokenizer.tokenize(" ( ").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).hasStartIndexThat().isEqualTo(2) + assertThat(tokens[0]).hasEndIndexThat().isEqualTo(3) + } + + @Test + fun testTokenize_rightParenthesis_producesRightParenthesisSymbol() { + val tokens = MathTokenizer.tokenize(")").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).isRightParenthesisSymbol() + } + + @Test + fun testTokenize_rightParenthesisSymbol_withSpaces_tokenHasCorrectIndices() { + val tokens = MathTokenizer.tokenize(" ) ").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).hasStartIndexThat().isEqualTo(2) + assertThat(tokens[0]).hasEndIndexThat().isEqualTo(3) + } + + @Test + fun testTokenize_firstLetterOfFunctionNameOnly_shouldParseAsVariable() { + val tokens = MathTokenizer.tokenize("a").toList() + + // Although there are functions starting with 'a', 'a' by itself is a variable name since + // there's no context to indicate that it's part of a function name. + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).isVariableWhoseName().isEqualTo("a") + } + + @Test + @RunParameterized( + Iteration("aa", "funcName=aa"), Iteration("ad", "funcName=ad"), Iteration("al", "funcName=al"), + Iteration("ca", "funcName=ca"), Iteration("ce", "funcName=ce"), Iteration("cr", "funcName=cr"), + Iteration("ea", "funcName=ea"), Iteration("ef", "funcName=ef"), Iteration("er", "funcName=er"), + Iteration("la", "funcName=la"), Iteration("lz", "funcName=lz"), Iteration("le", "funcName=le"), + Iteration("sa", "funcName=sa"), Iteration("sp", "funcName=sp"), Iteration("sz", "funcName=sz"), + Iteration("te", "funcName=te"), Iteration("to", "funcName=to"), Iteration("tr", "funcName=tr") + ) + fun testTokenize_twoVarsSharingOnlyFirstWithFunctionNames_shouldParseAsVariables() { + val tokens = MathTokenizer.tokenize(funcName).toList() + + // This test covers many cases where the first letter can be shared with function names without + // triggering a failure. Note that it doesn't cover all cases for simplicity. + assertThat(tokens).hasSize(2) + assertThat(tokens[0]).isVariableWhoseName().isNotEmpty() + assertThat(tokens[1]).isVariableWhoseName().isNotEmpty() + } + + @Test + @RunParameterized( + Iteration("ab", "funcName=ab"), Iteration("ac", "funcName=ac"), + Iteration("aco", "funcName=aco"), Iteration("as", "funcName=as"), + Iteration("asi", "funcName=asi"), Iteration("at", "funcName=at"), + Iteration("ata", "funcName=ata"), Iteration("co", "funcName=co"), + Iteration("cs", "funcName=cs"), Iteration("ex", "funcName=ex"), + Iteration("lo", "funcName=lo"), Iteration("log1", "funcName=log1"), + Iteration("se", "funcName=se"), Iteration("si", "funcName=si"), + Iteration("sq", "funcName=sq"), Iteration("ta", "funcName=ta") + ) + fun testTokenize_twoVarsSharedWithFunctionNames_shouldParseAsIncompleteFuncName() { + val tokens = MathTokenizer.tokenize(funcName).toList() + + // This test covers all cases where sharing the first few letters of a function name triggers a + // failure due to the grammar being limited to LL(1) parsing. + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).isIncompleteFunctionName() + } + + @Test + fun testTokenize_sqrtWithCapitalLetters_isInterpretedAsVariables() { + val tokens = MathTokenizer.tokenize("Sqrt").toList() + + // Function names can't be capitalized, so 'Sqrt' is treated as 4 consecutive variables. + assertThat(tokens).hasSize(4) + assertThat(tokens[0]).isVariableWhoseName().isEqualTo("S") + assertThat(tokens[1]).isVariableWhoseName().isEqualTo("q") + assertThat(tokens[2]).isVariableWhoseName().isEqualTo("r") + assertThat(tokens[3]).isVariableWhoseName().isEqualTo("t") + } + + @Test + fun testTokenize_sqrtWithSpaces_isInterpretedAsVariables() { + val tokens = MathTokenizer.tokenize("s qrt").toList() + + // Spaces break the function name, so the letters must be variables. + assertThat(tokens).hasSize(4) + assertThat(tokens[0]).isVariableWhoseName().isEqualTo("s") + assertThat(tokens[1]).isVariableWhoseName().isEqualTo("q") + assertThat(tokens[2]).isVariableWhoseName().isEqualTo("r") + assertThat(tokens[3]).isVariableWhoseName().isEqualTo("t") + } + + @Test + fun testTokenize_sameTokenTwice_parsesTwice() { + val tokens = MathTokenizer.tokenize("aa").toList() + + assertThat(tokens).hasSize(2) + assertThat(tokens[0]).isVariableWhoseName().isEqualTo("a") + assertThat(tokens[1]).isVariableWhoseName().isEqualTo("a") + } + + @Test + fun testTokenize_exclamationPoint_producesInvalidToken() { + val tokens = MathTokenizer.tokenize("!").toList() + + // '!' is not yet supported by the tokenizer, so it's an invalid token. + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).isInvalidToken() + } + + @Test + @RunParameterized( + Iteration("α", "token=α"), Iteration("Α", "token=Α"), + Iteration("β", "token=β"), Iteration("Β", "token=Β"), + Iteration("γ", "token=γ"), Iteration("Γ", "token=Γ"), + Iteration("δ", "token=δ"), Iteration("Δ", "token=Δ"), + Iteration("ϵ", "token=ϵ"), Iteration("Ε", "token=Ε"), + Iteration("ζ", "token=ζ"), Iteration("Ζ", "token=Ζ"), + Iteration("η", "token=η"), Iteration("Η", "token=Η"), + Iteration("θ", "token=θ"), Iteration("Θ", "token=Θ"), + Iteration("ι", "token=ι"), Iteration("Ι", "token=Ι"), + Iteration("κ", "token=κ"), Iteration("Κ", "token=Κ"), + Iteration("λ", "token=λ"), Iteration("Λ", "token=Λ"), + Iteration("μ", "token=μ"), Iteration("Μ", "token=Μ"), + Iteration("ν", "token=ν"), Iteration("Ν", "token=Ν"), + Iteration("ξ", "token=ξ"), Iteration("Ξ", "token=Ξ"), + Iteration("ο", "token=ο"), Iteration("Ο", "token=Ο"), + Iteration("π", "token=π"), Iteration("Π", "token=Π"), + Iteration("ρ", "token=ρ"), Iteration("Ρ", "token=Ρ"), + Iteration("σ", "token=σ"), Iteration("Σ", "token=Σ"), + Iteration("τ", "token=τ"), Iteration("Τ", "token=Τ"), + Iteration("υ", "token=υ"), Iteration("Υ", "token=Υ"), + Iteration("ϕ", "token=ϕ"), Iteration("Φ", "token=Φ"), + Iteration("χ", "token=χ"), Iteration("Χ", "token=Χ"), + Iteration("ψ", "token=ψ"), Iteration("Ψ", "token=Ψ"), + Iteration("ω", "token=ω"), Iteration("Ω", "token=Ω"), + Iteration("ς", "token=ς") + ) + fun testTokenize_greekLetters_produceInvalidTokens() { + val tokens = MathTokenizer.tokenize(token).toList() + + // Greek letters are not yet supported. + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).isInvalidToken() + } + + @Test + fun testTokenize_manyOtherUnicodeValues_produceInvalidTokens() { + // Build a large list of unicode characters minus those which are actually allowed. The ASCII + // range is excluded from this list. + val characters = ('\u007f' .. '\uffff').filterNot { + it in listOf('×', '÷', '−', '√') } + val charStr = characters.joinToString("") + + val tokens = MathTokenizer.tokenize(charStr).toList() + + // Verify that all of the unicode characters cover in this range are invalid. + assertThat(tokens).hasSize(charStr.length) + tokens.forEach { assertThat(it).isInvalidToken() } + // Sanity check to ensure that the tokens are actually populated. + assertThat(tokens.size).isGreaterThan(0x7fff) } - private companion object { - private fun assertThat(actual: T): TokenSubject = - assertAbout(createTokenSubjectFactory()).that(actual) + @Test + fun testTokenize_validAndInvalidTokens_tokenizerContinues() { + val tokens = MathTokenizer.tokenize("2*7!/|-9|").toList() + + assertThat(tokens).hasSize(9) + assertThat(tokens[0]).isPositiveIntegerWhoseValue().isEqualTo(2) + assertThat(tokens[1]).isMultiplySymbol() + assertThat(tokens[2]).isPositiveIntegerWhoseValue().isEqualTo(7) + assertThat(tokens[3]).isInvalidToken() + assertThat(tokens[4]).isDivideSymbol() + assertThat(tokens[5]).isInvalidToken() + assertThat(tokens[6]).isMinusSymbol() + assertThat(tokens[7]).isPositiveIntegerWhoseValue().isEqualTo(9) + assertThat(tokens[8]).isInvalidToken() + } + + @Test + fun testTokenize_manyTokenTypes_parseCorrectlyAndInOrder() { + val tokens = MathTokenizer.tokenize("1 * (√2 - 3.14) + 4^7-8/3×-2 + sqrt(7)÷3").toList() + + assertThat(tokens).hasSize(26) + assertThat(tokens[0]).isPositiveIntegerWhoseValue().isEqualTo(1) + assertThat(tokens[1]).isMultiplySymbol() + assertThat(tokens[2]).isLeftParenthesisSymbol() + assertThat(tokens[3]).isSquareRootSymbol() + assertThat(tokens[4]).isPositiveIntegerWhoseValue().isEqualTo(2) + assertThat(tokens[5]).isMinusSymbol() + assertThat(tokens[6]).isPositiveRealNumberWhoseValue().isWithin(1e-5).of(3.14) + assertThat(tokens[7]).isRightParenthesisSymbol() + assertThat(tokens[8]).isPlusSymbol() + assertThat(tokens[9]).isPositiveIntegerWhoseValue().isEqualTo(4) + assertThat(tokens[10]).isExponentiationSymbol() + assertThat(tokens[11]).isPositiveIntegerWhoseValue().isEqualTo(7) + assertThat(tokens[12]).isMinusSymbol() + assertThat(tokens[13]).isPositiveIntegerWhoseValue().isEqualTo(8) + assertThat(tokens[14]).isDivideSymbol() + assertThat(tokens[15]).isPositiveIntegerWhoseValue().isEqualTo(3) + assertThat(tokens[16]).isMultiplySymbol() + assertThat(tokens[17]).isMinusSymbol() + assertThat(tokens[18]).isPositiveIntegerWhoseValue().isEqualTo(2) + assertThat(tokens[19]).isPlusSymbol() + assertThat(tokens[20]).isFunctionNameThat().hasNameThat().isEqualTo("sqrt") + assertThat(tokens[21]).isLeftParenthesisSymbol() + assertThat(tokens[22]).isPositiveIntegerWhoseValue().isEqualTo(7) + assertThat(tokens[23]).isRightParenthesisSymbol() + assertThat(tokens[24]).isDivideSymbol() + assertThat(tokens[25]).isPositiveIntegerWhoseValue().isEqualTo(3) + } + + @Test + fun testTokenize_allFormsOfWhiteSpaceAreIgnored() { + val tokens = MathTokenizer.tokenize(" \n\t2\r\n 3 \n").toList() - private fun createTokenSubjectFactory() = - Subject.Factory, T>(::TokenSubject) + assertThat(tokens).hasSize(2) + assertThat(tokens[0]).isPositiveIntegerWhoseValue().isEqualTo(2) + assertThat(tokens[1]).isPositiveIntegerWhoseValue().isEqualTo(3) } } diff --git a/utility/src/test/java/org/oppia/android/util/math/PeekableIteratorTest.kt b/utility/src/test/java/org/oppia/android/util/math/PeekableIteratorTest.kt new file mode 100644 index 00000000000..96da81d6baa --- /dev/null +++ b/utility/src/test/java/org/oppia/android/util/math/PeekableIteratorTest.kt @@ -0,0 +1,600 @@ +package org.oppia.android.util.math + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import java.util.function.Supplier +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.`when` +import org.mockito.Mockito.reset +import org.mockito.Mockito.times +import org.mockito.Mockito.verify +import org.mockito.Mockito.verifyNoMoreInteractions +import org.mockito.junit.MockitoJUnit +import org.mockito.junit.MockitoRule +import org.mockito.stubbing.Answer +import org.oppia.android.testing.assertThrows +import org.oppia.android.util.math.PeekableIterator.Companion.toPeekableIterator +import org.robolectric.annotation.LooperMode + +/** Tests for [PeekableIterator]. */ +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") +@RunWith(AndroidJUnit4::class) +@LooperMode(LooperMode.Mode.PAUSED) +class PeekableIteratorTest { + @Rule + @JvmField + val mockitoRule: MockitoRule = MockitoJUnit.rule() + + @Mock lateinit var mockSequenceSupplier: Supplier + + @Test + fun testHasNext_emptySequence_returnsFalse() { + val sequence = sequenceOf() + val iterator = sequence.toPeekableIterator() + + val hasNext = iterator.hasNext() + + assertThat(hasNext).isFalse() + } + + @Test + fun testHasNext_singletonSequence_returnsTrue() { + val sequence = sequenceOf("element") + val iterator = sequence.toPeekableIterator() + + val hasNext = iterator.hasNext() + + assertThat(hasNext).isTrue() + } + + @Test + fun testNext_emptySequence_throwsException() { + val sequence = sequenceOf() + val iterator = sequence.toPeekableIterator() + + assertThrows(NoSuchElementException::class) { iterator.next() } + } + + @Test + fun testNext_singletonSequence_returnsValue() { + val sequence = sequenceOf("element") + val iterator = sequence.toPeekableIterator() + + val value = iterator.next() + + assertThat(value).isEqualTo("element") + } + + @Test + fun testNext_multipleCalls_multiElementSequence_returnsAllValues() { + val sequence = sequenceOf("first", "second", "third") + val iterator = sequence.toPeekableIterator() + + val value1 = iterator.next() + val value2 = iterator.next() + val value3 = iterator.next() + + assertThat(value1).isEqualTo("first") + assertThat(value2).isEqualTo("second") + assertThat(value3).isEqualTo("third") + } + + @Test + fun testHasNext_singletonSequence_afterNext_returnsFalse() { + val sequence = sequenceOf("element") + val iterator = sequence.toPeekableIterator() + iterator.next() + + val hasNext = iterator.hasNext() + + assertThat(hasNext).isFalse() + } + + @Test + fun testHasNext_multiElementSequence_afterNext_returnsTrue() { + val sequence = sequenceOf("first", "second", "third") + val iterator = sequence.toPeekableIterator() + iterator.next() + + val hasNext = iterator.hasNext() + + assertThat(hasNext).isTrue() + } + + @Test + fun testAsIterator_emptySequence_convertsToEmptyList() { + val sequence = sequenceOf() + val iterator = sequence.toPeekableIterator() + + val list = iterator.toList() + + assertThat(list).isEmpty() + } + + @Test + fun testAsIterator_singletonSequence_convertsToSingletonList() { + val sequence = sequenceOf("element") + val iterator = sequence.toPeekableIterator() + + val list = iterator.toList() + + assertThat(list).containsExactly("element") + } + + @Test + fun testAsIterator_multiElementSequence_convertsToList() { + val sequence = sequenceOf("first", "second", "third") + val iterator = sequence.toPeekableIterator() + + val list = iterator.toList() + + assertThat(list).containsExactly("first", "second", "third").inOrder() + } + + @Test + fun testHasNext_multiElementSequence_convertedToList_returnsFalse() { + val sequence = sequenceOf("first", "second", "third") + val iterator = sequence.toPeekableIterator() + iterator.toList() + + val hasNext = iterator.hasNext() + + // No elements remain after converting the iterator to a list (since it should be fully + // consumed). + assertThat(hasNext).isFalse() + } + + @Test + fun testPeek_emptySequence_returnsNull() { + val sequence = sequenceOf() + val iterator = sequence.toPeekableIterator() + + val nextElement = iterator.peek() + + assertThat(nextElement).isNull() + } + + @Test + fun testPeek_emptySequence_twice_returnsNull() { + val sequence = sequenceOf() + val iterator = sequence.toPeekableIterator() + iterator.peek() + + // Peek a second time. + val nextElement = iterator.peek() + + assertThat(nextElement).isNull() + } + + @Test + fun testPeek_singletonSequence_returnsElement() { + val sequence = sequenceOf("element") + val iterator = sequence.toPeekableIterator() + + val nextElement = iterator.peek() + + assertThat(nextElement).isEqualTo("element") + } + + @Test + fun testPeek_singletonSequence_twice_returnsElement() { + val sequence = sequenceOf("element") + val iterator = sequence.toPeekableIterator() + iterator.peek() + + // Peek a second time. + val nextElement = iterator.peek() + + assertThat(nextElement).isEqualTo("element") + } + + @Test + fun testPeek_singletonSequence_afterNext_returnsNull() { + val sequence = sequenceOf("element") + val iterator = sequence.toPeekableIterator() + iterator.next() + + val nextElement = iterator.peek() + + // There is no longer a next element since it was consumed. + assertThat(nextElement).isNull() + } + + @Test + fun testPeek_singletonSequence_afterNext_twice_returnsNull() { + val sequence = sequenceOf("element") + val iterator = sequence.toPeekableIterator() + iterator.next() + iterator.peek() + + // Peek a second time after consuming the element. + val nextElement = iterator.peek() + + // It's still missing. + assertThat(nextElement).isNull() + } + + @Test + fun testPeek_singletonSequence_peekThenNext_returnsElementFromBoth() { + val sequence = sequenceOf("element") + val iterator = sequence.toPeekableIterator() + + val nextElement = iterator.peek() + val consumedElement = iterator.next() + + // Both functions should return the same value in this order. + assertThat(nextElement).isEqualTo("element") + assertThat(consumedElement).isEqualTo("element") + } + + @Test + fun testExpectNextValue_emptySequence_returnsNull() { + val sequence = sequenceOf() + val iterator = sequence.toPeekableIterator() + + val matchedValue = iterator.expectNextValue { "does not match" } + + // No values to match. + assertThat(matchedValue).isNull() + } + + @Test + fun testExpectNextValue_valueMatches_returnsValue() { + val sequence = sequenceOf("matches") + val iterator = sequence.toPeekableIterator() + + val matchedValue = iterator.expectNextValue { "matches" } + + assertThat(matchedValue).isEqualTo("matches") + } + + @Test + fun testHasNext_afterExpectNextValue_valueMatches_returnsFalse() { + val sequence = sequenceOf("matches") + val iterator = sequence.toPeekableIterator() + iterator.expectNextValue { "matches" } + + val hasNext = iterator.hasNext() + + // No other elements since the only one was consumed. + assertThat(hasNext).isFalse() + } + + @Test + fun testPeek_afterExpectNextValue_valueMatches_returnsNull() { + val sequence = sequenceOf("matches") + val iterator = sequence.toPeekableIterator() + iterator.expectNextValue { "matches" } + + val nextElement = iterator.peek() + + // No other elements since the only one was consumed. + assertThat(nextElement).isNull() + } + + @Test + fun testExpectNextValue_valueDoesNotMatch_returnsNull() { + val sequence = sequenceOf("matches") + val iterator = sequence.toPeekableIterator() + + val matchedValue = iterator.expectNextValue { "does not match" } + + // No values to match. + assertThat(matchedValue).isNull() + } + + @Test + fun testHasNext_afterExpectNextValue_valueDoesNotMatch_returnsTrue() { + val sequence = sequenceOf("matches") + val iterator = sequence.toPeekableIterator() + iterator.expectNextValue { "does not match" } + + val hasNext = iterator.hasNext() + + // The element is still present. + assertThat(hasNext).isTrue() + } + + @Test + fun testPeek_afterExpectNextValue_valueDoesNotMatch_returnsElement() { + val sequence = sequenceOf("matches") + val iterator = sequence.toPeekableIterator() + iterator.expectNextValue { "does not match" } + + val nextElement = iterator.next() + + // The element is still present. + assertThat(nextElement).isEqualTo("matches") + } + + @Test + fun testExpectNextMatches_emptySequence_returnsNull() { + val sequence = sequenceOf() + val iterator = sequence.toPeekableIterator() + + val matchedValue = iterator.expectNextMatches { true } + + // No values to match. + assertThat(matchedValue).isNull() + } + + @Test + fun testExpectNextMatches_valueMatches_returnsValue() { + val sequence = sequenceOf("matches") + val iterator = sequence.toPeekableIterator() + + val matchedValue = iterator.expectNextMatches { true } + + assertThat(matchedValue).isEqualTo("matches") + } + + @Test + fun testHasNext_afterExpectNextMatches_valueMatches_returnsFalse() { + val sequence = sequenceOf("matches") + val iterator = sequence.toPeekableIterator() + iterator.expectNextMatches { true } + + val hasNext = iterator.hasNext() + + // No other elements since the only one was consumed. + assertThat(hasNext).isFalse() + } + + @Test + fun testPeek_afterExpectNextMatches_valueMatches_returnsNull() { + val sequence = sequenceOf("matches") + val iterator = sequence.toPeekableIterator() + iterator.expectNextMatches { true } + + val nextElement = iterator.peek() + + // No other elements since the only one was consumed. + assertThat(nextElement).isNull() + } + + @Test + fun testExpectNextMatches_valueDoesNotMatch_returnsNull() { + val sequence = sequenceOf("matches") + val iterator = sequence.toPeekableIterator() + + val matchedValue = iterator.expectNextMatches { false } + + // The predicate didn't match. + assertThat(matchedValue).isNull() + } + + @Test + fun testHasNext_afterExpectNextMatches_valueDoesNotMatch_returnsTrue() { + val sequence = sequenceOf("matches") + val iterator = sequence.toPeekableIterator() + iterator.expectNextMatches { false } + + val hasNext = iterator.hasNext() + + // The element is still present. + assertThat(hasNext).isTrue() + } + + @Test + fun testPeek_afterExpectNextMatches_valueDoesNotMatch_returnsElement() { + val sequence = sequenceOf("matches") + val iterator = sequence.toPeekableIterator() + iterator.expectNextMatches { false } + + val nextElement = iterator.peek() + + // The element is still present. + assertThat(nextElement).isEqualTo("matches") + } + + @Test + fun testGetRetrievalCount_emptySequence_returnsZero() { + val sequence = sequenceOf() + val iterator = sequence.toPeekableIterator() + + val retrievalCount = iterator.getRetrievalCount() + + assertThat(retrievalCount).isEqualTo(0) + } + + @Test + fun testGetRetrievalCount_singletonSequence_returnsZero() { + val sequence = sequenceOf("element") + val iterator = sequence.toPeekableIterator() + + val retrievalCount = iterator.getRetrievalCount() + + assertThat(retrievalCount).isEqualTo(0) + } + + @Test + fun testGetRetrievalCount_afterNext_singletonSequence_returnsOne() { + val sequence = sequenceOf("element") + val iterator = sequence.toPeekableIterator() + iterator.next() + + val retrievalCount = iterator.getRetrievalCount() + + // One element was removed. + assertThat(retrievalCount).isEqualTo(1) + } + + @Test + fun testGetRetrievalCount_afterPeek_singletonSequence_returnsZero() { + val sequence = sequenceOf("element") + val iterator = sequence.toPeekableIterator() + iterator.peek() + + val retrievalCount = iterator.getRetrievalCount() + + // Peek does not remove the element. + assertThat(retrievalCount).isEqualTo(0) + } + + @Test + fun testGetRetrievalCount_afterMatchingExpectNextValue_returnsOne() { + val sequence = sequenceOf("matches") + val iterator = sequence.toPeekableIterator() + iterator.expectNextValue { "matches" } + + val retrievalCount = iterator.getRetrievalCount() + + // One element was removed due to the match. + assertThat(retrievalCount).isEqualTo(1) + } + + @Test + fun testGetRetrievalCount_afterFailingExpectNextValue_returnsZero() { + val sequence = sequenceOf("matches") + val iterator = sequence.toPeekableIterator() + iterator.expectNextValue { "does not match" } + + val retrievalCount = iterator.getRetrievalCount() + + // No elements were removed since nothing matched. + assertThat(retrievalCount).isEqualTo(0) + } + + @Test + fun testGetRetrievalCount_afterMatchingExpectNextMatches_returnsOne() { + val sequence = sequenceOf("matches") + val iterator = sequence.toPeekableIterator() + iterator.expectNextMatches { true } + + val retrievalCount = iterator.getRetrievalCount() + + // One element was removed due to the match. + assertThat(retrievalCount).isEqualTo(1) + } + + @Test + fun testGetRetrievalCount_afterFailingExpectNextMatches_returnsZero() { + val sequence = sequenceOf("matches") + val iterator = sequence.toPeekableIterator() + iterator.expectNextMatches { false } + + val retrievalCount = iterator.getRetrievalCount() + + // No elements were removed since nothing matched. + assertThat(retrievalCount).isEqualTo(0) + } + + @Test + fun testGetRetrievalCount_afterMultipleNext_returnsNextCount() { + val sequence = sequenceOf("first", "second", "third") + val iterator = sequence.toPeekableIterator() + // Call next() twice. + iterator.next() + iterator.next() + + val retrievalCount = iterator.getRetrievalCount() + + // The number of consumed elements from the iterator should be returned. + assertThat(retrievalCount).isEqualTo(2) + } + + @Test + fun testGetRetrievalCount_afterConvertingToList_returnsListSize() { + val sequence = sequenceOf("first", "second", "third", "fourth", "fifth") + val iterator = sequence.toPeekableIterator() + val elements = iterator.toList() + + val retrievalCount = iterator.getRetrievalCount() + + // Since the iterator was fully consumed, the retrieval count should be the same as the list + // size. + assertThat(retrievalCount).isEqualTo(5) + assertThat(elements.size).isEqualTo(retrievalCount) + } + + @Test + fun testCreateIterator_doesNotConsumeElementsFromSequence() { + val generatedSequence = createGeneratingSequence() + + generatedSequence.toPeekableIterator() + + // The sequence is never called just upon iterator creation. + verifyNoMoreInteractions(mockSequenceSupplier) + } + + @Test + fun testPeek_consumesElementFromSequence() { + val iterator = createGeneratingSequence().toPeekableIterator() + + iterator.peek() + + // The first peek must consume one element in order to populate it. + verify(mockSequenceSupplier).get() + } + + @Test + fun testPeek_twice_doesNotConsumeAdditionalElementFromSequence() { + val iterator = createGeneratingSequence().toPeekableIterator() + iterator.peek() + reset(mockSequenceSupplier) + + // Peek a second time. + iterator.peek() + + // The second peek doesn't consume an element (since the iterator's contract is to never look + // more than 1 element ahead). + verifyNoMoreInteractions(mockSequenceSupplier) + } + + @Test + fun testNext_consumesOneElementFromSequence() { + val iterator = createGeneratingSequence().toPeekableIterator() + + iterator.next() + + // The sequence should have only one value retrieved due to the next() call. + verify(mockSequenceSupplier).get() + } + + @Test + fun testNext_twice_consumesTwoElementsFromSequence() { + val iterator = createGeneratingSequence().toPeekableIterator() + + // Iterate two items. + iterator.next() + iterator.next() + + // One value should be retrieved from the sequence for each next() call. + verify(mockSequenceSupplier, times(2)).get() + } + + @Test + fun testConvertToList_consumesAllElementsFromSequence() { + val generatedSequence = createGeneratingSequence() + val iterator = generatedSequence.toPeekableIterator() + + val list = iterator.toList() + + // The whole sequence should be consumed through the iterator when converting it to a list. Note + // the extra call to get() is for the final element that indicates the sequence has ended per + // generateSequence. + verify(mockSequenceSupplier, times(list.size + 1)).get() + assertThat(list).isNotEmpty() + } + + private fun createGeneratingSequence(): Sequence { + `when`(mockSequenceSupplier.get()).thenReturn("string0", "string1", "string2", "string3", null) + return generateSequence { mockSequenceSupplier.get() } + } + + private companion object { + /** + * Returns a [List] that contains all elements from the [Iterator] (i.e. the iterator is fully + * consumed). + */ + private fun Iterator.toList(): List { + return mutableListOf().apply { + this@toList.forEach(this::add) + } + } + } +} From 3fa8dadc59d9778955c3579dc8a88c5f39c6974e Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 20 Jan 2022 22:41:01 -0800 Subject: [PATCH 051/162] Lint fixes. This includes a fix for 'fun interface' not working with ktlint (see #4122). --- .../junit/OppiaParameterizedTestRunner.kt | 13 ++-- .../android/testing/junit/ParameterValue.kt | 75 +++++++++++++------ .../ParameterizedAndroidJUnit4ClassRunner.kt | 2 +- .../ParameterizedRobolectricTestRunner.kt | 4 +- .../junit/ParameterizedRunnerDelegate.kt | 2 +- .../oppia/android/util/math/MathTokenizer.kt | 2 +- .../android/util/math/PeekableIterator.kt | 4 +- .../android/util/math/MathTokenizerTest.kt | 2 +- .../android/util/math/PeekableIteratorTest.kt | 5 +- 9 files changed, 69 insertions(+), 40 deletions(-) diff --git a/testing/src/main/java/org/oppia/android/testing/junit/OppiaParameterizedTestRunner.kt b/testing/src/main/java/org/oppia/android/testing/junit/OppiaParameterizedTestRunner.kt index 3579bc95d5b..e237553a9fc 100644 --- a/testing/src/main/java/org/oppia/android/testing/junit/OppiaParameterizedTestRunner.kt +++ b/testing/src/main/java/org/oppia/android/testing/junit/OppiaParameterizedTestRunner.kt @@ -1,8 +1,5 @@ package org.oppia.android.testing.junit -import java.lang.annotation.Repeatable -import java.lang.reflect.Field -import java.lang.reflect.Method import org.junit.runner.Description import org.junit.runner.Runner import org.junit.runner.manipulation.Filter @@ -11,6 +8,9 @@ import org.junit.runner.manipulation.Sortable import org.junit.runner.manipulation.Sorter import org.junit.runner.notification.RunNotifier import org.junit.runners.Suite +import java.lang.annotation.Repeatable +import java.lang.reflect.Field +import java.lang.reflect.Method /** * JUnit test runner that enables support for parameterization, that is, running a single test @@ -71,7 +71,7 @@ import org.junit.runners.Suite * contain (thus they should be treated as undefined outside of tests that specific define their * value via [Iteration]). */ -class OppiaParameterizedTestRunner(private val testClass: Class<*>): Suite(testClass, listOf()) { +class OppiaParameterizedTestRunner(private val testClass: Class<*>) : Suite(testClass, listOf()) { private val parameterizedMethods = computeParameterizedMethods() private val childrenRunners by lazy { // Collect all parameterized methods (for each iteration they support) plus one test runner for @@ -239,7 +239,8 @@ class OppiaParameterizedTestRunner(private val testClass: Class<*>): Suite(testC annotation class Iteration(val name: String, vararg val keyValuePairs: String) private data class ParameterizedMethodDeclaration( - val method: Method, val rawValues: Map> + val method: Method, + val rawValues: Map> ) private class ProxyParameterizedTestRunner( @@ -247,7 +248,7 @@ class OppiaParameterizedTestRunner(private val testClass: Class<*>): Suite(testC private val parameterizedMethods: Map, private val methodName: String?, private val iterationName: String? = null - ): Runner(), Filterable, Sortable { + ) : Runner(), Filterable, Sortable { private val delegate by lazy { constructDelegate() } private val delegateRunner by lazy { checkNotNull(delegate as? Runner) { "Delegate runner isn't a JUnit runner: $delegate" } diff --git a/testing/src/main/java/org/oppia/android/testing/junit/ParameterValue.kt b/testing/src/main/java/org/oppia/android/testing/junit/ParameterValue.kt index 360a44649e0..fc113db4674 100644 --- a/testing/src/main/java/org/oppia/android/testing/junit/ParameterValue.kt +++ b/testing/src/main/java/org/oppia/android/testing/junit/ParameterValue.kt @@ -12,8 +12,9 @@ import java.lang.reflect.Field */ internal sealed class ParameterValue(val key: String, val value: Any) { private class BooleanParameterValue private constructor( - key: String, value: Boolean - ): ParameterValue(key, value) { + key: String, + value: Boolean + ) : ParameterValue(key, value) { companion object { /** * Returns a new [ParameterValue] for the specified [key] and a [Boolean] parsed @@ -35,8 +36,9 @@ internal sealed class ParameterValue(val key: String, val value: Any) { } private class IntParameterValue private constructor( - key: String, value: Int - ): ParameterValue(key, value) { + key: String, + value: Int + ) : ParameterValue(key, value) { companion object { /** * Returns a new [ParameterValue] for the specified [key] and an [Int] parsed representation @@ -49,8 +51,9 @@ internal sealed class ParameterValue(val key: String, val value: Any) { } private class LongParameterValue private constructor( - key: String, value: Long - ): ParameterValue(key, value) { + key: String, + value: Long + ) : ParameterValue(key, value) { companion object { /** * Returns a new [ParameterValue] for the specified [key] and a [Long] parsed representation @@ -63,8 +66,9 @@ internal sealed class ParameterValue(val key: String, val value: Any) { } private class FloatParameterValue private constructor( - key: String, value: Float - ): ParameterValue(key, value) { + key: String, + value: Float + ) : ParameterValue(key, value) { companion object { /** * Returns a new [ParameterValue] for the specified [key] and a [Float] parsed representation @@ -77,8 +81,9 @@ internal sealed class ParameterValue(val key: String, val value: Any) { } private class DoubleParameterValue private constructor( - key: String, value: Double - ): ParameterValue(key, value) { + key: String, + value: Double + ) : ParameterValue(key, value) { companion object { /** * Returns a new [ParameterValue] for the specified [key] and a [Double] parsed representation @@ -91,8 +96,9 @@ internal sealed class ParameterValue(val key: String, val value: Any) { } private class StringParameterValue private constructor( - key: String, value: String - ): ParameterValue(key, value) { + key: String, + value: String + ) : ParameterValue(key, value) { companion object { /** * Returns a new [ParameterValue] for the specified [key] and a [String] parsed representation @@ -104,12 +110,36 @@ internal sealed class ParameterValue(val key: String, val value: Any) { } internal companion object { - private val booleanValueParser = createParser(BooleanParameterValue::createParameter) - private val intValueParser = createParser(IntParameterValue::createParameter) - private val longValueParser = createParser(LongParameterValue::createParameter) - private val floatValueParser = createParser(FloatParameterValue::createParameter) - private val doubleValueParser = createParser(DoubleParameterValue::createParameter) - private val stringValueParser = createParser(StringParameterValue::createParameter) + private val booleanValueParser = object : ParameterValueParser { + override fun parseParameter(key: String, rawValue: String): ParameterValue? { + return BooleanParameterValue.createParameter(key, rawValue) + } + } + private val intValueParser = object : ParameterValueParser { + override fun parseParameter(key: String, rawValue: String): ParameterValue? { + return IntParameterValue.createParameter(key, rawValue) + } + } + private val longValueParser = object : ParameterValueParser { + override fun parseParameter(key: String, rawValue: String): ParameterValue? { + return LongParameterValue.createParameter(key, rawValue) + } + } + private val floatValueParser = object : ParameterValueParser { + override fun parseParameter(key: String, rawValue: String): ParameterValue? { + return FloatParameterValue.createParameter(key, rawValue) + } + } + private val doubleValueParser = object : ParameterValueParser { + override fun parseParameter(key: String, rawValue: String): ParameterValue? { + return DoubleParameterValue.createParameter(key, rawValue) + } + } + private val stringValueParser = object : ParameterValueParser { + override fun parseParameter(key: String, rawValue: String): ParameterValue { + return StringParameterValue.createParameter(key, rawValue) + } + } /** * Returns a new [ParameterValueParser] corresponding to the type of the specified [field], or @@ -127,17 +157,16 @@ internal sealed class ParameterValue(val key: String, val value: Any) { } } + // TODO(#4122): Use 'fun interface' here, instead, once ktlint supports it. This allows for + // method references to be passed to createParser when defining the parsers above. See the + // blame PR's change for this code for a commit that has the functional interface alternative. /** A string parser for a specific [ParameterValue] type. */ - fun interface ParameterValueParser { + interface ParameterValueParser { // ktlint-disable /** * Returns a [ParameterValue] corresponding to the specified [key], and with a type-safe * parsing of [rawValue], or null if the string value is invalid. */ fun parseParameter(key: String, rawValue: String): ParameterValue? } - - // A hack to work around the fact that Kotlin doesn't support assignment conversion from - // references to a functional interface. - private fun createParser(parser: ParameterValueParser) = parser } } diff --git a/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedAndroidJUnit4ClassRunner.kt b/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedAndroidJUnit4ClassRunner.kt index fcb642a437c..8526c4b557d 100644 --- a/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedAndroidJUnit4ClassRunner.kt +++ b/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedAndroidJUnit4ClassRunner.kt @@ -14,7 +14,7 @@ internal class ParameterizedAndroidJUnit4ClassRunner( private val parameterizedMethods: Map, private val methodName: String?, private val iterationName: String? -): AndroidJUnit4ClassRunner(testClass), ParameterizedRunnerOverrideMethods { +) : AndroidJUnit4ClassRunner(testClass), ParameterizedRunnerOverrideMethods { private val delegate by lazy { ParameterizedRunnerDelegate( parameterizedMethods, diff --git a/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedRobolectricTestRunner.kt b/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedRobolectricTestRunner.kt index 1a6df490125..161a4bce0c2 100644 --- a/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedRobolectricTestRunner.kt +++ b/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedRobolectricTestRunner.kt @@ -14,7 +14,7 @@ internal class ParameterizedRobolectricTestRunner( private val parameterizedMethods: Map, private val methodName: String?, private val iterationName: String? -): RobolectricTestRunner(testClass), ParameterizedRunnerOverrideMethods { +) : RobolectricTestRunner(testClass), ParameterizedRunnerOverrideMethods { private val delegate by lazy { ParameterizedRunnerDelegate( parameterizedMethods, @@ -37,7 +37,7 @@ internal class ParameterizedRobolectricTestRunner( override fun getHelperTestRunner( bootstrappedTestClass: Class<*>? ): HelperTestRunner { - return object: HelperTestRunner(bootstrappedTestClass) { + return object : HelperTestRunner(bootstrappedTestClass) { override fun methodInvoker(method: FrameworkMethod?, test: Any?): Statement { delegate.fetchMethodInvokerFromParent = { innerMethod, innerParent -> super.methodInvoker(innerMethod, innerParent) diff --git a/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedRunnerDelegate.kt b/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedRunnerDelegate.kt index 02d90dbd5d8..fc6c469aae9 100644 --- a/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedRunnerDelegate.kt +++ b/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedRunnerDelegate.kt @@ -13,7 +13,7 @@ internal class ParameterizedRunnerDelegate( private val parameterizedMethods: Map, private val methodName: String?, private val iterationName: String? -): ParameterizedRunnerOverrideMethods { +) : ParameterizedRunnerOverrideMethods { /** * A lambda used to call into the parent runner's [getChildren] method. This should be set by * helper parameterized test runners. diff --git a/utility/src/main/java/org/oppia/android/util/math/MathTokenizer.kt b/utility/src/main/java/org/oppia/android/util/math/MathTokenizer.kt index b710ae7908b..d45ce14d571 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathTokenizer.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathTokenizer.kt @@ -9,8 +9,8 @@ import org.oppia.android.app.model.MathBinaryOperation.Operator.SUBTRACT import org.oppia.android.app.model.MathUnaryOperation import org.oppia.android.app.model.MathUnaryOperation.Operator.NEGATE import org.oppia.android.app.model.MathUnaryOperation.Operator.POSITIVE -import java.lang.StringBuilder import org.oppia.android.util.math.PeekableIterator.Companion.toPeekableIterator +import java.lang.StringBuilder /** * Input tokenizer for math (numeric & algebraic) expressions and equations. diff --git a/utility/src/main/java/org/oppia/android/util/math/PeekableIterator.kt b/utility/src/main/java/org/oppia/android/util/math/PeekableIterator.kt index 4d752fdc2f1..8ecef1499bb 100644 --- a/utility/src/main/java/org/oppia/android/util/math/PeekableIterator.kt +++ b/utility/src/main/java/org/oppia/android/util/math/PeekableIterator.kt @@ -14,7 +14,7 @@ package org.oppia.android.util.math * * This class is not safe to use across multiple threads and requires synchronization. */ -class PeekableIterator private constructor( +class PeekableIterator private constructor( private val backingIterator: Iterator ) : Iterator { private var next: T? = null @@ -84,7 +84,7 @@ class PeekableIterator private constructor( * [List]). Some sequences do support multiple iteration, so the exact behavior of the returned * iterator will be sequence implementation dependent. */ - fun Sequence.toPeekableIterator(): PeekableIterator = + fun Sequence.toPeekableIterator(): PeekableIterator = PeekableIterator(iterator()) } } diff --git a/utility/src/test/java/org/oppia/android/util/math/MathTokenizerTest.kt b/utility/src/test/java/org/oppia/android/util/math/MathTokenizerTest.kt index 276911cb62a..a91ea971626 100644 --- a/utility/src/test/java/org/oppia/android/util/math/MathTokenizerTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/MathTokenizerTest.kt @@ -644,7 +644,7 @@ class MathTokenizerTest { fun testTokenize_manyOtherUnicodeValues_produceInvalidTokens() { // Build a large list of unicode characters minus those which are actually allowed. The ASCII // range is excluded from this list. - val characters = ('\u007f' .. '\uffff').filterNot { + val characters = ('\u007f'..'\uffff').filterNot { it in listOf('×', '÷', '−', '√') } val charStr = characters.joinToString("") diff --git a/utility/src/test/java/org/oppia/android/util/math/PeekableIteratorTest.kt b/utility/src/test/java/org/oppia/android/util/math/PeekableIteratorTest.kt index 96da81d6baa..0b6cddc7dbe 100644 --- a/utility/src/test/java/org/oppia/android/util/math/PeekableIteratorTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/PeekableIteratorTest.kt @@ -2,7 +2,6 @@ package org.oppia.android.util.math import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat -import java.util.function.Supplier import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -14,10 +13,10 @@ import org.mockito.Mockito.verify import org.mockito.Mockito.verifyNoMoreInteractions import org.mockito.junit.MockitoJUnit import org.mockito.junit.MockitoRule -import org.mockito.stubbing.Answer import org.oppia.android.testing.assertThrows import org.oppia.android.util.math.PeekableIterator.Companion.toPeekableIterator import org.robolectric.annotation.LooperMode +import java.util.function.Supplier /** Tests for [PeekableIterator]. */ // FunctionName: test names are conventionally named with underscores. @@ -56,7 +55,7 @@ class PeekableIteratorTest { val sequence = sequenceOf() val iterator = sequence.toPeekableIterator() - assertThrows(NoSuchElementException::class) { iterator.next() } + assertThrows(NoSuchElementException::class) { iterator.next() } } @Test From 87a41db6524a0f7fd9a8ad4b04764784402ccc76 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 20 Jan 2022 22:55:59 -0800 Subject: [PATCH 052/162] Remove internals that broke things. --- .../java/org/oppia/android/testing/junit/ParameterValue.kt | 4 ++-- .../org/oppia/android/testing/junit/ParameterizedMethod.kt | 4 ++-- .../android/testing/junit/ParameterizedRunnerDelegate.kt | 2 +- .../testing/junit/ParameterizedRunnerOverrideMethods.kt | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/testing/src/main/java/org/oppia/android/testing/junit/ParameterValue.kt b/testing/src/main/java/org/oppia/android/testing/junit/ParameterValue.kt index fc113db4674..a389a338518 100644 --- a/testing/src/main/java/org/oppia/android/testing/junit/ParameterValue.kt +++ b/testing/src/main/java/org/oppia/android/testing/junit/ParameterValue.kt @@ -10,7 +10,7 @@ import java.lang.reflect.Field * @property value the type-correct value to assign to the field prior to executing the iteration * corresponding to this value */ -internal sealed class ParameterValue(val key: String, val value: Any) { +sealed class ParameterValue(val key: String, val value: Any) { private class BooleanParameterValue private constructor( key: String, value: Boolean @@ -109,7 +109,7 @@ internal sealed class ParameterValue(val key: String, val value: Any) { } } - internal companion object { + companion object { private val booleanValueParser = object : ParameterValueParser { override fun parseParameter(key: String, rawValue: String): ParameterValue? { return BooleanParameterValue.createParameter(key, rawValue) diff --git a/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedMethod.kt b/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedMethod.kt index d56cb4e3e87..e9de6ce70f7 100644 --- a/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedMethod.kt +++ b/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedMethod.kt @@ -9,7 +9,7 @@ import java.util.Locale * * @property methodName the name of the test method that's been parameterized */ -internal class ParameterizedMethod( +class ParameterizedMethod( val methodName: String, private val values: Map>, private val parameterFields: List @@ -25,7 +25,7 @@ internal class ParameterizedMethod( * method is called for each iteration (since the test method should be executed multiples, once * for each of its iteration). */ - internal fun initializeForTest(testClassInstance: Any, iterationName: String) { + fun initializeForTest(testClassInstance: Any, iterationName: String) { // Retrieve the setters for the fields (since these are expected to be used instead of direct // property access in Kotlin). Note that these need to be re-fetched since the instance class // may change (due to Robolectric instrumentation including custom class loading & bytecode diff --git a/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedRunnerDelegate.kt b/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedRunnerDelegate.kt index fc6c469aae9..dc9699b31d5 100644 --- a/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedRunnerDelegate.kt +++ b/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedRunnerDelegate.kt @@ -9,7 +9,7 @@ import org.junit.runners.model.Statement * This class performs the actual field injection and execution delegation for running each * parameterized test method. */ -internal class ParameterizedRunnerDelegate( +class ParameterizedRunnerDelegate( private val parameterizedMethods: Map, private val methodName: String?, private val iterationName: String? diff --git a/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedRunnerOverrideMethods.kt b/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedRunnerOverrideMethods.kt index 6d591ec274f..890685fd674 100644 --- a/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedRunnerOverrideMethods.kt +++ b/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedRunnerOverrideMethods.kt @@ -6,7 +6,7 @@ import org.junit.runners.model.Statement /** * Specifies methods that the helper parameterized runners should override from JUnit's test runner. */ -internal interface ParameterizedRunnerOverrideMethods { +interface ParameterizedRunnerOverrideMethods { /** See [org.junit.runners.BlockJUnit4ClassRunner.getChildren]. */ fun getChildren(): MutableList From c0172ceff6be19d63e3da63f0218faab8f7d6fc8 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 21 Jan 2022 11:45:17 -0800 Subject: [PATCH 053/162] Add regex exemptions. --- scripts/assets/file_content_validation_checks.textproto | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/assets/file_content_validation_checks.textproto b/scripts/assets/file_content_validation_checks.textproto index 5bef515e6df..ee0fc5331f6 100644 --- a/scripts/assets/file_content_validation_checks.textproto +++ b/scripts/assets/file_content_validation_checks.textproto @@ -119,6 +119,7 @@ file_content_checks { exempted_file_name: "domain/src/main/java/org/oppia/android/domain/util/WorkDataExtensions.kt" exempted_file_name: "domain/src/test/java/org/oppia/android/domain/onboarding/AppStartupStateControllerTest.kt" exempted_file_name: "testing/src/main/java/org/oppia/android/testing/OppiaTestRunner.kt" + exempted_file_name: "testing/src/main/java/org/oppia/android/testing/junit/ParameterizedMethod.kt" exempted_file_name: "testing/src/main/java/org/oppia/android/testing/time/FakeOppiaClock.kt" exempted_file_name: "utility/src/main/java/org/oppia/android/util/extensions/BundleExtensions.kt" exempted_file_name: "utility/src/main/java/org/oppia/android/util/locale/MachineLocaleImpl.kt" @@ -242,6 +243,7 @@ file_content_checks { exempted_file_name: "app/src/sharedTest/java/org/oppia/android/app/splash/SplashActivityTest.kt" exempted_file_name: "app/src/test/java/org/oppia/android/app/player/state/StateFragmentLocalTest.kt" exempted_file_name: "testing/src/main/java/org/oppia/android/testing/junit/InitializeDefaultLocaleRule.kt" + exempted_file_name: "testing/src/main/java/org/oppia/android/testing/junit/ParameterizedMethod.kt" exempted_file_name: "testing/src/main/java/org/oppia/android/testing/robolectric/ShadowBidiFormatter.kt" exempted_file_name: "testing/src/test/java/org/oppia/android/testing/robolectric/ShadowBidiFormatterTest.kt" exempted_file_name: "utility/src/main/java/org/oppia/android/util/logging/firebase/FirebaseEventLogger.kt" From ff41eebf63547cb7536d8b9c3b026ee99e309691 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 21 Jan 2022 12:13:38 -0800 Subject: [PATCH 054/162] Post-merge fix. --- .../java/org/oppia/android/util/math/MathExpressionParser.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt b/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt index ebebe862430..688ba4408fd 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt @@ -64,6 +64,7 @@ import org.oppia.android.util.math.MathTokenizer.Companion.Token.VariableName import kotlin.math.absoluteValue import org.oppia.android.util.math.MathParsingError.EquationHasTooManyEqualsError import org.oppia.android.util.math.MathParsingError.EquationIsMissingEqualsError +import org.oppia.android.util.math.PeekableIterator.Companion.toPeekableIterator class MathExpressionParser private constructor(private val parseContext: ParseContext) { // TODO: @@ -637,7 +638,7 @@ class MathExpressionParser private constructor(private val parseContext: ParseCo private sealed class ParseContext(val rawExpression: String) { val tokens: PeekableIterator by lazy { - PeekableIterator.fromSequence(MathTokenizer.tokenize(rawExpression)) + MathTokenizer.tokenize(rawExpression).toPeekableIterator() } private var previousToken: Token? = null From f5b657175b33b88148a9c333692b60ee43d101a6 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 21 Jan 2022 17:56:33 -0800 Subject: [PATCH 055/162] Add error tests. These tests are more or less comprehensive based on existing tests and some new ideas. All errors are now covered by MathExpressionParserTest. Error ordering is not tested. A new Truth subject was added for easier testing, as well (for MathParsingError). --- .../oppia/android/testing/math/BUILD.bazel | 17 + .../testing/math/MathParsingErrorSubject.kt | 268 ++++ .../android/util/math/MathExpressionParser.kt | 115 +- .../util/math/AlgebraicEquationParserTest.kt | 12 - .../math/AlgebraicExpressionParserTest.kt | 54 +- .../org/oppia/android/util/math/BUILD.bazel | 4 +- .../util/math/MathExpressionParserTest.kt | 1278 +++++++++++++---- .../util/math/NumericExpressionParserTest.kt | 64 +- 8 files changed, 1411 insertions(+), 401 deletions(-) create mode 100644 testing/src/main/java/org/oppia/android/testing/math/MathParsingErrorSubject.kt diff --git a/testing/src/main/java/org/oppia/android/testing/math/BUILD.bazel b/testing/src/main/java/org/oppia/android/testing/math/BUILD.bazel index ea0b8ba6b54..d60f0f8aafa 100644 --- a/testing/src/main/java/org/oppia/android/testing/math/BUILD.bazel +++ b/testing/src/main/java/org/oppia/android/testing/math/BUILD.bazel @@ -74,6 +74,23 @@ kt_android_library( ], ) +kt_android_library( + name = "math_parsing_error_subject", + testonly = True, + srcs = [ + "MathParsingErrorSubject.kt", + ], + visibility = [ + "//:oppia_testing_visibility", + ], + deps = [ + ":math_expression_subject", + ":real_subject", + "//third_party:com_google_truth_truth", + "//utility/src/main/java/org/oppia/android/util/math:parsing_error", + ], +) + kt_android_library( name = "polynomial_subject", testonly = True, diff --git a/testing/src/main/java/org/oppia/android/testing/math/MathParsingErrorSubject.kt b/testing/src/main/java/org/oppia/android/testing/math/MathParsingErrorSubject.kt new file mode 100644 index 00000000000..5db1b5f61d9 --- /dev/null +++ b/testing/src/main/java/org/oppia/android/testing/math/MathParsingErrorSubject.kt @@ -0,0 +1,268 @@ +package org.oppia.android.testing.math + +import com.google.common.truth.ComparableSubject +import com.google.common.truth.FailureMetadata +import com.google.common.truth.IterableSubject +import com.google.common.truth.StringSubject +import com.google.common.truth.Subject +import com.google.common.truth.Truth.assertAbout +import com.google.common.truth.Truth.assertThat +import org.oppia.android.app.model.MathBinaryOperation +import org.oppia.android.testing.math.MathExpressionSubject.Companion.assertThat +import org.oppia.android.testing.math.RealSubject.Companion.assertThat +import org.oppia.android.util.math.MathParsingError +import org.oppia.android.util.math.MathParsingError.DisabledVariablesInUseError +import org.oppia.android.util.math.MathParsingError.EquationHasTooManyEqualsError +import org.oppia.android.util.math.MathParsingError.EquationIsMissingEqualsError +import org.oppia.android.util.math.MathParsingError.EquationMissingLhsOrRhsError +import org.oppia.android.util.math.MathParsingError.ExponentIsVariableExpressionError +import org.oppia.android.util.math.MathParsingError.ExponentTooLargeError +import org.oppia.android.util.math.MathParsingError.FunctionNameIncompleteError +import org.oppia.android.util.math.MathParsingError.GenericError +import org.oppia.android.util.math.MathParsingError.HangingSquareRootError +import org.oppia.android.util.math.MathParsingError.InvalidFunctionInUseError +import org.oppia.android.util.math.MathParsingError.MultipleRedundantParenthesesError +import org.oppia.android.util.math.MathParsingError.NestedExponentsError +import org.oppia.android.util.math.MathParsingError.NoVariableOrNumberAfterBinaryOperatorError +import org.oppia.android.util.math.MathParsingError.NoVariableOrNumberBeforeBinaryOperatorError +import org.oppia.android.util.math.MathParsingError.NumberAfterVariableError +import org.oppia.android.util.math.MathParsingError.RedundantParenthesesForIndividualTermsError +import org.oppia.android.util.math.MathParsingError.SingleRedundantParenthesesError +import org.oppia.android.util.math.MathParsingError.SpacesBetweenNumbersError +import org.oppia.android.util.math.MathParsingError.SubsequentBinaryOperatorsError +import org.oppia.android.util.math.MathParsingError.SubsequentUnaryOperatorsError +import org.oppia.android.util.math.MathParsingError.TermDividedByZeroError +import org.oppia.android.util.math.MathParsingError.UnbalancedParenthesesError +import org.oppia.android.util.math.MathParsingError.UnnecessarySymbolsError +import org.oppia.android.util.math.MathParsingError.VariableInNumericExpressionError + +// TODO: file issue to add tests. + +class MathParsingErrorSubject( + metadata: FailureMetadata, + private val actual: MathParsingError +) : Subject(metadata, actual) { + fun isSpacesBetweenNumbers() { + assertThat(actual).isEqualTo(SpacesBetweenNumbersError) + } + + fun isUnbalancedParentheses() { + assertThat(actual).isEqualTo(UnbalancedParenthesesError) + } + + fun isSingleRedundantParenthesesThat(): SingleRedundantParenthesesSubject { + return SingleRedundantParenthesesSubject.assertThat(verifyAsType()) + } + + fun isMultipleRedundantParenthesesThat(): MultipleRedundantParenthesesSubject { + return MultipleRedundantParenthesesSubject.assertThat(verifyAsType()) + } + + fun isRedundantIndividualTermsParensThat(): RedundantParenthesesForIndividualTermsSubject { + return RedundantParenthesesForIndividualTermsSubject.assertThat(verifyAsType()) + } + + fun isUnnecessarySymbolWithSymbolThat(): StringSubject { + return assertThat(verifyAsType().invalidSymbol) + } + + fun isNumberAfterVariableThat(): NumberAfterVariableSubject { + return NumberAfterVariableSubject.assertThat(verifyAsType()) + } + + fun isSubsequentBinaryOperatorsThat(): SubsequentBinaryOperatorsSubject { + return SubsequentBinaryOperatorsSubject.assertThat(verifyAsType()) + } + + fun isSubsequentUnaryOperators() { + assertThat(actual).isEqualTo(SubsequentUnaryOperatorsError) + } + + fun isNoVarOrNumBeforeBinaryOperatorThat(): NoVariableOrNumberBeforeBinaryOperatorSubject { + return NoVariableOrNumberBeforeBinaryOperatorSubject.assertThat(verifyAsType()) + } + + fun isNoVariableOrNumberAfterBinaryOperatorThat(): NoVariableOrNumberAfterBinaryOperatorSubject { + return NoVariableOrNumberAfterBinaryOperatorSubject.assertThat(verifyAsType()) + } + + fun isExponentIsVariableExpression() { + assertThat(actual).isEqualTo(ExponentIsVariableExpressionError) + } + + fun isExponentTooLarge() { + assertThat(actual).isEqualTo(ExponentTooLargeError) + } + + fun isNestedExponents() { + assertThat(actual).isEqualTo(NestedExponentsError) + } + + fun isHangingSquareRoot() { + assertThat(actual).isEqualTo(HangingSquareRootError) + } + + fun isTermDividedByZero() { + assertThat(actual).isEqualTo(TermDividedByZeroError) + } + + fun isVariableInNumericExpression() { + assertThat(actual).isEqualTo(VariableInNumericExpressionError) + } + + fun isDisabledVariablesInUseWithVariablesThat(): IterableSubject { + return assertThat(verifyAsType().variables) + } + + fun isEquationIsMissingEquals() { + assertThat(actual).isEqualTo(EquationIsMissingEqualsError) + } + + fun isEquationHasTooManyEquals() { + assertThat(actual).isEqualTo(EquationHasTooManyEqualsError) + } + + fun isEquationMissingLhsOrRhs() { + assertThat(actual).isEqualTo(EquationMissingLhsOrRhsError) + } + + fun isInvalidFunctionInUseWithNameThat(): StringSubject { + return assertThat(verifyAsType().functionName) + } + + fun isFunctionNameIncomplete() { + assertThat(actual).isEqualTo(FunctionNameIncompleteError) + } + + fun isGenericError() { + assertThat(actual).isEqualTo(GenericError) + } + + private inline fun verifyAsType(): T { + assertThat(actual).isInstanceOf(T::class.java) + return actual as T + } + + class SingleRedundantParenthesesSubject( + metadata: FailureMetadata, + private val actual: SingleRedundantParenthesesError + ) : Subject(metadata, actual) { + fun hasRawExpressionThat(): StringSubject = assertThat(actual.rawExpression) + + fun hasExpressionThat(): MathExpressionSubject = assertThat(actual.expression) + + companion object { + internal fun assertThat( + actual: SingleRedundantParenthesesError + ): SingleRedundantParenthesesSubject { + return assertAbout(::SingleRedundantParenthesesSubject).that(actual) + } + } + } + + class MultipleRedundantParenthesesSubject( + metadata: FailureMetadata, + private val actual: MultipleRedundantParenthesesError + ) : Subject(metadata, actual) { + fun hasRawExpressionThat(): StringSubject = assertThat(actual.rawExpression) + + fun hasExpressionThat(): MathExpressionSubject = assertThat(actual.expression) + + companion object { + internal fun assertThat( + actual: MultipleRedundantParenthesesError + ): MultipleRedundantParenthesesSubject { + return assertAbout(::MultipleRedundantParenthesesSubject).that(actual) + } + } + } + + class RedundantParenthesesForIndividualTermsSubject( + metadata: FailureMetadata, + private val actual: RedundantParenthesesForIndividualTermsError + ) : Subject(metadata, actual) { + fun hasRawExpressionThat(): StringSubject = assertThat(actual.rawExpression) + + fun hasExpressionThat(): MathExpressionSubject = assertThat(actual.expression) + + companion object { + internal fun assertThat( + actual: RedundantParenthesesForIndividualTermsError + ): RedundantParenthesesForIndividualTermsSubject { + return assertAbout(::RedundantParenthesesForIndividualTermsSubject).that(actual) + } + } + } + + class NumberAfterVariableSubject( + metadata: FailureMetadata, + private val actual: NumberAfterVariableError + ) : Subject(metadata, actual) { + fun hasNumberThat(): RealSubject = assertThat(actual.number) + + fun hasVariableThat(): StringSubject = assertThat(actual.variable) + + companion object { + internal fun assertThat(actual: NumberAfterVariableError): NumberAfterVariableSubject = + assertAbout(::NumberAfterVariableSubject).that(actual) + } + } + + class SubsequentBinaryOperatorsSubject( + metadata: FailureMetadata, + private val actual: SubsequentBinaryOperatorsError + ) : Subject(metadata, actual) { + fun hasFirstOperatorThat(): StringSubject = assertThat(actual.operator1) + + fun hasSecondOperatorThat(): StringSubject = assertThat(actual.operator2) + + companion object { + internal fun assertThat( + actual: SubsequentBinaryOperatorsError + ): SubsequentBinaryOperatorsSubject { + return assertAbout(::SubsequentBinaryOperatorsSubject).that(actual) + } + } + } + + class NoVariableOrNumberBeforeBinaryOperatorSubject( + metadata: FailureMetadata, + private val actual: NoVariableOrNumberBeforeBinaryOperatorError + ) : Subject(metadata, actual) { + fun hasOperatorThat(): ComparableSubject = + assertThat(actual.operator) + + fun hasOperatorSymbolThat(): StringSubject = assertThat(actual.operatorSymbol) + + companion object { + internal fun assertThat( + actual: NoVariableOrNumberBeforeBinaryOperatorError + ): NoVariableOrNumberBeforeBinaryOperatorSubject { + return assertAbout(::NoVariableOrNumberBeforeBinaryOperatorSubject).that(actual) + } + } + } + + class NoVariableOrNumberAfterBinaryOperatorSubject( + metadata: FailureMetadata, + private val actual: NoVariableOrNumberAfterBinaryOperatorError + ) : Subject(metadata, actual) { + fun hasOperatorThat(): ComparableSubject = + assertThat(actual.operator) + + fun hasOperatorSymbolThat(): StringSubject = assertThat(actual.operatorSymbol) + + companion object { + internal fun assertThat( + actual: NoVariableOrNumberAfterBinaryOperatorError + ): NoVariableOrNumberAfterBinaryOperatorSubject { + return assertAbout(::NoVariableOrNumberAfterBinaryOperatorSubject).that(actual) + } + } + } + + companion object { + fun assertThat(actual: MathParsingError): MathParsingErrorSubject = + assertAbout(::MathParsingErrorSubject).that(actual) + } +} diff --git a/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt b/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt index 688ba4408fd..e0afccf30e4 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt @@ -62,6 +62,7 @@ import org.oppia.android.util.math.MathTokenizer.Companion.Token.RightParenthesi import org.oppia.android.util.math.MathTokenizer.Companion.Token.SquareRootSymbol import org.oppia.android.util.math.MathTokenizer.Companion.Token.VariableName import kotlin.math.absoluteValue +import org.oppia.android.app.model.MathUnaryOperation.Operator as UnaryOperator import org.oppia.android.util.math.MathParsingError.EquationHasTooManyEqualsError import org.oppia.android.util.math.MathParsingError.EquationIsMissingEqualsError import org.oppia.android.util.math.PeekableIterator.Companion.toPeekableIterator @@ -565,40 +566,82 @@ class MathExpressionParser private constructor(private val parseContext: ParseCo } private fun checkForLearnerErrors(expression: MathExpression): MathParsingError? { - val firstMultiRedundantGroup = expression.findFirstMultiRedundantGroup() - val nextRedundantGroup = expression.findNextRedundantGroup() - val nextUnaryOperation = expression.findNextRedundantUnaryOperation() - val nextExpWithVariableExp = expression.findNextExponentiationWithVariablePower() - val nextExpWithTooLargePower = expression.findNextExponentiationWithTooLargePower() - val nextExpWithNestedExp = expression.findNextNestedExponentiation() - val nextDivByZero = expression.findNextDivisionByZero() - val disallowedVariables = expression.findAllDisallowedVariables(parseContext) // Note that the order of checks here is important since errors have precedence, and some are // redundant and, in the wrong order, may cause the wrong error to be returned. val includeOptionalErrors = parseContext.errorCheckingMode.includesOptionalErrors() - return when { - includeOptionalErrors && firstMultiRedundantGroup != null -> { - val subExpression = parseContext.extractSubexpression(firstMultiRedundantGroup) - MultipleRedundantParenthesesError(subExpression, firstMultiRedundantGroup) - } - includeOptionalErrors && expression.expressionTypeCase == GROUP -> - SingleRedundantParenthesesError(parseContext.rawExpression, expression) - includeOptionalErrors && nextRedundantGroup != null -> { - val subExpression = parseContext.extractSubexpression(nextRedundantGroup) - RedundantParenthesesForIndividualTermsError(subExpression, nextRedundantGroup) - } - includeOptionalErrors && nextUnaryOperation != null -> SubsequentUnaryOperatorsError - includeOptionalErrors && nextExpWithVariableExp != null -> ExponentIsVariableExpressionError - includeOptionalErrors && nextExpWithTooLargePower != null -> ExponentTooLargeError - includeOptionalErrors && nextExpWithNestedExp != null -> NestedExponentsError - includeOptionalErrors && nextDivByZero != null -> TermDividedByZeroError - includeOptionalErrors && disallowedVariables.isNotEmpty() -> - DisabledVariablesInUseError(disallowedVariables.toList()) - else -> ensureNoRemainingTokens() + val optionalError = if (includeOptionalErrors) { + checkForFirstRedundantGroupError(expression) + ?: checkForWholeExpressionGroupRedundancy(expression) + ?: checkForRedundantGroupError(expression) + ?: checkForRedundantUnaryOperation(expression) + ?: checkForExponentVariablePowers(expression) + ?: checkForTooLargeExponentPower(expression) + ?: checkForNestedExponentiations(expression) + ?: checkForDivisionByZero(expression) + ?: checkForDisallowedVariables(expression) + ?: checkForUnaryPlus(expression) + } else null + return optionalError ?: checkForRemainingTokens() + } + + private fun checkForFirstRedundantGroupError(expression: MathExpression): MathParsingError? { + return expression.findFirstMultiRedundantGroup()?.let { firstMultiRedundantGroup -> + val subExpression = parseContext.extractSubexpression(firstMultiRedundantGroup) + MultipleRedundantParenthesesError(subExpression, firstMultiRedundantGroup) + } + } + + private fun checkForWholeExpressionGroupRedundancy( + expression: MathExpression + ): MathParsingError? { + return if (expression.expressionTypeCase == GROUP) { + SingleRedundantParenthesesError(parseContext.extractSubexpression(expression), expression) + } else null + } + + private fun checkForRedundantGroupError(expression: MathExpression): MathParsingError? { + return expression.findNextRedundantGroup()?.let { nextRedundantGroup -> + val subExpression = parseContext.extractSubexpression(nextRedundantGroup) + RedundantParenthesesForIndividualTermsError(subExpression, nextRedundantGroup) + } + } + + private fun checkForRedundantUnaryOperation(expression: MathExpression): MathParsingError? { + return expression.findNextRedundantUnaryOperation()?.let { SubsequentUnaryOperatorsError } + } + + private fun checkForExponentVariablePowers(expression: MathExpression): MathParsingError? { + return expression.findNextExponentiationWithVariablePower()?.let { + ExponentIsVariableExpressionError + } + } + + private fun checkForTooLargeExponentPower(expression: MathExpression): MathParsingError? { + return expression.findNextExponentiationWithTooLargePower()?.let { ExponentTooLargeError } + } + + private fun checkForNestedExponentiations(expression: MathExpression): MathParsingError? { + return expression.findNextNestedExponentiation()?.let { NestedExponentsError } + } + + private fun checkForDivisionByZero(expression: MathExpression): MathParsingError? { + return expression.findNextDivisionByZero()?.let { TermDividedByZeroError } + } + + private fun checkForDisallowedVariables(expression: MathExpression): MathParsingError? { + return expression.findAllDisallowedVariables(parseContext).takeIf { it.isNotEmpty() }?.let { + DisabledVariablesInUseError(it.toList()) } } - private fun ensureNoRemainingTokens(): MathParsingError? { + private fun checkForUnaryPlus(expression: MathExpression): MathParsingError? { + return expression.findNextUnaryPlus()?.let { + // The operatorSymbol can't be trivially extracted, so just force it to '+' for correctness. + NoVariableOrNumberBeforeBinaryOperatorError(ADD, operatorSymbol = "+") + } + } + + private fun checkForRemainingTokens(): MathParsingError? { // Make sure all tokens were consumed (otherwise there are trailing tokens which invalidate the // whole grammar). return if (parseContext.hasMoreTokens()) { @@ -1030,6 +1073,22 @@ class MathExpressionParser private constructor(private val parseContext: ParseCo } } + private fun MathExpression.findNextUnaryPlus(): MathExpression? { + return when (expressionTypeCase) { + BINARY_OPERATION -> + binaryOperation.leftOperand.findNextUnaryPlus() + ?: binaryOperation.rightOperand.findNextUnaryPlus() + UNARY_OPERATION -> when (unaryOperation.operator) { + POSITIVE -> this + NEGATE, UnaryOperator.OPERATOR_UNSPECIFIED, UnaryOperator.UNRECOGNIZED, null -> + unaryOperation.operand.findNextUnaryPlus() + } + FUNCTION_CALL -> functionCall.argument.findNextUnaryPlus() + GROUP -> group.findNextUnaryPlus() + CONSTANT, VARIABLE, EXPRESSIONTYPE_NOT_SET, null -> null + } + } + private fun MathExpression.isVariableExpression(): Boolean { return when (expressionTypeCase) { VARIABLE -> true diff --git a/utility/src/test/java/org/oppia/android/util/math/AlgebraicEquationParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/AlgebraicEquationParserTest.kt index 94fc6e50ab4..8e94d04c379 100644 --- a/utility/src/test/java/org/oppia/android/util/math/AlgebraicEquationParserTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/AlgebraicEquationParserTest.kt @@ -16,9 +16,6 @@ import org.robolectric.annotation.LooperMode class AlgebraicEquationParserTest { @Test fun testLotsOfCasesForAlgebraicEquation() { - expectFailureWhenParsingAlgebraicEquation(" x =") - expectFailureWhenParsingAlgebraicEquation(" = y") - val equation1 = parseAlgebraicEquationSuccessfully("x = 1") assertThat(equation1).hasLeftHandSideThat().hasStructureThatMatches { variable { @@ -134,9 +131,6 @@ class AlgebraicEquationParserTest { } } - expectFailureWhenParsingAlgebraicEquation("y = (x+1)(x-1) 2") - expectFailureWhenParsingAlgebraicEquation("y 2 = (x+1)(x-1)") - val equation5 = parseAlgebraicEquationSuccessfully( "a*x^2 + b*x + c = 0", allowedVariables = listOf("x", "a", "b", "c") @@ -201,12 +195,6 @@ class AlgebraicEquationParserTest { private companion object { // TODO: fix helper API. - private fun expectFailureWhenParsingAlgebraicEquation(expression: String): MathParsingError { - val result = parseAlgebraicEquationWithAllErrors(expression) - assertThat(result).isInstanceOf(MathParsingResult.Failure::class.java) - return (result as MathParsingResult.Failure).error - } - private fun parseAlgebraicEquationSuccessfully( expression: String, allowedVariables: List = listOf("x", "y", "z") diff --git a/utility/src/test/java/org/oppia/android/util/math/AlgebraicExpressionParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/AlgebraicExpressionParserTest.kt index 9fea4084970..e05d9c5906f 100644 --- a/utility/src/test/java/org/oppia/android/util/math/AlgebraicExpressionParserTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/AlgebraicExpressionParserTest.kt @@ -19,8 +19,6 @@ class AlgebraicExpressionParserTest { fun testLotsOfCasesForAlgebraicExpression() { // TODO: split this up // TODO: add log string generation for expressions. - expectFailureWhenParsingAlgebraicExpression("") - val expression1 = parseAlgebraicExpressionWithAllErrors("1") assertThat(expression1).hasStructureThatMatches { constant { @@ -187,8 +185,6 @@ class AlgebraicExpressionParserTest { } } - expectFailureWhenParsingAlgebraicExpression("sqr(2)") - val expression64 = parseAlgebraicExpressionWithoutOptionalErrors("xyz(2)") assertThat(expression64).hasStructureThatMatches { multiplication { @@ -232,8 +228,6 @@ class AlgebraicExpressionParserTest { } } - expectFailureWhenParsingAlgebraicExpression("73 2") - // Verify order of operations between higher & lower precedent operators. val expression32 = parseAlgebraicExpressionWithAllErrors("3+4^5") assertThat(expression32).hasStructureThatMatches { @@ -357,8 +351,6 @@ class AlgebraicExpressionParserTest { } } - expectFailureWhenParsingAlgebraicExpression("x = √2 × 7 ÷ 4") - val expression8 = parseAlgebraicExpressionWithAllErrors("(1+2)(3+4)") assertThat(expression8).hasStructureThatMatches { multiplication { @@ -397,9 +389,6 @@ class AlgebraicExpressionParserTest { } } - // Right implicit multiplication of numbers isn't allowed. - expectFailureWhenParsingAlgebraicExpression("(1+2)2") - val expression10 = parseAlgebraicExpressionWithAllErrors("2(1+2)") assertThat(expression10).hasStructureThatMatches { multiplication { @@ -427,9 +416,6 @@ class AlgebraicExpressionParserTest { } } - // Right implicit multiplication of numbers isn't allowed. - expectFailureWhenParsingAlgebraicExpression("sqrt(2)3") - val expression12 = parseAlgebraicExpressionWithAllErrors("3sqrt(2)") assertThat(expression12).hasStructureThatMatches { multiplication { @@ -651,7 +637,7 @@ class AlgebraicExpressionParserTest { } } - val expression18 = parseAlgebraicExpressionWithAllErrors("1++4") + val expression18 = parseAlgebraicExpressionWithoutOptionalErrors("1++4") assertThat(expression18).hasStructureThatMatches { addition { leftOperand { @@ -691,8 +677,6 @@ class AlgebraicExpressionParserTest { } } - expectFailureWhenParsingAlgebraicExpression("1-^-4") - val expression20 = parseAlgebraicExpressionWithAllErrors("√2 × 7 ÷ 4") assertThat(expression20).hasStructureThatMatches { division { @@ -722,8 +706,6 @@ class AlgebraicExpressionParserTest { } } - expectFailureWhenParsingAlgebraicExpression("1+2 &asdf") - val expression21 = parseAlgebraicExpressionWithAllErrors("sqrt(2)sqrt(3)sqrt(4)") // Note that this tree demonstrates left associativity. assertThat(expression21).hasStructureThatMatches { @@ -1005,13 +987,6 @@ class AlgebraicExpressionParserTest { } } - // Numbers cannot have implicit multiplication unless they are in groups. - expectFailureWhenParsingAlgebraicExpression("2 2") - - expectFailureWhenParsingAlgebraicExpression("2 2^2") - - expectFailureWhenParsingAlgebraicExpression("2^2 2") - val expression31 = parseAlgebraicExpressionWithoutOptionalErrors("(3)(4)(5)") assertThat(expression31).hasStructureThatMatches { multiplication { @@ -1091,9 +1066,6 @@ class AlgebraicExpressionParserTest { } } - // An exponentiation can never be an implicit right operand. - expectFailureWhenParsingAlgebraicExpression("2^(3)2^2") - val expression35 = parseAlgebraicExpressionWithoutOptionalErrors("2^(3)*2^2") assertThat(expression35).hasStructureThatMatches { multiplication { @@ -1169,9 +1141,6 @@ class AlgebraicExpressionParserTest { } } - // An exponentiation can never be an implicit right operand. - expectFailureWhenParsingAlgebraicExpression("2^3(4)2^3") - val expression38 = parseAlgebraicExpressionWithoutOptionalErrors("2^3(4)*2^3") assertThat(expression38).hasStructureThatMatches { // 2^3(4)*2^3 @@ -1226,14 +1195,6 @@ class AlgebraicExpressionParserTest { } } - expectFailureWhenParsingAlgebraicExpression("2^2 2^2") - expectFailureWhenParsingAlgebraicExpression("(3) 2^2") - expectFailureWhenParsingAlgebraicExpression("sqrt(3) 2^2") - expectFailureWhenParsingAlgebraicExpression("√2 2^2") - expectFailureWhenParsingAlgebraicExpression("2^2 3") - - expectFailureWhenParsingAlgebraicExpression("-2 3") - val expression39 = parseAlgebraicExpressionWithAllErrors("-(1+2)") assertThat(expression39).hasStructureThatMatches { negation { @@ -1684,9 +1645,6 @@ class AlgebraicExpressionParserTest { } } - // Should fail for algebra. - expectFailureWhenParsingAlgebraicExpression("x7") - // Should pass for algebra. val expression67 = parseAlgebraicExpressionWithAllErrors("2x^2y^-3") assertThat(expression67).hasStructureThatMatches { @@ -1894,16 +1852,6 @@ class AlgebraicExpressionParserTest { private companion object { // TODO: fix helper API. - private fun expectFailureWhenParsingAlgebraicExpression( - expression: String, - allowedVariables: List = listOf("x", "y", "z") - ): MathParsingError { - val result = - parseAlgebraicExpressionInternal(expression, ErrorCheckingMode.ALL_ERRORS, allowedVariables) - assertThat(result).isInstanceOf(MathParsingResult.Failure::class.java) - return (result as MathParsingResult.Failure).error - } - private fun parseAlgebraicExpressionWithoutOptionalErrors( expression: String, allowedVariables: List = listOf("x", "y", "z") diff --git a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel index 02ed22ac702..5d2a0422271 100644 --- a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel @@ -86,7 +86,9 @@ oppia_android_test( test_manifest = "//utility:test_manifest", deps = [ "//model/src/main/proto:math_java_proto_lite", - "//third_party:androidx_test_ext_junit", + "//testing/src/main/java/org/oppia/android/testing/junit:oppia_parameterized_test_runner", + "//testing/src/main/java/org/oppia/android/testing/junit:parameterized_robolectric_test_runner", + "//testing/src/main/java/org/oppia/android/testing/math:math_parsing_error_subject", "//third_party:com_google_truth_extensions_truth-liteproto-extension", "//third_party:com_google_truth_truth", "//third_party:junit_junit", diff --git a/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt index 88e0a46ac79..f74e1db819a 100644 --- a/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt @@ -1,348 +1,1138 @@ package org.oppia.android.util.math -import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.MathBinaryOperation +import org.oppia.android.app.model.MathBinaryOperation.Operator.ADD +import org.oppia.android.app.model.MathBinaryOperation.Operator.DIVIDE +import org.oppia.android.app.model.MathBinaryOperation.Operator.EXPONENTIATE +import org.oppia.android.app.model.MathBinaryOperation.Operator.MULTIPLY +import org.oppia.android.app.model.MathBinaryOperation.Operator.SUBTRACT import org.oppia.android.app.model.MathEquation import org.oppia.android.app.model.MathExpression +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Iteration +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Parameter +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.RunParameterized +import org.oppia.android.testing.math.MathParsingErrorSubject.Companion.assertThat import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode +import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode.ALL_ERRORS +import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode.REQUIRED_ONLY import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult -import org.oppia.android.util.math.MathParsingError.DisabledVariablesInUseError -import org.oppia.android.util.math.MathParsingError.EquationHasTooManyEqualsError -import org.oppia.android.util.math.MathParsingError.EquationIsMissingEqualsError -import org.oppia.android.util.math.MathParsingError.EquationMissingLhsOrRhsError -import org.oppia.android.util.math.MathParsingError.ExponentIsVariableExpressionError -import org.oppia.android.util.math.MathParsingError.ExponentTooLargeError -import org.oppia.android.util.math.MathParsingError.FunctionNameIncompleteError -import org.oppia.android.util.math.MathParsingError.HangingSquareRootError -import org.oppia.android.util.math.MathParsingError.InvalidFunctionInUseError -import org.oppia.android.util.math.MathParsingError.MultipleRedundantParenthesesError -import org.oppia.android.util.math.MathParsingError.NestedExponentsError -import org.oppia.android.util.math.MathParsingError.NoVariableOrNumberAfterBinaryOperatorError -import org.oppia.android.util.math.MathParsingError.NoVariableOrNumberBeforeBinaryOperatorError -import org.oppia.android.util.math.MathParsingError.NumberAfterVariableError -import org.oppia.android.util.math.MathParsingError.RedundantParenthesesForIndividualTermsError -import org.oppia.android.util.math.MathParsingError.SingleRedundantParenthesesError -import org.oppia.android.util.math.MathParsingError.SpacesBetweenNumbersError -import org.oppia.android.util.math.MathParsingError.SubsequentBinaryOperatorsError -import org.oppia.android.util.math.MathParsingError.SubsequentUnaryOperatorsError -import org.oppia.android.util.math.MathParsingError.TermDividedByZeroError -import org.oppia.android.util.math.MathParsingError.UnbalancedParenthesesError -import org.oppia.android.util.math.MathParsingError.UnnecessarySymbolsError -import org.oppia.android.util.math.MathParsingError.VariableInNumericExpressionError import org.robolectric.annotation.LooperMode -/** Tests for [MathExpressionParser]. */ -@RunWith(AndroidJUnit4::class) +/** + * Tests for [MathExpressionParser]. + * + * Note that while this verifies that, at a high level, numeric expressions, algebraic expressions, + * and algebraic equations can be successfully parsed, it mainly focuses on errors (and some passing + * cases that closely relate to possible errors). + * + * Further, this mainly relies on numeric expressions to ensure error detection works since it's + * assumed that algebraic equations rely on algebraic expressions, and algebraic expressions rely on + * numeric expressions. + * + * Finally, there are dedicated test suites for each of numeric expressions + * [NumericExpressionParserTest], algebraic expressions [AlgebraicExpressionParserTest], and + * algebraic equations [AlgebraicEquationParserTest]. + */ +// FunctionName: test names are conventionally named with underscores. +// SameParameterValue: tests should have specific context included/excluded for readability. +@Suppress("FunctionName", "SameParameterValue") +@RunWith(OppiaParameterizedTestRunner::class) @LooperMode(LooperMode.Mode.PAUSED) class MathExpressionParserTest { - // TODO: add high-level checks for the three types, but don't test in detail since there are - // separate suites. Also, document the separate suites' existence in this suites's KDoc. + @Parameter + lateinit var lhsOp: String + @Parameter + lateinit var rhsOp: String + @Parameter + lateinit var binOp: String + @Parameter + lateinit var subExp: String + @Parameter + lateinit var func: String @Test - fun testErrorCases() { - // TODO: split up. - val failure1 = expectFailureWhenParsingNumericExpression("73 2") - assertThat(failure1).isEqualTo(SpacesBetweenNumbersError) + fun testParseNumExp_basicExpression_doesNotFail() { + expectSuccessWhenParsingNumericExpression("1 + 2") + } - val failure2 = expectFailureWhenParsingNumericExpression("(73") - assertThat(failure2).isEqualTo(UnbalancedParenthesesError) + @Test + fun testParseAlgExp_basicExpression_doesNotFail() { + expectSuccessWhenParsingAlgebraicExpression("x + y") + } - val failure3 = expectFailureWhenParsingNumericExpression("73)") - assertThat(failure3).isEqualTo(UnbalancedParenthesesError) + @Test + fun testParseAlgebraicEquation_basicEquation_doesNotFail() { + expectSuccessWhenParsingAlgebraicEquation("y = 2x + 3") + } - val failure4 = expectFailureWhenParsingNumericExpression("((73)") - assertThat(failure4).isEqualTo(UnbalancedParenthesesError) + @Test + fun testParseNumExp_emptyExpression_returnsGenericError() { + val error = expectFailureWhenParsingNumericExpression("") - val failure5 = expectFailureWhenParsingNumericExpression("73 (") - assertThat(failure5).isEqualTo(UnbalancedParenthesesError) + assertThat(error).isGenericError() + } - val failure6 = expectFailureWhenParsingNumericExpression("73 )") - assertThat(failure6).isEqualTo(UnbalancedParenthesesError) + @Test + fun testParseNumExp_numbersWithSpaces_returnsSpacesBetweenNumbersError() { + val error = expectFailureWhenParsingNumericExpression("73 2") - val failure7 = expectFailureWhenParsingNumericExpression("sqrt(73") - assertThat(failure7).isEqualTo(UnbalancedParenthesesError) + // Numbers cannot be implicitly multiplied unless they are in groups. + assertThat(error).isSpacesBetweenNumbers() + } - // TODO: test properties on errors (& add better testing library for errors, or at least helpers). - val failure8 = expectFailureWhenParsingNumericExpression("(7 * 2 + 4)") - assertThat(failure8).isInstanceOf(SingleRedundantParenthesesError::class.java) + @Test + fun testParseNumExp_spaceBetweenNegatedAndRegularNumber_returnsSpacesBetweenNumbersError() { + val error = expectFailureWhenParsingNumericExpression("-2 3") - val failure9 = expectFailureWhenParsingNumericExpression("((5 + 4))") - assertThat(failure9).isInstanceOf(MultipleRedundantParenthesesError::class.java) + assertThat(error).isSpacesBetweenNumbers() + } - val failure13 = expectFailureWhenParsingNumericExpression("(((5 + 4)))") - assertThat(failure13).isInstanceOf(MultipleRedundantParenthesesError::class.java) + @Test + fun testParseNumExp_numberAndExponentSeparatedBySpace_returnsSpacesBetweenNumbersError() { + val error = expectFailureWhenParsingNumericExpression("2 2^2") - val failure14 = expectFailureWhenParsingNumericExpression("1+((5 + 4))") - assertThat(failure14).isInstanceOf(MultipleRedundantParenthesesError::class.java) + // Unless a similar algebraic expression (e.g. 2x^2), this is not valid. + assertThat(error).isSpacesBetweenNumbers() + } - val failure15 = expectFailureWhenParsingNumericExpression("1+(7*((( 9 + 3) )))") - assertThat(failure15).isInstanceOf(MultipleRedundantParenthesesError::class.java) - assertThat((failure15 as MultipleRedundantParenthesesError).rawExpression) - .isEqualTo("(( 9 + 3) )") + @Test + fun testParseNumExp_squareRootAndExponentSeparatedBySpace_returnsSpacesBetweenNumbersError() { + val error = expectFailureWhenParsingNumericExpression("√2 2^2") - parseNumericExpressionSuccessfully("1+(5+4)") - parseNumericExpressionSuccessfully("(5+4)+1") + // Ensure the square root special case doesn't change the implicit multiplication rule for + // numbers. + assertThat(error).isSpacesBetweenNumbers() + } - val failure10 = expectFailureWhenParsingNumericExpression("(5) + 4") - assertThat(failure10).isInstanceOf(RedundantParenthesesForIndividualTermsError::class.java) + @Test + fun testParseNumExp_exponentAndNumberSeparatedBySpace_returnsSpacesBetweenNumbersError() { + val error = expectFailureWhenParsingNumericExpression("2^2 2") - val failure11 = expectFailureWhenParsingNumericExpression("5^(2)") - assertThat(failure11).isInstanceOf(RedundantParenthesesForIndividualTermsError::class.java) - assertThat((failure11 as RedundantParenthesesForIndividualTermsError).rawExpression) - .isEqualTo("2") + // Right implicit multiplication for numbers is never allowed. + assertThat(error).isSpacesBetweenNumbers() + } - val failure12 = expectFailureWhenParsingNumericExpression("sqrt((2))") - assertThat(failure12).isInstanceOf(RedundantParenthesesForIndividualTermsError::class.java) + @Test + fun testParseNumExp_twoExponentsSeparatedBySpace_returnsSpacesBetweenNumbersError() { + val error = expectFailureWhenParsingNumericExpression("2^2 3^2") - val failure16 = expectFailureWhenParsingNumericExpression("$2") - assertThat(failure16).isInstanceOf(UnnecessarySymbolsError::class.java) - assertThat((failure16 as UnnecessarySymbolsError).invalidSymbol).isEqualTo("$") + // Subsequent exponents are never implicitly multiplied for numeric expressions. + assertThat(error).isSpacesBetweenNumbers() + } - val failure17 = expectFailureWhenParsingNumericExpression("5%") - assertThat(failure17).isInstanceOf(UnnecessarySymbolsError::class.java) - assertThat((failure17 as UnnecessarySymbolsError).invalidSymbol).isEqualTo("%") + @Test + fun testParseAlgExp_numberAndVariableBaseExponentSeparatedBySpace_doesNotFail() { + // Implicit multiplication with numbers on the left is allowed if the right is a variable raised + // to a power (in order to support polynomial syntax). + expectSuccessWhenParsingAlgebraicExpression("2 x^2") + } - val failure18 = expectFailureWhenParsingAlgebraicExpression("x5") - assertThat(failure18).isInstanceOf(NumberAfterVariableError::class.java) - assertThat((failure18 as NumberAfterVariableError).number.integer).isEqualTo(5) - assertThat(failure18.variable).isEqualTo("x") + @Test + fun testParseAlgExp_varBaseExponentAndNumberSeparatedBySpace_returnsSpacesBetweenNumbersError() { + val error = expectFailureWhenParsingAlgebraicExpression("x^2 2") + + // Right implicit multiplication for numbers is never allowed. + assertThat(error).isSpacesBetweenNumbers() + } + + @Test + fun testParseAlgExp_twoAdjacentVariableBaseExponentsSeparatedBySpace_doesNotFail() { + // Similarly, this is supported for polynomial syntax. + expectSuccessWhenParsingAlgebraicExpression("x^2 y^2") + } + + @Test + fun testParseAlgExp_twoAdjacentNumericExponentsSepBySpace_returnsSpacesBetweenNumbersError() { + val error = expectFailureWhenParsingAlgebraicExpression("2^2 3^2") + + // While the variable version of this works, the numeric does not (explicit multiplication is + // required). + assertThat(error).isSpacesBetweenNumbers() + } - val failure19 = expectFailureWhenParsingAlgebraicExpression("2+y 3.14*7") - assertThat(failure19).isInstanceOf(NumberAfterVariableError::class.java) - assertThat((failure19 as NumberAfterVariableError).number.irrational).isWithin(1e-5).of(3.14) - assertThat(failure19.variable).isEqualTo("y") + @Test + fun testParseNumExp_leftParenAndNumber_returnsUnbalancedParenthesesError() { + val error = expectFailureWhenParsingNumericExpression("(73") + + assertThat(error).isUnbalancedParentheses() + } + + @Test + fun testParseNumExp_numberAndRightParen_returnsUnbalancedParenthesesError() { + val error = expectFailureWhenParsingNumericExpression("73)") - // TODO: expand to multiple tests or use parametrized tests. - // RHS operators don't result in unary operations (which are valid in the grammar). - val rhsOperators = listOf("*", "×", "/", "÷", "^") - val lhsOperators = rhsOperators + listOf("+", "-", "−") - val operatorCombinations = lhsOperators.flatMap { op1 -> rhsOperators.map { op1 to it } } - for ((op1, op2) in operatorCombinations) { - val failure22 = expectFailureWhenParsingNumericExpression(expression = "1 $op1$op2 2") - assertThat(failure22).isInstanceOf(SubsequentBinaryOperatorsError::class.java) - assertThat((failure22 as SubsequentBinaryOperatorsError).operator1).isEqualTo(op1) - assertThat(failure22.operator2).isEqualTo(op2) + assertThat(error).isUnbalancedParentheses() + } + + @Test + fun testParseNumExp_numberGroup_extraLeftParen_returnsUnbalancedParenthesesError() { + val error = expectFailureWhenParsingNumericExpression("((73)") + + assertThat(error).isUnbalancedParentheses() + } + + @Test + fun testParseNumExp_number_hangingLeftParen_returnsUnbalancedParenthesesError() { + val error = expectFailureWhenParsingNumericExpression("73 (") + + assertThat(error).isUnbalancedParentheses() + } + + @Test + fun testParseNumExp_number_hangingRightParen_returnsUnbalancedParenthesesError() { + val error = expectFailureWhenParsingNumericExpression("73 )") + + assertThat(error).isUnbalancedParentheses() + } + + @Test + fun testParseNumExp_sqrt_missingRightParen_returnsUnbalancedParenthesesError() { + val error = expectFailureWhenParsingNumericExpression("sqrt(73") + + assertThat(error).isUnbalancedParentheses() + } + + @Test + fun testParseNumExp_outerExpressionGrouped_returnsRedundantParensErrorWithDetails() { + val error = expectFailureWhenParsingNumericExpression("(7 * 2 + 4)") + + assertThat(error).isSingleRedundantParenthesesThat().apply { + // The valid sub-expression should be captured as part of the error. + hasRawExpressionThat().isEqualTo("(7 * 2 + 4)") + hasExpressionThat().hasStructureThatMatches { + group { + addition { + leftOperand { + multiplication { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(7) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + } + + @Test + fun testParseNumExp_leftExpInRightNumericImplicitMult_returnsRedundantParensErrorWithDetails() { + val error = expectFailureWhenParsingNumericExpression("(1 + 2)2") + + // Note that this error occurs because the '2' is considered an extra token in the token stream, + // so only '(1 + 2)' is parsed. + assertThat(error).isSingleRedundantParenthesesThat().apply { + // The valid sub-expression should be captured as part of the error. + hasRawExpressionThat().isEqualTo("(1 + 2)") + hasExpressionThat().hasStructureThatMatches { + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + + @Test + fun testParseNumExp_rightExpInLeftNumericImplicitMult_doesNotFail() { + // As compared with the above, implicit left multiplication is supported with numbers when + // parentheses are used. + expectSuccessWhenParsingNumericExpression("2(1 + 2)") + } + + @Test + fun testParseNumExp_singleVarTermInImplicitExpMult_returnsRedundantParensErrorWithDetails() { + val error = expectFailureWhenParsingNumericExpression("(3) 2^2") + + // Note that this error occurs because the '2^2' is considered extra tokens in the token stream, + // so only '(3)' is parsed. + assertThat(error).isSingleRedundantParenthesesThat().apply { + // The valid sub-expression should be captured as part of the error. + hasRawExpressionThat().isEqualTo("(3)") + hasExpressionThat().hasStructureThatMatches { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } } + } - val failure37 = expectFailureWhenParsingNumericExpression("++2") - assertThat(failure37).isInstanceOf(SubsequentUnaryOperatorsError::class.java) + @Test + fun testParseNumExp_outerExpressionGrouped_optionalErrorsDisabled_doesNotFail() { + // This won't fail if optional errors are disabled. + expectSuccessWhenParsingNumericExpression("(7 * 2 + 4)", errorCheckingMode = REQUIRED_ONLY) + } - val failure38 = expectFailureWhenParsingAlgebraicExpression("--x") - assertThat(failure38).isInstanceOf(SubsequentUnaryOperatorsError::class.java) + @Test + fun testParseNumExp_everythingDoubleParens_returnsMultiParensErrorWithDetails() { + val error = expectFailureWhenParsingNumericExpression("((5 + 4))") + + assertThat(error).isMultipleRedundantParenthesesThat().apply { + // The valid sub-expression should be captured as part of the error. + hasRawExpressionThat().isEqualTo("(5 + 4)") + hasExpressionThat().hasStructureThatMatches { + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + } - val failure39 = expectFailureWhenParsingAlgebraicExpression("-+x") - assertThat(failure39).isInstanceOf(SubsequentUnaryOperatorsError::class.java) + @Test + fun testParseNumExp_everythingTripleParens_returnsMultiParensErrorWithDetails() { + val error = expectFailureWhenParsingNumericExpression("(((5 + 4)))") + + assertThat(error).isMultipleRedundantParenthesesThat().apply { + // The valid sub-expression should be captured as part of the error. + hasRawExpressionThat().isEqualTo("((5 + 4))") + hasExpressionThat().hasStructureThatMatches { + group { + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + } + } - val failure40 = expectFailureWhenParsingNumericExpression("+-2") - assertThat(failure40).isInstanceOf(SubsequentUnaryOperatorsError::class.java) + @Test + fun testParseNumExp_expWithDoubleParens_returnsMultiParensErrorWithDetails() { + val error = expectFailureWhenParsingNumericExpression("1+((5 + 4))") + + assertThat(error).isMultipleRedundantParenthesesThat().apply { + // The valid sub-expression should be captured as part of the error. + hasRawExpressionThat().isEqualTo("(5 + 4)") + hasExpressionThat().hasStructureThatMatches { + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + } - parseNumericExpressionSuccessfully("2++3") // Will succeed since it's 2 + (+2). - val failure41 = expectFailureWhenParsingNumericExpression("2+++3") - assertThat(failure41).isInstanceOf(SubsequentUnaryOperatorsError::class.java) + @Test + fun testParseNumExp_expWithTripleParens_returnsMultiParensErrorWithDetails() { + val error = expectFailureWhenParsingNumericExpression("1+(7*((( 9 + 3) )))") + + assertThat(error).isMultipleRedundantParenthesesThat().apply { + // The valid sub-expression should be captured as part of the error. + hasRawExpressionThat().isEqualTo("(( 9 + 3) )") + hasExpressionThat().hasStructureThatMatches { + group { + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(9) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + } + } + } - val failure23 = expectFailureWhenParsingNumericExpression("/2") - assertThat(failure23).isInstanceOf(NoVariableOrNumberBeforeBinaryOperatorError::class.java) - assertThat((failure23 as NoVariableOrNumberBeforeBinaryOperatorError).operator) - .isEqualTo(MathBinaryOperation.Operator.DIVIDE) + @Test + fun testParseNumExp_expWithTripleParens_optionalErrorsDisabled_doesNotFail() { + // This doesn't trigger an error when optional errors are disabled. + expectSuccessWhenParsingNumericExpression( + "1+(7*((( 9 + 3) )))", errorCheckingMode = REQUIRED_ONLY + ) + } - val failure24 = expectFailureWhenParsingAlgebraicExpression("*x") - assertThat(failure24).isInstanceOf(NoVariableOrNumberBeforeBinaryOperatorError::class.java) - assertThat((failure24 as NoVariableOrNumberBeforeBinaryOperatorError).operator) - .isEqualTo(MathBinaryOperation.Operator.MULTIPLY) + @Test + fun testParseNumExp_innerExpWithSingleParens_onRight_doesNotFail() { + // Succeeds because the right parenthetical term is complex and is part of an outer expression. + expectSuccessWhenParsingNumericExpression("1+(5+4)") + } - val failure27 = expectFailureWhenParsingNumericExpression("2^") - assertThat(failure27).isInstanceOf(NoVariableOrNumberAfterBinaryOperatorError::class.java) - assertThat((failure27 as NoVariableOrNumberAfterBinaryOperatorError).operator) - .isEqualTo(MathBinaryOperation.Operator.EXPONENTIATE) + @Test + fun testParseNumExp_innerExpWithSingleParens_onLeft_doesNotFail() { + // Succeeds because the left parenthetical term is complex and is part of an outer expression. + expectSuccessWhenParsingNumericExpression("(5+4)+1") + } - val failure25 = expectFailureWhenParsingNumericExpression("2/") - assertThat(failure25).isInstanceOf(NoVariableOrNumberAfterBinaryOperatorError::class.java) - assertThat((failure25 as NoVariableOrNumberAfterBinaryOperatorError).operator) - .isEqualTo(MathBinaryOperation.Operator.DIVIDE) + @Test + fun testParseNumExp_numberSingleParen_onLeft_returnsSingleTermParenErrorWithDetails() { + val error = expectFailureWhenParsingNumericExpression("(5) + 4") + + assertThat(error).isRedundantIndividualTermsParensThat().apply { + // The valid sub-expression should be captured as part of the error. + hasRawExpressionThat().isEqualTo("5") + hasExpressionThat().hasStructureThatMatches { + constant { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + } + } - val failure26 = expectFailureWhenParsingAlgebraicExpression("x*") - assertThat(failure26).isInstanceOf(NoVariableOrNumberAfterBinaryOperatorError::class.java) - assertThat((failure26 as NoVariableOrNumberAfterBinaryOperatorError).operator) - .isEqualTo(MathBinaryOperation.Operator.MULTIPLY) + @Test + fun testParseNumExp_numberSingleParen_onRight_returnsSingleTermParenErrorWithDetails() { + val error = expectFailureWhenParsingNumericExpression("5^(2)") + + assertThat(error).isRedundantIndividualTermsParensThat().apply { + // The valid sub-expression should be captured as part of the error. + hasRawExpressionThat().isEqualTo("2") + hasExpressionThat().hasStructureThatMatches { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } - val failure28 = expectFailureWhenParsingAlgebraicExpression("x+") - assertThat(failure28).isInstanceOf(NoVariableOrNumberAfterBinaryOperatorError::class.java) - assertThat((failure28 as NoVariableOrNumberAfterBinaryOperatorError).operator) - .isEqualTo(MathBinaryOperation.Operator.ADD) + @Test + fun testParseNumExp_numberSingleParen_sqrt_returnsSingleTermParenErrorWithDetails() { + val error = expectFailureWhenParsingNumericExpression("sqrt((2))") + + assertThat(error).isRedundantIndividualTermsParensThat().apply { + // The valid sub-expression should be captured as part of the error. + hasRawExpressionThat().isEqualTo("2") + hasExpressionThat().hasStructureThatMatches { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } - val failure29 = expectFailureWhenParsingAlgebraicExpression("x-") - assertThat(failure29).isInstanceOf(NoVariableOrNumberAfterBinaryOperatorError::class.java) - assertThat((failure29 as NoVariableOrNumberAfterBinaryOperatorError).operator) - .isEqualTo(MathBinaryOperation.Operator.SUBTRACT) + @Test + fun testParseNumExp_numSingleParen_betweenImplicitMult_returnsSingleTermParenErrorWithDetails() { + val error = expectFailureWhenParsingNumericExpression("2^3(4)2^3") + + // While parentheses can enable implicit multiplication with numbers, due to exponents never + // being valid right implicit multiplication operands (for numeric expressions) the above is + // considered invalid (plus the '4' is by itself without anything else being in the group). + assertThat(error).isRedundantIndividualTermsParensThat().apply { + // The valid sub-expression should be captured as part of the error. + hasRawExpressionThat().isEqualTo("4") + hasExpressionThat().hasStructureThatMatches { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } - val failure42 = expectFailureWhenParsingAlgebraicExpression("2^x") - assertThat(failure42).isInstanceOf(ExponentIsVariableExpressionError::class.java) + @Test + fun testParseNumExp_numberSingleParen_sqrt_optionalErrorsDisabled_doesNotFail() { + // This doesn't trigger an error when optional errors are disabled. + expectSuccessWhenParsingNumericExpression("sqrt((2))", errorCheckingMode = REQUIRED_ONLY) + } - val failure43 = expectFailureWhenParsingAlgebraicExpression("2^(1+x)") - assertThat(failure43).isInstanceOf(ExponentIsVariableExpressionError::class.java) + @Test + fun testParseNumExp_dollarSign_returnsUnnecessarySymbolErrorWithDetails() { + val error = expectFailureWhenParsingNumericExpression("$2") - val failure44 = expectFailureWhenParsingAlgebraicExpression("2^3^x") - assertThat(failure44).isInstanceOf(ExponentIsVariableExpressionError::class.java) + assertThat(error).isUnnecessarySymbolWithSymbolThat().isEqualTo("$") + } - val failure45 = expectFailureWhenParsingAlgebraicExpression("2^sqrt(x)") - assertThat(failure45).isInstanceOf(ExponentIsVariableExpressionError::class.java) + @Test + fun testParseNumExp_exclamation_returnsUnnecessarySymbolErrorWithDetails() { + val error = expectFailureWhenParsingNumericExpression("5!") - val failure46 = expectFailureWhenParsingNumericExpression("2^7") - assertThat(failure46).isInstanceOf(ExponentTooLargeError::class.java) + assertThat(error).isUnnecessarySymbolWithSymbolThat().isEqualTo("!") + } - val failure47 = expectFailureWhenParsingNumericExpression("2^30.12") - assertThat(failure47).isInstanceOf(ExponentTooLargeError::class.java) + @Test + fun testParseAlgExp_unexpectedAmpersand_returnsUnnecessarySymbolErrorWithDetails() { + val error = expectFailureWhenParsingAlgebraicExpression("1+2 &xyz") - parseNumericExpressionSuccessfully("2^3") + assertThat(error).isUnnecessarySymbolWithSymbolThat().isEqualTo("&") + } - val failure48 = expectFailureWhenParsingNumericExpression("2^3^2") - assertThat(failure48).isInstanceOf(NestedExponentsError::class.java) + @Test + fun testParseAlgExp_numberRightOfVar_returnsNumberAfterVariableErrorWithDetails() { + val error = expectFailureWhenParsingAlgebraicExpression("x5") - val failure49 = expectFailureWhenParsingAlgebraicExpression("x^2^5") - assertThat(failure49).isInstanceOf(NestedExponentsError::class.java) + assertThat(error).isNumberAfterVariableThat().apply { + hasNumberThat().isIntegerThat().isEqualTo(5) + hasVariableThat().isEqualTo("x") + } + } - val failure20 = expectFailureWhenParsingNumericExpression("2√") - assertThat(failure20).isInstanceOf(HangingSquareRootError::class.java) + @Test + fun testParseAlgExp_expRightOfVar_returnsNumberAfterVariableErrorWithDetails() { + val error = expectFailureWhenParsingAlgebraicExpression("2+y 3.14*7") - val failure50 = expectFailureWhenParsingNumericExpression("2/0") - assertThat(failure50).isInstanceOf(TermDividedByZeroError::class.java) + assertThat(error).isNumberAfterVariableThat().apply { + hasNumberThat().isIrrationalThat().isWithin(1e-5).of(3.14) + hasVariableThat().isEqualTo("y") + } + } - val failure51 = expectFailureWhenParsingAlgebraicExpression("x/0") - assertThat(failure51).isInstanceOf(TermDividedByZeroError::class.java) + @Test + fun testParseAlgEq_numberRightOfVar_leftSide_returnsNumberAfterVariableErrorWithDetails() { + val error = expectFailureWhenParsingAlgebraicEquation("y 2 = (x+1)(x-1)") - val failure52 = expectFailureWhenParsingNumericExpression("sqrt(2+7/0.0)") - assertThat(failure52).isInstanceOf(TermDividedByZeroError::class.java) + assertThat(error).isNumberAfterVariableThat().apply { + hasNumberThat().isIntegerThat().isEqualTo(2) + hasVariableThat().isEqualTo("y") + } + } - val failure21 = expectFailureWhenParsingNumericExpression("x+y") - assertThat(failure21).isInstanceOf(VariableInNumericExpressionError::class.java) + @Test + @RunParameterized( + // Note that these parameters are intentionally set up to avoid double unary operators (such as + // -- or ++) since those result in different errors due to unary operations being higher + // precedence. In general, unary operators can't appear on the right since they'll be treated as + // such. + Iteration("**", "lhsOp=*", "rhsOp=*"), Iteration("×*", "lhsOp=×", "rhsOp=*"), + Iteration("/*", "lhsOp=/", "rhsOp=*"), Iteration("÷*", "lhsOp=÷", "rhsOp=*"), + Iteration("^*", "lhsOp=^", "rhsOp=*"), Iteration("+*", "lhsOp=+", "rhsOp=*"), + Iteration("-*", "lhsOp=-", "rhsOp=*"), Iteration("−*", "lhsOp=−", "rhsOp=*"), + Iteration("*×", "lhsOp=*", "rhsOp=×"), Iteration("××", "lhsOp=×", "rhsOp=×"), + Iteration("/×", "lhsOp=/", "rhsOp=×"), Iteration("÷×", "lhsOp=÷", "rhsOp=×"), + Iteration("^×", "lhsOp=^", "rhsOp=×"), Iteration("+×", "lhsOp=+", "rhsOp=×"), + Iteration("-×", "lhsOp=-", "rhsOp=×"), Iteration("−×", "lhsOp=−", "rhsOp=×"), + Iteration("*/", "lhsOp=*", "rhsOp=/"), Iteration("×/", "lhsOp=×", "rhsOp=/"), + Iteration("//", "lhsOp=/", "rhsOp=/"), Iteration("÷/", "lhsOp=÷", "rhsOp=/"), + Iteration("^/", "lhsOp=^", "rhsOp=/"), Iteration("+/", "lhsOp=+", "rhsOp=/"), + Iteration("-/", "lhsOp=-", "rhsOp=/"), Iteration("−/", "lhsOp=−", "rhsOp=/"), + Iteration("*÷", "lhsOp=*", "rhsOp=÷"), Iteration("×÷", "lhsOp=×", "rhsOp=÷"), + Iteration("/÷", "lhsOp=/", "rhsOp=÷"), Iteration("÷÷", "lhsOp=÷", "rhsOp=÷"), + Iteration("^÷", "lhsOp=^", "rhsOp=÷"), Iteration("+÷", "lhsOp=+", "rhsOp=÷"), + Iteration("-÷", "lhsOp=-", "rhsOp=÷"), Iteration("−÷", "lhsOp=−", "rhsOp=÷"), + Iteration("*^", "lhsOp=*", "rhsOp=^"), Iteration("×^", "lhsOp=×", "rhsOp=^"), + Iteration("/^", "lhsOp=/", "rhsOp=^"), Iteration("÷^", "lhsOp=÷", "rhsOp=^"), + Iteration("^^", "lhsOp=^", "rhsOp=^"), Iteration("+^", "lhsOp=+", "rhsOp=^"), + Iteration("-^", "lhsOp=-", "rhsOp=^"), Iteration("−^", "lhsOp=−", "rhsOp=^") + ) + fun testParseNumExp_adjacentBinaryOps_returnsSubsequentBinaryOperatorsErrorWithDetails() { + val expression = "1 $lhsOp$rhsOp 2" + + val error = expectFailureWhenParsingNumericExpression(expression) + + // Generally, two adjacent binary operators is an error since a value is expected between them. + assertThat(error).isSubsequentBinaryOperatorsThat().apply { + hasFirstOperatorThat().isEqualTo(lhsOp) + hasSecondOperatorThat().isEqualTo(rhsOp) + } + } - val failure53 = expectFailureWhenParsingAlgebraicExpression("x+y+a") - assertThat(failure53).isInstanceOf(DisabledVariablesInUseError::class.java) - assertThat((failure53 as DisabledVariablesInUseError).variables).containsExactly("a") + @Test + fun testParseNumExp_doubleUnaryPlus_returnsSubsequentUnaryOperatorsError() { + val error = expectFailureWhenParsingNumericExpression("++2") - val failure54 = expectFailureWhenParsingAlgebraicExpression("apple") - assertThat(failure54).isInstanceOf(DisabledVariablesInUseError::class.java) - assertThat((failure54 as DisabledVariablesInUseError).variables) - .containsExactly("a", "p", "l", "e") + assertThat(error).isSubsequentUnaryOperators() + } - val failure55 = - expectFailureWhenParsingAlgebraicExpression("apple", allowedVariables = listOf("a", "p", "l")) - assertThat(failure55).isInstanceOf(DisabledVariablesInUseError::class.java) - assertThat((failure55 as DisabledVariablesInUseError).variables).containsExactly("e") + @Test + fun testParseNumExp_doubleUnaryMinus_returnsSubsequentUnaryOperatorsError() { + val error = expectFailureWhenParsingAlgebraicExpression("--x") - parseAlgebraicExpressionSuccessfully("x+y+z") + assertThat(error).isSubsequentUnaryOperators() + } - val failure56 = - expectFailureWhenParsingAlgebraicExpression("x+y+z", allowedVariables = listOf()) - assertThat(failure56).isInstanceOf(DisabledVariablesInUseError::class.java) - assertThat((failure56 as DisabledVariablesInUseError).variables).containsExactly("x", "y", "z") + @Test + fun testParseNumExp_unaryMinusPlus_returnsSubsequentUnaryOperatorsError() { + val error = expectFailureWhenParsingAlgebraicExpression("-+x") - val failure30 = expectFailureWhenParsingAlgebraicEquation("x==2") - assertThat(failure30).isInstanceOf(EquationHasTooManyEqualsError::class.java) + assertThat(error).isSubsequentUnaryOperators() + } - val failure31 = expectFailureWhenParsingAlgebraicEquation("x=2=y") - assertThat(failure31).isInstanceOf(EquationHasTooManyEqualsError::class.java) + @Test + fun testParseNumExp_unaryPlusMinus_returnsSubsequentUnaryOperatorsError() { + val error = expectFailureWhenParsingNumericExpression("+-2") - val failure32 = expectFailureWhenParsingAlgebraicEquation("x=2=") - assertThat(failure32).isInstanceOf(EquationHasTooManyEqualsError::class.java) + assertThat(error).isSubsequentUnaryOperators() + } - val failure59 = expectFailureWhenParsingAlgebraicEquation("x") - assertThat(failure59).isInstanceOf(EquationIsMissingEqualsError::class.java) + @Test + fun testParseNumExp_twoMinuses_doesNotFail() { + expectSuccessWhenParsingNumericExpression("2--3") // Will succeed since it's 2 - (-2). + } - val failure33 = expectFailureWhenParsingAlgebraicEquation("x=") - assertThat(failure33).isInstanceOf(EquationMissingLhsOrRhsError::class.java) + @Test + fun testParseNumExp_threeMinuses_returnsSubsequentUnaryOperatorsError() { + val error = expectFailureWhenParsingNumericExpression("2---3") - val failure34 = expectFailureWhenParsingAlgebraicEquation("=x") - assertThat(failure34).isInstanceOf(EquationMissingLhsOrRhsError::class.java) + // The above results in this error since it's effectively "2 - (--3)", where the "--3" is + // invalid. + assertThat(error).isSubsequentUnaryOperators() + } - val failure35 = expectFailureWhenParsingAlgebraicEquation("=x") - assertThat(failure35).isInstanceOf(EquationMissingLhsOrRhsError::class.java) + @Test + fun testParseNumExp_threeMinuses_optionalErrorsDisabled_doesNotFail() { + // This doesn't trigger a failure when optional errors are disabled. + expectSuccessWhenParsingNumericExpression("2---3", errorCheckingMode = REQUIRED_ONLY) + } - // TODO: expand to multiple tests or use parametrized tests. - val prohibitedFunctionNames = - listOf( - "exp", "log", "log10", "ln", "sin", "cos", "tan", "cot", "csc", "sec", "atan", "asin", - "acos", "abs" - ) - for (functionName in prohibitedFunctionNames) { - val failure36 = expectFailureWhenParsingAlgebraicEquation("$functionName(0.5)") - assertThat(failure36).isInstanceOf(InvalidFunctionInUseError::class.java) - assertThat((failure36 as InvalidFunctionInUseError).functionName).isEqualTo(functionName) + @Test + @RunParameterized( + // Note that unary operators like '+' and '-' are excluded here since they may result in valid + // unary operations. + Iteration("nothing_times_something_asterisk", "binOp=*"), + Iteration("nothing_times_something", "binOp=×"), + Iteration("nothing_divides_something_slash", "binOp=/"), + Iteration("nothing_divides_something", "binOp=÷"), + Iteration("nothing_to_power_of_something", "binOp=^") + ) + fun testParseNumExp_binOnlyOps_noLeftValue_returnsNoVarOrNumBeforeBinOperatorErrorWithDetails() { + val expression = "$binOp 2" + val operator = BINARY_SYMBOL_TO_OPERATOR_MAP.getValue(binOp) + + val error = expectFailureWhenParsingNumericExpression(expression) + + // A binary operator with no left-hand side is invalid. + assertThat(error).isNoVarOrNumBeforeBinaryOperatorThat().apply { + hasOperatorThat().isEqualTo(operator) + hasOperatorSymbolThat().isEqualTo(binOp) } + } - val failure57 = expectFailureWhenParsingAlgebraicExpression("sq") - assertThat(failure57).isInstanceOf(FunctionNameIncompleteError::class.java) + @Test + fun testParseNumExp_unaryPlus_returnsNoVarOrNumBeforeBinOperatorErrorWithDetails() { + val error = expectFailureWhenParsingNumericExpression("+2") + + // While '+2' is a valid unary expression, it's treated as an error (since it's more likely to + // be a mistyped binary operation than a no-side effect unary operation). + assertThat(error).isNoVarOrNumBeforeBinaryOperatorThat().apply { + hasOperatorThat().isEqualTo(ADD) + hasOperatorSymbolThat().isEqualTo("+") + } + } - val failure58 = expectFailureWhenParsingAlgebraicExpression("sqr") - assertThat(failure58).isInstanceOf(FunctionNameIncompleteError::class.java) + @Test + fun testParseNumExp_unaryPlus_optionalErrorsDisabled_doesNotFail() { + // This doesn't trigger a failure when optional errors are disabled. + expectSuccessWhenParsingNumericExpression("+2", errorCheckingMode = REQUIRED_ONLY) + } - // TODO: Other cases: sqrt(, sqrt(), sqrt 2, +2 + @Test + @RunParameterized( + // Note that unary operators like '+' and '-' are excluded here since they may result in valid + // unary operations. + Iteration("nothing_times_something_asterisk", "binOp=*"), + Iteration("nothing_times_something", "binOp=×"), + Iteration("nothing_divides_something_slash", "binOp=/"), + Iteration("nothing_divides_something", "binOp=÷"), + Iteration("nothing_to_power_of_something", "binOp=^") + ) + fun testParseAlgExp_binOnlyOps_noLeftValue_returnsNoVarOrNumBeforeBinOperatorErrorWithDetails() { + val expression = "$binOp x" + val operator = BINARY_SYMBOL_TO_OPERATOR_MAP.getValue(binOp) + + val error = expectFailureWhenParsingAlgebraicExpression(expression) + + // A binary operator with no left-hand side is invalid. + assertThat(error).isNoVarOrNumBeforeBinaryOperatorThat().apply { + hasOperatorThat().isEqualTo(operator) + hasOperatorSymbolThat().isEqualTo(binOp) + } } - private companion object { - // TODO: fix helper API. + @Test + @RunParameterized( + Iteration("something_times_nothing_asterisk", "binOp=*"), + Iteration("something_times_nothing", "binOp=×"), + Iteration("something_divides_nothing_slash", "binOp=/"), + Iteration("something_divides_nothing", "binOp=÷"), + Iteration("something_to_power_of_nothing", "binOp=^"), + Iteration("something_adds_nothing", "binOp=+"), + Iteration("something_subtracts_nothing_hyphen", "binOp=-"), + Iteration("something_subtracts_nothing", "binOp=−") + ) + fun testParseNumExp_binaryOps_noRightValue_returnsNoVarOrNumAfterBinOperatorErrorWithDetails() { + val expression = "2 $binOp" + val operator = BINARY_SYMBOL_TO_OPERATOR_MAP.getValue(binOp) + + val error = expectFailureWhenParsingNumericExpression(expression) + + // A binary operator with no right-hand side is invalid. + assertThat(error).isNoVariableOrNumberAfterBinaryOperatorThat().apply { + hasOperatorThat().isEqualTo(operator) + hasOperatorSymbolThat().isEqualTo(binOp) + } + } - private fun expectFailureWhenParsingNumericExpression(expression: String): MathParsingError { - val result = parseNumericExpressionWithAllErrors(expression) - assertThat(result).isInstanceOf(MathParsingResult.Failure::class.java) - return (result as MathParsingResult.Failure).error + @Test + @RunParameterized( + Iteration("something_times_nothing_asterisk", "binOp=*"), + Iteration("something_times_nothing", "binOp=×"), + Iteration("something_divides_nothing_slash", "binOp=/"), + Iteration("something_divides_nothing", "binOp=÷"), + Iteration("something_to_power_of_nothing", "binOp=^"), + Iteration("something_adds_nothing", "binOp=+"), + Iteration("something_subtracts_nothing_hyphen", "binOp=-"), + Iteration("something_subtracts_nothing", "binOp=−") + ) + fun testParseAlgExp_binaryOps_noRightValue_returnsNoVarOrNumAfterBinOperatorErrorWithDetails() { + val expression = "x $binOp" + val operator = BINARY_SYMBOL_TO_OPERATOR_MAP.getValue(binOp) + + val error = expectFailureWhenParsingAlgebraicExpression(expression) + + // A binary operator with no right-hand side is invalid. + assertThat(error).isNoVariableOrNumberAfterBinaryOperatorThat().apply { + hasOperatorThat().isEqualTo(operator) + hasOperatorSymbolThat().isEqualTo(binOp) } + } + + @Test + @RunParameterized( + Iteration("var_directly_in_exp", "subExp=x"), + Iteration("var_directly_in_sub_exp", "subExp=(1+x)"), + Iteration("var_directly_in_nested_exp", "subExp=3^x"), + Iteration("var_directly_in_sqrt", "subExp=sqrt(x)") + ) + fun testParseAlgExp_powersWithVariableExpressions_returnsExponentIsVariableExpressionError() { + val expression = "2^$subExp" + + val error = expectFailureWhenParsingAlgebraicExpression(expression) + + // Regardless of how a variable is within an exponent's power, it's always invalid. + assertThat(error).isExponentIsVariableExpression() + } + + @Test + fun testParseAlgExp_powersWithVariableExpression_optionalErrorsDisabled_doesNotFail() { + // This doesn't trigger a failure when optional errors are disabled. + expectSuccessWhenParsingAlgebraicExpression("2^x", errorCheckingMode = REQUIRED_ONLY) + } + + @Test + fun testParseNumExp_largeIntegerExponent_returnsExponentTooLargeError() { + val error = expectFailureWhenParsingNumericExpression("2^7") + + assertThat(error).isExponentTooLarge() + } - private fun parseNumericExpressionSuccessfully(expression: String): MathExpression { - val result = parseNumericExpressionWithAllErrors(expression) - return (result as MathParsingResult.Success).result + @Test + fun testParseNumExp_largeRealExponent_returnsExponentTooLargeError() { + val error = expectFailureWhenParsingNumericExpression("2^30.12") + + assertThat(error).isExponentTooLarge() + } + + @Test + fun testParseNumExp_largeRealExponent_optionalErrorsDisabled_doesNotFail() { + // This doesn't trigger a failure when optional errors are disabled. + expectSuccessWhenParsingNumericExpression("2^30.12", errorCheckingMode = REQUIRED_ONLY) + } + + @Test + fun testParseNumExp_smallIntegerExponent_doesNotFail() { + // Smaller exponents are fine. + expectSuccessWhenParsingNumericExpression("2^3") + } + + @Test + fun testParseNumExp_nestedExponents_returnsNestedExponentsError() { + val error = expectFailureWhenParsingNumericExpression("2^3^2") + + assertThat(error).isNestedExponents() + } + + @Test + fun testParseAlgExp_nestedExponents_returnsNestedExponentsError() { + val error = expectFailureWhenParsingAlgebraicExpression("x^2^5") + + assertThat(error).isNestedExponents() + } + + @Test + fun testParseAlgExp_nestedExponents_optionalErrorsDisabled_doesNotFail() { + // This doesn't trigger a failure when optional errors are disabled. + expectSuccessWhenParsingAlgebraicExpression("x^2^5", errorCheckingMode = REQUIRED_ONLY) + } + + @Test + fun testParseNumExp_noSquareRootArgumentForSymbol_returnsHangingSquareRootError() { + val error = expectFailureWhenParsingNumericExpression("2√") + + assertThat(error).isHangingSquareRoot() + } + + @Test + fun testParseNumExp_integerDividedByZero_returnsTermDividedByZeroError() { + val error = expectFailureWhenParsingNumericExpression("2/0") + + assertThat(error).isTermDividedByZero() + } + + @Test + fun testParseAlgExp_variableDividedByZero_returnsTermDividedByZeroError() { + val error = expectFailureWhenParsingAlgebraicExpression("x/0") + + assertThat(error).isTermDividedByZero() + } + + @Test + fun testParseNumExp_integerDividedByZeroInSqrt_returnsTermDividedByZeroError() { + val error = expectFailureWhenParsingNumericExpression("sqrt(2+7/0.0)") + + assertThat(error).isTermDividedByZero() + } + + @Test + fun testParseNumExp_integerDividedByZeroInSqrt_optionalErrorsDisabled_doesNotFail() { + // This doesn't trigger a failure when optional errors are disabled. + expectSuccessWhenParsingNumericExpression("sqrt(2+7/0.0)", errorCheckingMode = REQUIRED_ONLY) + } + + @Test + fun testParseNumExp_addVariables_returnsVariableInNumericExpressionError() { + val error = expectFailureWhenParsingNumericExpression("x+y") + + assertThat(error).isVariableInNumericExpression() + } + + @Test + fun testParseAlgExp_addUnsupportedVariable_returnsDisabledVariablesInUseErrorWithDetails() { + val allowedVariables = listOf("x", "y") + + val error = expectFailureWhenParsingAlgebraicExpression("x+y+a", allowedVariables) + + // 'a' isn't an allowed variable. + assertThat(error).isDisabledVariablesInUseWithVariablesThat().containsExactly("a") + } + + @Test + fun testParseAlgExp_multUnsupportedVariables_returnsDisabledVariablesInUseErrorWithDetails() { + val allowedVariables = listOf("x", "y") + + val error = expectFailureWhenParsingAlgebraicExpression("apple", allowedVariables) + + // All disabled variables should be considered. + assertThat(error) + .isDisabledVariablesInUseWithVariablesThat() + .containsExactly("a", "p", "l", "e") + } + + @Test + fun testParseAlgExp_multUnsupportedVariables_optionalErrorsDisabled_doesNotFail() { + val allowedVariables = listOf("x", "y") + + // This doesn't trigger a failure when optional errors are disabled. + expectSuccessWhenParsingAlgebraicExpression( + "apple", allowedVariables, errorCheckingMode = REQUIRED_ONLY + ) + } + + @Test + fun testParseAlgExp_addSupportedVariables_doesNotFail() { + val allowedVariables = listOf("x", "y", "a") + + // If only allowed variables are used, no errors should be reported. + expectSuccessWhenParsingAlgebraicExpression("x+y+a", allowedVariables) + } + + @Test + fun testParseAlgExp_addSupportedVariables_noneSupported_returnsDisabledVarsErrorWithDetails() { + val allowedVariables = listOf() + + val error = expectFailureWhenParsingAlgebraicExpression("x+y+z", allowedVariables) + + // No allowed variables essentially results in variables no longer being supported (though with + // less targeted errors than when using numeric expressions). + assertThat(error).isDisabledVariablesInUseWithVariablesThat().containsExactly("x", "y", "z") + } + + @Test + fun testParseAlgEq_twoEquals_returnsEquationHasTooManyEqualsError() { + val error = expectFailureWhenParsingAlgebraicEquation("x==2") + + assertThat(error).isEquationHasTooManyEquals() + } + + @Test + fun testParseAlgEq_doubleEquivalence_returnsEquationHasTooManyEqualsError() { + val error = expectFailureWhenParsingAlgebraicEquation("x=2=y") + + assertThat(error).isEquationHasTooManyEquals() + } + + @Test + fun testParseAlgEq_doubleEquivalence_missingSecondRhs_returnsEquationHasTooManyEqualsError() { + val error = expectFailureWhenParsingAlgebraicEquation("x=2=") + + assertThat(error).isEquationHasTooManyEquals() + } + + @Test + fun testParseAlgEq_noEquals_returnsEquationIsMissingEqualsError() { + val error = expectFailureWhenParsingAlgebraicEquation("x") + + assertThat(error).isEquationIsMissingEquals() + } + + @Test + fun testParseAlgEq_somethingEqualsNothing_returnsEquationMissingLhsOrRhsError() { + val error = expectFailureWhenParsingAlgebraicEquation("x=") + + assertThat(error).isEquationMissingLhsOrRhs() + } + + @Test + fun testParseAlgEq_nothingEqualsSomething_returnsEquationMissingLhsOrRhsError() { + val error = expectFailureWhenParsingAlgebraicEquation("=x") + + assertThat(error).isEquationMissingLhsOrRhs() + } + + @Test + @RunParameterized( + Iteration("exp", "func=exp"), Iteration("log", "func=log"), Iteration("log10", "func=log10"), + Iteration("ln", "func=ln"), Iteration("sin", "func=sin"), Iteration("cos", "func=cos"), + Iteration("tan", "func=tan"), Iteration("cot", "func=cot"), Iteration("csc", "func=csc"), + Iteration("sec", "func=sec"), Iteration("atan", "func=atan"), Iteration("asin", "func=asin"), + Iteration("acos", "func=acos"), Iteration("abs", "func=abs") + ) + fun testParseNumExp_prohibitedFunctionInUse_returnsInvalidFunctionInUseErrorWithDetails() { + val expression = "$func(0.5+1)" + + val error = expectFailureWhenParsingAlgebraicEquation(expression) + + // Usage of detected unsupported functions should result in failures. + assertThat(error).isInvalidFunctionInUseWithNameThat().isEqualTo(func) + } + + @Test + fun testParseAlgExp_unknownFunction_doesNotFail() { + val allowedVariables = LOWERCASE_LATIN_ALPHABET + + // An unknown function won't fail since, so long as it's not similar to known functions, it will + // be treated as implicit variable multiplication. This will fail if the letters composing the + // name are unsupported variables or if attempted in a numeric expression (since variables + // aren't supported). Finally, the '+1' avoids redundant parentheses errors. + expectSuccessWhenParsingAlgebraicExpression("round(2+1)", allowedVariables) + } + + @Test + @RunParameterized( + Iteration("ex", "func=ex"), Iteration("lo", "func=lo"), Iteration("log1", "func=log1"), + Iteration("si", "func=si"), Iteration("co", "func=co"), Iteration("ta", "func=ta"), + Iteration("cs", "func=cs"), Iteration("se", "func=se"), Iteration("at", "func=at"), + Iteration("ata", "func=ata"), Iteration("as", "func=as"), Iteration("asi", "func=asi"), + Iteration("ac", "func=ac"), Iteration("aco", "func=aco"), Iteration("ab", "func=ab"), + Iteration("sq", "func=sq"), Iteration("sqr", "func=sqr") + + ) + fun testParseAlgExp_startOfKnownFunction_returnsFunctionNameIncompleteError() { + val expression = "$func(0.5+1)" + val error = expectFailureWhenParsingAlgebraicExpression(expression) + + + // Starting a detected function but not completing it should result in an incomplete name error. + assertThat(error).isFunctionNameIncomplete() + } + + @Test + @RunParameterized( + Iteration("a", "func=a"), Iteration("c", "func=c"), Iteration("e", "func=e"), + Iteration("l", "func=l"), Iteration("s", "func=s"), Iteration("t", "func=t") + ) + fun testParseAlgExp_firstLetterOfKnownFunctions_areValidExpressions() { + val expression = "$func(0.5+1)" + val allowedVariables = LOWERCASE_LATIN_ALPHABET + + // The first letter of a function is just a variable (it's never treated as the start of a + // function name unless more letters are provided). + expectSuccessWhenParsingAlgebraicExpression(expression, allowedVariables) + } + + @Test + fun testParseNumExp_sqrtFunc_missingArgumentAndRightParen_returnsGenericError() { + val error = expectFailureWhenParsingNumericExpression("sqrt(") + + assertThat(error).isGenericError() + } + + @Test + fun testParseNumExp_sqrtFunc_missingArgument_returnsGenericError() { + val error = expectFailureWhenParsingNumericExpression("sqrt()") + + assertThat(error).isGenericError() + } + + @Test + fun testParseNumExp_sqrtFunc_missingParensWithFloatingArgument_returnsGenericError() { + val error = expectFailureWhenParsingNumericExpression("sqrt 2") + + assertThat(error).isGenericError() + } + + @Test + fun testParseAlgEq_extraNumberAtEnd_returnsGenericError() { + val error = expectFailureWhenParsingAlgebraicEquation("y = (x+1)(x-1) 2") + + // The trailing '2' isn't used in the expression. + assertThat(error).isGenericError() + } + + @Test + fun testParseAlgExp_hasEquals_returnsGenericError() { + val error = expectFailureWhenParsingAlgebraicExpression("x = √2 × 7 ÷ 4") + + // '=' is not allowed in algebraic expressions. + assertThat(error).isGenericError() + } + + @Test + fun testParseNumExp_trailingNumber_afterSqrt_returnsGenericError() { + val error = expectFailureWhenParsingNumericExpression("sqrt(2)3") + + // Right implicit multiplication of numbers isn't allowed. In this case, it's likely the '3' is + // being interpreted as an additional token that's unused. + assertThat(error).isGenericError() + } + + @Test + fun testParseNumExp_trailingImplicitlyMultipliedExponent_returnsGenericError() { + val error = expectFailureWhenParsingNumericExpression("sqrt(3) 2^2") + + // Right implicit multiplication of numeric exponents isn't allowed, though in this case it's + // likely that the '2^2' is being interpreted as additional, unused tokens. + assertThat(error).isGenericError() + } + + private companion object { + private val BINARY_SYMBOL_TO_OPERATOR_MAP = mapOf( + "*" to MULTIPLY, + "×" to MULTIPLY, + "/" to DIVIDE, + "÷" to DIVIDE, + "^" to EXPONENTIATE, + "+" to ADD, + "-" to SUBTRACT, + "−" to SUBTRACT + ) + private val LOWERCASE_LATIN_ALPHABET = listOf( + "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", + "t", "u", "v", "w", "x", "y", "z" + ) + + private fun expectSuccessWhenParsingNumericExpression( + expression: String, errorCheckingMode: ErrorCheckingMode = ALL_ERRORS + ) { + expectSuccessfulParsingResult(parseNumericExpression(expression, errorCheckingMode)) } - private fun parseNumericExpressionWithAllErrors( - expression: String - ): MathParsingResult { - return MathExpressionParser.parseNumericExpression(expression, ErrorCheckingMode.ALL_ERRORS) + private fun expectFailureWhenParsingNumericExpression(expression: String): MathParsingError { + return expectFailingParsingResult(parseNumericExpression(expression)) } - private fun expectFailureWhenParsingAlgebraicExpression( + private fun expectSuccessWhenParsingAlgebraicExpression( expression: String, - allowedVariables: List = listOf("x", "y", "z") + allowedVariables: List = listOf("x", "y", "z"), + errorCheckingMode: ErrorCheckingMode = ALL_ERRORS + ) { + expectSuccessfulParsingResult( + parseAlgebraicExpression(expression, allowedVariables, errorCheckingMode) + ) + } + + private fun expectFailureWhenParsingAlgebraicExpression( + expression: String, allowedVariables: List = listOf("x", "y", "z") ): MathParsingError { - val result = - parseAlgebraicExpressionWithAllErrors(expression, allowedVariables) - assertThat(result).isInstanceOf(MathParsingResult.Failure::class.java) - return (result as MathParsingResult.Failure).error + return expectFailingParsingResult(parseAlgebraicExpression(expression, allowedVariables)) } - private fun parseAlgebraicExpressionSuccessfully( - expression: String, - allowedVariables: List = listOf("x", "y", "z") - ): MathExpression { - val result = parseAlgebraicExpressionWithAllErrors(expression, allowedVariables) - return (result as MathParsingResult.Success).result + private fun expectSuccessWhenParsingAlgebraicEquation(expression: String) { + expectSuccessfulParsingResult(parseAlgebraicEquation(expression)) } - private fun parseAlgebraicExpressionWithAllErrors( - expression: String, - allowedVariables: List = listOf("x", "y", "z") + private fun expectFailureWhenParsingAlgebraicEquation(expression: String): MathParsingError { + return expectFailingParsingResult(parseAlgebraicEquation(expression)) + } + + private fun parseNumericExpression( + expression: String, errorCheckingMode: ErrorCheckingMode = ALL_ERRORS ): MathParsingResult { - return MathExpressionParser.parseAlgebraicExpression( - expression, allowedVariables, ErrorCheckingMode.ALL_ERRORS - ) + return MathExpressionParser.parseNumericExpression(expression, errorCheckingMode) } - private fun expectFailureWhenParsingAlgebraicEquation(expression: String): MathParsingError { - val result = parseAlgebraicEquationInternal(expression, ErrorCheckingMode.ALL_ERRORS) - assertThat(result).isInstanceOf(MathParsingResult.Failure::class.java) - return (result as MathParsingResult.Failure).error + private fun parseAlgebraicExpression( + expression: String, allowedVariables: List, + errorCheckingMode: ErrorCheckingMode = ALL_ERRORS + ): MathParsingResult { + return MathExpressionParser.parseAlgebraicExpression( + expression, allowedVariables, errorCheckingMode + ) } - private fun parseAlgebraicEquationInternal( - expression: String, - errorCheckingMode: ErrorCheckingMode, - allowedVariables: List = listOf("x", "y", "z") - ): MathParsingResult { + private fun parseAlgebraicEquation(expression: String): MathParsingResult { return MathExpressionParser.parseAlgebraicEquation( - expression, allowedVariables, errorCheckingMode + expression, allowedVariables = listOf("x", "y", "z"), ALL_ERRORS ) } + + private fun expectSuccessfulParsingResult(result: MathParsingResult) { + assertThat(result).isInstanceOf(MathParsingResult.Success::class.java) + } + + private fun expectFailingParsingResult(result: MathParsingResult): MathParsingError { + assertThat(result).isInstanceOf(MathParsingResult.Failure::class.java) + return (result as MathParsingResult.Failure).error + } } } diff --git a/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt index d1f9e17b47d..22bfefdea84 100644 --- a/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt @@ -19,8 +19,6 @@ class NumericExpressionParserTest { fun testLotsOfCasesForNumericExpression() { // TODO: split this up // TODO: add log string generation for expressions. - expectFailureWhenParsingNumericExpression("") - val expression1 = parseNumericExpressionWithAllErrors("1") assertThat(expression1).hasStructureThatMatches { constant { @@ -28,8 +26,6 @@ class NumericExpressionParserTest { } } - expectFailureWhenParsingNumericExpression("x") - val expression2 = parseNumericExpressionWithAllErrors(" 2 ") assertThat(expression2).hasStructureThatMatches { constant { @@ -44,10 +40,6 @@ class NumericExpressionParserTest { } } - expectFailureWhenParsingNumericExpression(" x ") - - expectFailureWhenParsingNumericExpression(" z x ") - val expression4 = parseNumericExpressionWithoutOptionalErrors("2^3^2") assertThat(expression4).hasStructureThatMatches { exponentiation { @@ -163,10 +155,6 @@ class NumericExpressionParserTest { } } - expectFailureWhenParsingNumericExpression("sqr(2)") - - expectFailureWhenParsingNumericExpression("xyz(2)") - val expression6 = parseNumericExpressionWithAllErrors("732") assertThat(expression6).hasStructureThatMatches { constant { @@ -297,8 +285,6 @@ class NumericExpressionParserTest { } } - expectFailureWhenParsingNumericExpression("x = √2 × 7 ÷ 4") - val expression8 = parseNumericExpressionWithAllErrors("(1+2)(3+4)") assertThat(expression8).hasStructureThatMatches { multiplication { @@ -337,9 +323,6 @@ class NumericExpressionParserTest { } } - // Right implicit multiplication of numbers isn't allowed. - expectFailureWhenParsingNumericExpression("(1+2)2") - val expression10 = parseNumericExpressionWithAllErrors("2(1+2)") assertThat(expression10).hasStructureThatMatches { multiplication { @@ -367,9 +350,6 @@ class NumericExpressionParserTest { } } - // Right implicit multiplication of numbers isn't allowed. - expectFailureWhenParsingNumericExpression("sqrt(2)3") - val expression12 = parseNumericExpressionWithAllErrors("3sqrt(2)") assertThat(expression12).hasStructureThatMatches { multiplication { @@ -390,8 +370,6 @@ class NumericExpressionParserTest { } } - expectFailureWhenParsingNumericExpression("xsqrt(2)") - val expression13 = parseNumericExpressionWithAllErrors("sqrt(2)*(1+2)*(3-2^5)") assertThat(expression13).hasStructureThatMatches { multiplication { @@ -573,7 +551,7 @@ class NumericExpressionParserTest { } } - val expression18 = parseNumericExpressionWithAllErrors("1++4") + val expression18 = parseNumericExpressionWithoutOptionalErrors("1++4") assertThat(expression18).hasStructureThatMatches { addition { leftOperand { @@ -613,8 +591,6 @@ class NumericExpressionParserTest { } } - expectFailureWhenParsingNumericExpression("1-^-4") - val expression20 = parseNumericExpressionWithAllErrors("√2 × 7 ÷ 4") assertThat(expression20).hasStructureThatMatches { division { @@ -644,8 +620,6 @@ class NumericExpressionParserTest { } } - expectFailureWhenParsingNumericExpression("1+2 &asdf") - val expression21 = parseNumericExpressionWithAllErrors("sqrt(2)sqrt(3)sqrt(4)") // Note that this tree demonstrates left associativity. assertThat(expression21).hasStructureThatMatches { @@ -927,13 +901,6 @@ class NumericExpressionParserTest { } } - // Numbers cannot have implicit multiplication unless they are in groups. - expectFailureWhenParsingNumericExpression("2 2") - - expectFailureWhenParsingNumericExpression("2 2^2") - - expectFailureWhenParsingNumericExpression("2^2 2") - val expression31 = parseNumericExpressionWithoutOptionalErrors("(3)(4)(5)") assertThat(expression31).hasStructureThatMatches { multiplication { @@ -1013,9 +980,6 @@ class NumericExpressionParserTest { } } - // An exponentiation can never be an implicit right operand. - expectFailureWhenParsingNumericExpression("2^(3)2^2") - val expression35 = parseNumericExpressionWithoutOptionalErrors("2^(3)*2^2") assertThat(expression35).hasStructureThatMatches { multiplication { @@ -1091,9 +1055,6 @@ class NumericExpressionParserTest { } } - // An exponentiation can never be an implicit right operand. - expectFailureWhenParsingNumericExpression("2^3(4)2^3") - val expression38 = parseNumericExpressionWithoutOptionalErrors("2^3(4)*2^3") assertThat(expression38).hasStructureThatMatches { // 2^3(4)*2^3 @@ -1148,14 +1109,6 @@ class NumericExpressionParserTest { } } - expectFailureWhenParsingNumericExpression("2^2 2^2") - expectFailureWhenParsingNumericExpression("(3) 2^2") - expectFailureWhenParsingNumericExpression("sqrt(3) 2^2") - expectFailureWhenParsingNumericExpression("√2 2^2") - expectFailureWhenParsingNumericExpression("2^2 3") - - expectFailureWhenParsingNumericExpression("-2 3") - val expression39 = parseNumericExpressionWithAllErrors("-(1+2)") assertThat(expression39).hasStructureThatMatches { negation { @@ -1178,9 +1131,6 @@ class NumericExpressionParserTest { } } - // Should pass for algebra. - expectFailureWhenParsingNumericExpression("-2 x") - val expression40 = parseNumericExpressionWithAllErrors("-2 (1+2)") assertThat(expression40).hasStructureThatMatches { // The negation happens last for parity with other common calculators. @@ -1590,12 +1540,6 @@ class NumericExpressionParserTest { } } - // Should fail for algebra. - expectFailureWhenParsingNumericExpression("x7") - - // Should pass for algebra. - expectFailureWhenParsingNumericExpression("2x^2") - val expression54 = parseNumericExpressionWithAllErrors("2*2/-4+7*2") assertThat(expression54).hasStructureThatMatches { // 2*2/-4+7*2 -> ((2*2)/(-4))+(7*2) @@ -1745,12 +1689,6 @@ class NumericExpressionParserTest { private companion object { // TODO: fix helper API. - private fun expectFailureWhenParsingNumericExpression(expression: String): MathParsingError { - val result = parseNumericExpressionInternal(expression, ErrorCheckingMode.ALL_ERRORS) - assertThat(result).isInstanceOf(MathParsingResult.Failure::class.java) - return (result as MathParsingResult.Failure).error - } - private fun parseNumericExpressionWithoutOptionalErrors(expression: String): MathExpression { val result = parseNumericExpressionInternal( From b7ac054a7dad6fb27ef79368a5976ce091afb924 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 21 Jan 2022 18:12:12 -0800 Subject: [PATCH 056/162] Finish algebraic equation tests. --- .../util/math/AlgebraicEquationParserTest.kt | 119 +++++++++++++----- 1 file changed, 87 insertions(+), 32 deletions(-) diff --git a/utility/src/test/java/org/oppia/android/util/math/AlgebraicEquationParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/AlgebraicEquationParserTest.kt index 8e94d04c379..afdc0588f00 100644 --- a/utility/src/test/java/org/oppia/android/util/math/AlgebraicEquationParserTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/AlgebraicEquationParserTest.kt @@ -1,38 +1,61 @@ package org.oppia.android.util.math -import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat +import androidx.test.ext.junit.runners.AndroidJUnit4 import org.junit.Test import org.junit.runner.RunWith import org.oppia.android.app.model.MathEquation +import org.oppia.android.app.model.MathFunctionCall.FunctionType.SQUARE_ROOT import org.oppia.android.testing.math.MathEquationSubject.Companion.assertThat import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult import org.robolectric.annotation.LooperMode -/** Tests for [MathExpressionParser]. */ +/** + * Tests for [MathExpressionParser]. + * + * Note that this test suite specifically focuses on verifying that the parser can correctly parse + * algebraic equations (i.e. via [MathExpressionParser.parseAlgebraicEquation]. This suite is not as + * thorough as may be expected because: + * 1. It relies heavily on [AlgebraicExpressionParserTest] for verifying that algebraic expressions + * can be correctly parsed. This suite is mainly geared toward verifying that the parser + * essentially just parses expressions for each side of the equation. + * 2. Error cases are tested in [MathExpressionParserTest] instead of here, including for equations. + */ +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) class AlgebraicEquationParserTest { @Test - fun testLotsOfCasesForAlgebraicEquation() { - val equation1 = parseAlgebraicEquationSuccessfully("x = 1") - assertThat(equation1).hasLeftHandSideThat().hasStructureThatMatches { + fun testParseAlgEq_simpleVariableAssignment_correctlyParsesBothSidesStructures() { + val equation = parseAlgebraicEquation("x = 1") + + assertThat(equation).hasLeftHandSideThat().hasStructureThatMatches { variable { withNameThat().isEqualTo("x") } } + assertThat(equation).hasRightHandSideThat().hasStructureThatMatches { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + } - val equation2 = - parseAlgebraicEquationSuccessfully( + @Test + fun testParseAlgEq_slopeInterceptForm_additionalVars_correctlyParsesBothSidesStructures() { + val equation = + parseAlgebraicEquation( "y = mx + b", allowedVariables = listOf("x", "y", "b", "m") ) - assertThat(equation2).hasLeftHandSideThat().hasStructureThatMatches { + + assertThat(equation).hasLeftHandSideThat().hasStructureThatMatches { variable { withNameThat().isEqualTo("y") } } - assertThat(equation2).hasRightHandSideThat().hasStructureThatMatches { + assertThat(equation).hasRightHandSideThat().hasStructureThatMatches { addition { leftOperand { multiplication { @@ -55,14 +78,18 @@ class AlgebraicEquationParserTest { } } } + } - val equation3 = parseAlgebraicEquationSuccessfully("y = (x+1)^2") - assertThat(equation3).hasLeftHandSideThat().hasStructureThatMatches { + @Test + fun testParseAlgEq_binomialAssignedToY_correctlyParsesBothSidesStructures() { + val equation = parseAlgebraicEquation("y = (x+1)^2") + + assertThat(equation).hasLeftHandSideThat().hasStructureThatMatches { variable { withNameThat().isEqualTo("y") } } - assertThat(equation3).hasRightHandSideThat().hasStructureThatMatches { + assertThat(equation).hasRightHandSideThat().hasStructureThatMatches { exponentiation { leftOperand { group { @@ -87,14 +114,18 @@ class AlgebraicEquationParserTest { } } } + } - val equation4 = parseAlgebraicEquationSuccessfully("y = (x+1)(x-1)") - assertThat(equation4).hasLeftHandSideThat().hasStructureThatMatches { + @Test + fun testParseAlgEq_factoredPolynomialAssignedToY_correctlyParsesBothSidesStructures() { + val equation = parseAlgebraicEquation("y = (x+1)(x-1)") + + assertThat(equation).hasLeftHandSideThat().hasStructureThatMatches { variable { withNameThat().isEqualTo("y") } } - assertThat(equation4).hasRightHandSideThat().hasStructureThatMatches { + assertThat(equation).hasRightHandSideThat().hasStructureThatMatches { multiplication { leftOperand { group { @@ -130,12 +161,16 @@ class AlgebraicEquationParserTest { } } } + } - val equation5 = - parseAlgebraicEquationSuccessfully( + @Test + fun testParseAlgEq_generalLineEquation_onLeftSide_correctlyParsesBothSidesStructures() { + val equation = + parseAlgebraicEquation( "a*x^2 + b*x + c = 0", allowedVariables = listOf("x", "a", "b", "c") ) - assertThat(equation5).hasLeftHandSideThat().hasStructureThatMatches { + + assertThat(equation).hasLeftHandSideThat().hasStructureThatMatches { addition { leftOperand { addition { @@ -185,31 +220,51 @@ class AlgebraicEquationParserTest { } } } - assertThat(equation5).hasRightHandSideThat().hasStructureThatMatches { + assertThat(equation).hasRightHandSideThat().hasStructureThatMatches { constant { withValueThat().isIntegerThat().isEqualTo(0) } } } - private companion object { - // TODO: fix helper API. + @Test + fun testParseAlgEq_nonPolynomialEquation_correctlyParsesBothSidesStructures() { + val equation = parseAlgebraicEquation("x = 2^sqrt(3)") - private fun parseAlgebraicEquationSuccessfully( - expression: String, - allowedVariables: List = listOf("x", "y", "z") - ): MathEquation { - val result = parseAlgebraicEquationWithAllErrors(expression, allowedVariables) - return (result as MathParsingResult.Success).result + assertThat(equation).hasLeftHandSideThat().hasStructureThatMatches { + variable { + withNameThat().isEqualTo("x") + } } + assertThat(equation).hasRightHandSideThat().hasStructureThatMatches { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + } + } - private fun parseAlgebraicEquationWithAllErrors( - expression: String, - allowedVariables: List = listOf("x", "y", "z") - ): MathParsingResult { - return MathExpressionParser.parseAlgebraicEquation( + private companion object { + private fun parseAlgebraicEquation( + expression: String, allowedVariables: List = listOf("x", "y", "z") + ): MathEquation { + val result = MathExpressionParser.parseAlgebraicEquation( expression, allowedVariables, ErrorCheckingMode.ALL_ERRORS ) + assertThat(result).isInstanceOf(MathParsingResult.Success::class.java) + return (result as MathParsingResult.Success).result } } } From a122515dd564d0ef2663789cfee7b2e0165f7ddb Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 21 Jan 2022 20:35:15 -0800 Subject: [PATCH 057/162] Reimplement numeric expression tests. This is almost a full replacement. The new tests are more structured and intentional to cover key high-level concepts. More tests may be added in the future, but this is a sensible initial test offering. This also updates MathExpressionSubject to support checking specifically for implicit multiplication (and it's now required for such cases since explicit is otherwise assumed). --- .../testing/math/MathExpressionSubject.kt | 11 +- .../util/math/AlgebraicEquationParserTest.kt | 4 +- .../math/AlgebraicExpressionParserTest.kt | 2 + .../util/math/MathExpressionParserTest.kt | 15 +- .../util/math/NumericExpressionParserTest.kt | 2151 ++++++++--------- 5 files changed, 1029 insertions(+), 1154 deletions(-) diff --git a/testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt b/testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt index c5eb730c868..674d110af7e 100644 --- a/testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt +++ b/testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt @@ -167,12 +167,19 @@ class MathExpressionSubject private constructor( * * This method will fail if the expression corresponding to the subject is not a multiplication * operation. See [BinaryOperationComparator] for example syntax. + * + * This verifies that the multiplication operation is explicit by default, and this behavior can + * be overwritten using [isImplicit]. */ - fun multiplication(init: BinaryOperationComparator.() -> Unit) { + fun multiplication(isImplicit: Boolean = false, init: BinaryOperationComparator.() -> Unit) { BinaryOperationComparator.createFromExpression( expression, expectedOperator = MathBinaryOperation.Operator.MULTIPLY - ).also(init) + ).also { + assertWithMessage( + "Expected multiplication to be ${if (isImplicit) "implicit" else "explicit" }" + ).that(expression.binaryOperation.isImplicit).isEqualTo(isImplicit) + }.also(init) } /** diff --git a/utility/src/test/java/org/oppia/android/util/math/AlgebraicEquationParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/AlgebraicEquationParserTest.kt index afdc0588f00..01f0aeddc27 100644 --- a/utility/src/test/java/org/oppia/android/util/math/AlgebraicEquationParserTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/AlgebraicEquationParserTest.kt @@ -58,7 +58,7 @@ class AlgebraicEquationParserTest { assertThat(equation).hasRightHandSideThat().hasStructureThatMatches { addition { leftOperand { - multiplication { + multiplication(isImplicit = true) { leftOperand { variable { withNameThat().isEqualTo("m") @@ -126,7 +126,7 @@ class AlgebraicEquationParserTest { } } assertThat(equation).hasRightHandSideThat().hasStructureThatMatches { - multiplication { + multiplication(isImplicit = true) { leftOperand { group { addition { diff --git a/utility/src/test/java/org/oppia/android/util/math/AlgebraicExpressionParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/AlgebraicExpressionParserTest.kt index e05d9c5906f..9a1957515bb 100644 --- a/utility/src/test/java/org/oppia/android/util/math/AlgebraicExpressionParserTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/AlgebraicExpressionParserTest.kt @@ -15,6 +15,8 @@ import org.robolectric.annotation.LooperMode @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) class AlgebraicExpressionParserTest { + // TODO: finish docs. + @Test fun testLotsOfCasesForAlgebraicExpression() { // TODO: split this up diff --git a/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt index f74e1db819a..4bfdb4199da 100644 --- a/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt @@ -42,16 +42,11 @@ import org.robolectric.annotation.LooperMode @RunWith(OppiaParameterizedTestRunner::class) @LooperMode(LooperMode.Mode.PAUSED) class MathExpressionParserTest { - @Parameter - lateinit var lhsOp: String - @Parameter - lateinit var rhsOp: String - @Parameter - lateinit var binOp: String - @Parameter - lateinit var subExp: String - @Parameter - lateinit var func: String + @Parameter lateinit var lhsOp: String + @Parameter lateinit var rhsOp: String + @Parameter lateinit var binOp: String + @Parameter lateinit var subExp: String + @Parameter lateinit var func: String @Test fun testParseNumExp_basicExpression_doesNotFail() { diff --git a/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt index 22bfefdea84..42971dc5d57 100644 --- a/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt @@ -11,77 +11,93 @@ import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingM import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult import org.robolectric.annotation.LooperMode -/** Tests for [MathExpressionParser]. */ +/** + * Tests for [MathExpressionParser]. + * + * This test suite specifically focuses on ensuring that fundamental numeric expressions, when + * parsed, yield the correct [MathExpression] structure that ensures proper order of operations (for + * both operator associativity and precedence). This suite does not cover errors (see + * [MathExpressionParserTest] for those tests), nor algebraic expressions (see + * [AlgebraicExpressionParserTest]). + */ +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) class NumericExpressionParserTest { @Test - fun testLotsOfCasesForNumericExpression() { - // TODO: split this up - // TODO: add log string generation for expressions. - val expression1 = parseNumericExpressionWithAllErrors("1") - assertThat(expression1).hasStructureThatMatches { + fun testParse_singleInteger_returnsExpressionWithConstant() { + val expression = parseNumericExpressionWithAllErrors("1") + + assertThat(expression).hasStructureThatMatches { constant { withValueThat().isIntegerThat().isEqualTo(1) } } + } + + @Test + fun testParse_singleInteger_withWhitespace_returnsExpressionWithConstant() { + val expression = parseNumericExpressionWithAllErrors(" 2 ") - val expression2 = parseNumericExpressionWithAllErrors(" 2 ") - assertThat(expression2).hasStructureThatMatches { + assertThat(expression).hasStructureThatMatches { constant { withValueThat().isIntegerThat().isEqualTo(2) } } + } + + @Test + fun testParse_singleInteger_multipleDigits_returnsExpressionWithConstant() { + val expression = parseNumericExpressionWithAllErrors("732") + + assertThat(expression).hasStructureThatMatches { + constant { + withValueThat().isIntegerThat().isEqualTo(732) + } + } + } + + @Test + fun testParse_singleRealNumber_withWhitespace_returnsExpressionWithConstant() { + val expression = parseNumericExpressionWithAllErrors(" 2.5 ") - val expression3 = parseNumericExpressionWithAllErrors(" 2.5 ") - assertThat(expression3).hasStructureThatMatches { + assertThat(expression).hasStructureThatMatches { constant { withValueThat().isIrrationalThat().isWithin(1e-5).of(2.5) } } + } - val expression4 = parseNumericExpressionWithoutOptionalErrors("2^3^2") - assertThat(expression4).hasStructureThatMatches { - exponentiation { + @Test + fun testParse_addition_returnsExpressionWithBinaryOperation() { + val expression = parseNumericExpressionWithAllErrors("1 + 2") + + assertThat(expression).hasStructureThatMatches { + addition { leftOperand { constant { - withValueThat().isIntegerThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(1) } } rightOperand { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } + constant { + withValueThat().isIntegerThat().isEqualTo(2) } } } } + } - val expression23 = parseNumericExpressionWithAllErrors("(2^3)^2") - assertThat(expression23).hasStructureThatMatches { - exponentiation { + @Test + fun testParse_subtraction_returnsExpressionWithBinaryOperation() { + val expression = parseNumericExpressionWithAllErrors("1 - 2") + + assertThat(expression).hasStructureThatMatches { + subtraction { leftOperand { - group { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } + constant { + withValueThat().isIntegerThat().isEqualTo(1) } } rightOperand { @@ -91,749 +107,245 @@ class NumericExpressionParserTest { } } } + } - val expression24 = parseNumericExpressionWithAllErrors("512/32/4") - assertThat(expression24).hasStructureThatMatches { - division { + @Test + fun testParse_subtraction_withMathSymbol_returnsExpressionWithBinaryOperation() { + val expression = parseNumericExpressionWithAllErrors("1 − 2") + + assertThat(expression).hasStructureThatMatches { + subtraction { leftOperand { - division { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(512) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(32) - } - } + constant { + withValueThat().isIntegerThat().isEqualTo(1) } } rightOperand { constant { - withValueThat().isIntegerThat().isEqualTo(4) + withValueThat().isIntegerThat().isEqualTo(2) } } } } + } - val expression25 = parseNumericExpressionWithAllErrors("512/(32/4)") - assertThat(expression25).hasStructureThatMatches { - division { + @Test + fun testParse_multiplication_returnsExpressionWithBinaryOperation() { + val expression = parseNumericExpressionWithAllErrors("1 * 2") + + assertThat(expression).hasStructureThatMatches { + multiplication { leftOperand { constant { - withValueThat().isIntegerThat().isEqualTo(512) + withValueThat().isIntegerThat().isEqualTo(1) } } rightOperand { - group { - division { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(32) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } + constant { + withValueThat().isIntegerThat().isEqualTo(2) } } } } + } - val expression5 = parseNumericExpressionWithAllErrors("sqrt(2)") - assertThat(expression5).hasStructureThatMatches { - functionCallTo(SQUARE_ROOT) { - argument { + @Test + fun testParse_multiplication_withMathSymbol_returnsExpressionWithBinaryOperation() { + val expression = parseNumericExpressionWithAllErrors("1 × 2") + + assertThat(expression).hasStructureThatMatches { + multiplication { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { constant { withValueThat().isIntegerThat().isEqualTo(2) } } } } + } - val expression6 = parseNumericExpressionWithAllErrors("732") - assertThat(expression6).hasStructureThatMatches { - constant { - withValueThat().isIntegerThat().isEqualTo(732) - } - } + @Test + fun testParse_division_returnsExpressionWithBinaryOperation() { + val expression = parseNumericExpressionWithAllErrors("1 / 2") - // Verify order of operations between higher & lower precedent operators. - val expression32 = parseNumericExpressionWithAllErrors("3+4^5") - assertThat(expression32).hasStructureThatMatches { - addition { + assertThat(expression).hasStructureThatMatches { + division { leftOperand { constant { - withValueThat().isIntegerThat().isEqualTo(3) + withValueThat().isIntegerThat().isEqualTo(1) } } rightOperand { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(5) - } - } + constant { + withValueThat().isIntegerThat().isEqualTo(2) } } } } + } - val expression7 = parseNumericExpressionWithoutOptionalErrors("3*2-3+4^7*8/3*2+7") - assertThat(expression7).hasStructureThatMatches { - // To better visualize the precedence & order of operations, see this grouped version: - // (((3*2)-3)+((((4^7)*8)/3)*2))+7. - addition { + @Test + fun testParse_division_withMathSymbol_returnsExpressionWithBinaryOperation() { + val expression = parseNumericExpressionWithAllErrors("1 ÷ 2") + + assertThat(expression).hasStructureThatMatches { + division { leftOperand { - // ((3*2)-3)+((((4^7)*8)/3)*2) - addition { - leftOperand { - // (1*2)-3 - subtraction { - leftOperand { - // 3*2 - multiplication { - leftOperand { - // 3 - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - // 2 - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - rightOperand { - // 3 - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - rightOperand { - // (((4^7)*8)/3)*2 - multiplication { - leftOperand { - // ((4^7)*8)/3 - division { - leftOperand { - // (4^7)*8 - multiplication { - leftOperand { - // 4^7 - exponentiation { - leftOperand { - // 4 - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - rightOperand { - // 7 - constant { - withValueThat().isIntegerThat().isEqualTo(7) - } - } - } - } - rightOperand { - // 8 - constant { - withValueThat().isIntegerThat().isEqualTo(8) - } - } - } - } - rightOperand { - // 3 - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - rightOperand { - // 2 - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } + constant { + withValueThat().isIntegerThat().isEqualTo(1) } } rightOperand { - // 7 constant { - withValueThat().isIntegerThat().isEqualTo(7) + withValueThat().isIntegerThat().isEqualTo(2) } } } } + } - val expression8 = parseNumericExpressionWithAllErrors("(1+2)(3+4)") - assertThat(expression8).hasStructureThatMatches { - multiplication { + @Test + fun testParse_exponentiation_returnsExpressionWithBinaryOperation() { + val expression = parseNumericExpressionWithAllErrors("1 ^ 2") + + assertThat(expression).hasStructureThatMatches { + exponentiation { leftOperand { - group { - addition { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } + constant { + withValueThat().isIntegerThat().isEqualTo(1) } } rightOperand { - group { - addition { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } + constant { + withValueThat().isIntegerThat().isEqualTo(2) } } } } + } - val expression10 = parseNumericExpressionWithAllErrors("2(1+2)") - assertThat(expression10).hasStructureThatMatches { - multiplication { - leftOperand { + @Test + fun testParse_negation_returnsExpressionWithUnaryOperation() { + val expression = parseNumericExpressionWithAllErrors("-2") + + assertThat(expression).hasStructureThatMatches { + negation { + operand { constant { withValueThat().isIntegerThat().isEqualTo(2) } } - rightOperand { - group { - addition { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - } - - val expression12 = parseNumericExpressionWithAllErrors("3sqrt(2)") - assertThat(expression12).hasStructureThatMatches { - multiplication { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - - val expression13 = parseNumericExpressionWithAllErrors("sqrt(2)*(1+2)*(3-2^5)") - assertThat(expression13).hasStructureThatMatches { - multiplication { - leftOperand { - multiplication { - leftOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - rightOperand { - group { - addition { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - } - rightOperand { - group { - subtraction { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(5) - } - } - } - } - } - } - } - } - } - - val expression58 = parseNumericExpressionWithAllErrors("sqrt(2)(1+2)(3-2^5)") - assertThat(expression58).hasStructureThatMatches { - multiplication { - leftOperand { - multiplication { - leftOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - rightOperand { - group { - addition { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - } - rightOperand { - group { - subtraction { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(5) - } - } - } - } - } - } - } } } + } - val expression14 = parseNumericExpressionWithoutOptionalErrors("((3))") - assertThat(expression14).hasStructureThatMatches { - group { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } + @Test + fun testParse_positiveUnary_withoutOptionalErrors_returnsExpressionWithUnaryOperation() { + val expression = parseNumericExpressionWithoutOptionalErrors("+2") - val expression15 = parseNumericExpressionWithoutOptionalErrors("++3") - assertThat(expression15).hasStructureThatMatches { + assertThat(expression).hasStructureThatMatches { positive { operand { - positive { - operand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - } - } - - val expression16 = parseNumericExpressionWithoutOptionalErrors("--4") - assertThat(expression16).hasStructureThatMatches { - negation { - operand { - negation { - operand { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - } - } - - val expression17 = parseNumericExpressionWithAllErrors("1+-4") - assertThat(expression17).hasStructureThatMatches { - addition { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - rightOperand { - negation { - operand { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - } - } - - val expression18 = parseNumericExpressionWithoutOptionalErrors("1++4") - assertThat(expression18).hasStructureThatMatches { - addition { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - rightOperand { - positive { - operand { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - } - } - - val expression19 = parseNumericExpressionWithAllErrors("1--4") - assertThat(expression19).hasStructureThatMatches { - subtraction { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - rightOperand { - negation { - operand { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - } - } - - val expression20 = parseNumericExpressionWithAllErrors("√2 × 7 ÷ 4") - assertThat(expression20).hasStructureThatMatches { - division { - leftOperand { - multiplication { - leftOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(7) - } - } - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - - val expression21 = parseNumericExpressionWithAllErrors("sqrt(2)sqrt(3)sqrt(4)") - // Note that this tree demonstrates left associativity. - assertThat(expression21).hasStructureThatMatches { - multiplication { - leftOperand { - multiplication { - leftOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - rightOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - } - } - rightOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - } - } - - val expression22 = parseNumericExpressionWithAllErrors("(1+2)(3-7^2)(5+-17)") - assertThat(expression22).hasStructureThatMatches { - multiplication { - leftOperand { - multiplication { - leftOperand { - // 1+2 - group { - addition { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - rightOperand { - // 3-7^2 - group { - subtraction { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(7) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - } - } - } - rightOperand { - // 5+-17 - group { - addition { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(5) - } - } - rightOperand { - negation { - operand { - constant { - withValueThat().isIntegerThat().isEqualTo(17) - } - } - } - } - } + constant { + withValueThat().isIntegerThat().isEqualTo(2) } } } } + } - val expression26 = parseNumericExpressionWithAllErrors("3^-2") - assertThat(expression26).hasStructureThatMatches { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - negation { - operand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } + @Test + fun testParse_integerInParentheses_withoutOptionalErrors_returnsExpressionWithGroup() { + val expression = parseNumericExpressionWithoutOptionalErrors("(2)") + + assertThat(expression).hasStructureThatMatches { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(2) } } } + } - val expression27 = parseNumericExpressionWithoutOptionalErrors("(3^-2)^(3^-2)") - assertThat(expression27).hasStructureThatMatches { - exponentiation { - leftOperand { - group { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - negation { - operand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } + @Test + fun testParse_inlineSquareRoot_returnsExpressionWithFunctionCall() { + val expression = parseNumericExpressionWithAllErrors("√2") + + assertThat(expression).hasStructureThatMatches { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) } } - rightOperand { - group { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - negation { - operand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } + } + } + } + + @Test + fun testParse_explicitSquareRoot_returnsExpressionWithFunctionCall() { + val expression = parseNumericExpressionWithAllErrors("sqrt(2)") + + assertThat(expression).hasStructureThatMatches { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) } } } } + } - val expression28 = parseNumericExpressionWithAllErrors("1-3^sqrt(4)") - assertThat(expression28).hasStructureThatMatches { - subtraction { + @Test + fun testParse_multiplicationAndAddition_returnsExpWithMultResolvedFirst() { + val expression = parseNumericExpressionWithAllErrors("1+2*3") + + // Multiplication is resolved first since it's higher precedence. + assertThat(expression).hasStructureThatMatches { + addition { leftOperand { constant { withValueThat().isIntegerThat().isEqualTo(1) } } rightOperand { - exponentiation { + multiplication { leftOperand { constant { - withValueThat().isIntegerThat().isEqualTo(3) + withValueThat().isIntegerThat().isEqualTo(2) } } rightOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } + constant { + withValueThat().isIntegerThat().isEqualTo(3) } } } } } } + } + + @Test + fun testParse_exponentiationAndMultiplication_returnsExpWithExponentsResolvedFirst() { + val expression = parseNumericExpressionWithAllErrors("2*3^4") - // "Hard" order of operation problems loosely based on & other problems that can often stump - // people: https://www.basic-mathematics.com/hard-order-of-operations-problems.html. - val expression29 = parseNumericExpressionWithAllErrors("3÷2*(3+4)") - assertThat(expression29).hasStructureThatMatches { + // Exponentiation is resolved first since it's higher precedence. + assertThat(expression).hasStructureThatMatches { multiplication { leftOperand { - division { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + exponentiation { leftOperand { constant { withValueThat().isIntegerThat().isEqualTo(3) @@ -841,319 +353,294 @@ class NumericExpressionParserTest { } rightOperand { constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - rightOperand { - group { - addition { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } + withValueThat().isIntegerThat().isEqualTo(4) } } } } } } + } - val expression59 = parseNumericExpressionWithAllErrors("3÷2(3+4)") - assertThat(expression59).hasStructureThatMatches { - multiplication { + @Test + fun testParse_groupAndExponentiation_returnsExpWithGroupResolvedFirst() { + val expression = parseNumericExpressionWithAllErrors("(2*3)^4") + + // Exponentiation is resolved last since the group is higher precedence. + assertThat(expression).hasStructureThatMatches { + exponentiation { leftOperand { - division { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - rightOperand { group { - addition { + multiplication { leftOperand { constant { - withValueThat().isIntegerThat().isEqualTo(3) + withValueThat().isIntegerThat().isEqualTo(2) } } rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - } - } - } - - val expression31 = parseNumericExpressionWithoutOptionalErrors("(3)(4)(5)") - assertThat(expression31).hasStructureThatMatches { - multiplication { - leftOperand { - multiplication { - leftOperand { - group { constant { withValueThat().isIntegerThat().isEqualTo(3) } } } - rightOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } } } rightOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(5) - } - } - } - } - } - - val expression33 = parseNumericExpressionWithoutOptionalErrors("2^(3)") - assertThat(expression33).hasStructureThatMatches { - exponentiation { - leftOperand { constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } + withValueThat().isIntegerThat().isEqualTo(4) } } } } + } - // Verify that implicit multiple has lower precedence than exponentiation. - val expression34 = parseNumericExpressionWithoutOptionalErrors("2^(3)(4)") - assertThat(expression34).hasStructureThatMatches { - multiplication { - leftOperand { + @Test + fun testParse_negationAndExponentiation_returnsExpWithNegationResolvedLast() { + val expression = parseNumericExpressionWithAllErrors("-3^4") + + // Exponentiation is resolved first since negation is lower precedent. + assertThat(expression).hasStructureThatMatches { + negation { + operand { exponentiation { leftOperand { constant { - withValueThat().isIntegerThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(3) } } rightOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } + constant { + withValueThat().isIntegerThat().isEqualTo(4) } } } } - rightOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } } } + } - val expression35 = parseNumericExpressionWithoutOptionalErrors("2^(3)*2^2") - assertThat(expression35).hasStructureThatMatches { - multiplication { + @Test + fun testParse_inlineSquareRootAndExponentiation_returnsExpWithSquareRootResolvedFirst() { + val expression = parseNumericExpressionWithAllErrors("√3^4") + + // The square root is resolved first since it's higher precedence. + assertThat(expression).hasStructureThatMatches { + exponentiation { leftOperand { - exponentiation { - leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } + withValueThat().isIntegerThat().isEqualTo(3) } } } } rightOperand { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } + constant { + withValueThat().isIntegerThat().isEqualTo(4) } } } } + } - // An exponentiation can be a right operand of an implicit mult if it's grouped. - val expression36 = parseNumericExpressionWithoutOptionalErrors("2^(3)(2^2)") - assertThat(expression36).hasStructureThatMatches { - multiplication { + @Test + fun testParse_additionAndSubtraction_returnsExpWithBothAtSamePrecedenceAndLeftAssociative() { + val expression = parseNumericExpressionWithAllErrors("1+2-3+4-5") + + // Addition and subtraction are resolved in-order since they're the same precedence, but they're + // resolved with left associativity (that is, left-to-right). The above expression can have its + // associativity made clearer with grouping: (((1+2)-3)+4)-5. + assertThat(expression).hasStructureThatMatches { + subtraction { leftOperand { - exponentiation { + addition { leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) + subtraction { + leftOperand { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } } } rightOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } + constant { + withValueThat().isIntegerThat().isEqualTo(4) } } } } rightOperand { - group { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } + constant { + withValueThat().isIntegerThat().isEqualTo(5) } } } } + } - val expression38 = parseNumericExpressionWithoutOptionalErrors("2^3(4)*2^3") - assertThat(expression38).hasStructureThatMatches { - // 2^3(4)*2^3 - multiplication { + @Test + fun testParse_multiplicationAndDivision_returnsExpWithBothAtSamePrecedenceAndLeftAssociative() { + val expression = parseNumericExpressionWithAllErrors("2*3/4*5/6") + + // Multiplication and division are resolved in-order since they're the same precedence, but + // they're resolved with left associativity (that is, left-to-right). The above expression can + // have its associativity made clearer with grouping: (((2*3)/4)*5)/6. + assertThat(expression).hasStructureThatMatches { + division { leftOperand { - // 2^3(4) multiplication { leftOperand { - // 2^3 - exponentiation { + division { leftOperand { - // 2 - constant { - withValueThat().isIntegerThat().isEqualTo(2) + multiplication { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } } } rightOperand { - // 3 constant { - withValueThat().isIntegerThat().isEqualTo(3) + withValueThat().isIntegerThat().isEqualTo(4) } } } } rightOperand { - // 4 - group { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } + constant { + withValueThat().isIntegerThat().isEqualTo(5) } } } } rightOperand { - // 2^3 + constant { + withValueThat().isIntegerThat().isEqualTo(6) + } + } + } + } + } + + @Test + fun testParse_nestedExponents_returnsExpWithExponentsAsRightAssociative() { + val expression = parseNumericExpressionWithoutOptionalErrors("2^3^4") + + // Exponentiation is resolved with right associativity, that is, from right to left. This is + // made clearer by grouping: 2^(3^4). Note that this is a specific choice made by the + // implementation as there's no broad consensus around exponentiation associativity for infix + // exponentiation. Right associativity is ideal since it more closely matches written-out + // exponentiation (where the nested exponent is resolved first). + assertThat(expression).hasStructureThatMatches { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { exponentiation { leftOperand { - // 2 constant { - withValueThat().isIntegerThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(3) } } rightOperand { - // 3 constant { - withValueThat().isIntegerThat().isEqualTo(3) + withValueThat().isIntegerThat().isEqualTo(4) } } } } } } + } - val expression39 = parseNumericExpressionWithAllErrors("-(1+2)") - assertThat(expression39).hasStructureThatMatches { - negation { - operand { + @Test + fun testParse_nestedExponents_withGroups_returnsExpWithForcedLeftAssociativeExponent() { + val expression = parseNumericExpressionWithAllErrors("(2^3)^4") + + // Nested exponentiation can be "forced" to be left-associative by using a group to explicitly + // change the order (since groups have higher precedence than exponents). + assertThat(expression).hasStructureThatMatches { + exponentiation { + leftOperand { group { - addition { + exponentiation { leftOperand { constant { - withValueThat().isIntegerThat().isEqualTo(1) + withValueThat().isIntegerThat().isEqualTo(2) } } rightOperand { constant { - withValueThat().isIntegerThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(3) } } } } } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } } } + } - val expression40 = parseNumericExpressionWithAllErrors("-2 (1+2)") - assertThat(expression40).hasStructureThatMatches { - // The negation happens last for parity with other common calculators. - negation { - operand { - multiplication { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) + @Test + fun testParse_negationAndInlineSquareRoot_returnsExpWithBothResolvedWithRightAssociativity() { + val expression = parseNumericExpressionWithAllErrors("√-13+-√17") + + // This expression demonstrates a few things: + // 1. Combining binary and unary operators (to demonstrate relative precedence). + // 2. That square roots are demonstrated with right associativity (i.e. the inner operator + // happens first). + assertThat(expression).hasStructureThatMatches { + addition { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + negation { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(13) + } + } } } - rightOperand { - group { - addition { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } + } + } + rightOperand { + negation { + operand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(17) } } } @@ -1162,57 +649,76 @@ class NumericExpressionParserTest { } } } + } - val expression41 = parseNumericExpressionWithoutOptionalErrors("-2^3(4)") - assertThat(expression41).hasStructureThatMatches { - negation { + @Test + fun testParse_multipleNegAndPosOps_noOptionalErrors_returnsExpWithRightAssociativeUnaryOps() { + val expression = parseNumericExpressionWithoutOptionalErrors("+--++-3") + + // This demonstrates that unary operators are resolved with right associativity (i.e. + // right-to-left with the innermost operator resolving first). + assertThat(expression).hasStructureThatMatches { + positive { operand { - multiplication { - leftOperand { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) + negation { + operand { + negation { + operand { + positive { + operand { + positive { + operand { + negation { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + } } } } } - rightOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } } } } } + } - val expression43 = parseNumericExpressionWithoutOptionalErrors("√2^2(3)") - assertThat(expression43).hasStructureThatMatches { - multiplication { + @Test + fun testParse_explicitMultiplication_returnsExpressionThatDoesNotHaveImplicitMultiplication() { + val expression = parseNumericExpressionWithAllErrors("2 * 3") + + assertThat(expression).hasStructureThatMatches { + multiplication(isImplicit = false) { leftOperand { - exponentiation { - leftOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + + @Test + fun testParse_twoAdjacentNumbers_withGroup_withoutOptionalErrors_returnsExpWithImplicitMult() { + // This isn't valid without turning off extra error detecting since redundant parentheses (i.e. + // the "(3)") trigger an error. + val expression = parseNumericExpressionWithoutOptionalErrors("2(3)") + + assertThat(expression).hasStructureThatMatches { + multiplication(isImplicit = true) { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) } } rightOperand { @@ -1224,32 +730,31 @@ class NumericExpressionParserTest { } } } + } - val expression60 = parseNumericExpressionWithoutOptionalErrors("√(2^2(3))") - assertThat(expression60).hasStructureThatMatches { - functionCallTo(SQUARE_ROOT) { - argument { + @Test + fun testParse_numberNextToParentheses_returnsExpWithImplicitMultiplication() { + val expression = parseNumericExpressionWithAllErrors("2(1+2)") + + // The parentheses indicate implicit multiplication. + assertThat(expression).hasStructureThatMatches { + multiplication(isImplicit = true) { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { group { - multiplication { + addition { leftOperand { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } + constant { + withValueThat().isIntegerThat().isEqualTo(1) } } rightOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } + constant { + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -1257,26 +762,42 @@ class NumericExpressionParserTest { } } } + } - val expression42 = parseNumericExpressionWithAllErrors("-2*-2") - // Note that the following structure is not the same as (-2)*(-2) since unary negation has - // higher precedence than multiplication, so it's first & recurses to include the entire - // multiplication expression. - assertThat(expression42).hasStructureThatMatches { - negation { - operand { - multiplication { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) + @Test + fun testParse_twoAdjacentParentheticalAdditions_returnsExpWithImplicitlyMultipliedSubExps() { + val expression = parseNumericExpressionWithAllErrors("(1+2)(3+4)") + + // Two adjacent expressions may sometimes be considered implicitly multiplied. + assertThat(expression).hasStructureThatMatches { + multiplication(isImplicit = true) { + leftOperand { + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + rightOperand { + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } } - } - rightOperand { - negation { - operand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) } } } @@ -1284,50 +805,69 @@ class NumericExpressionParserTest { } } } + } - // TODO: Here & elsewhere, fix the fact that this is actually a valid use of single-term - // parentheses (there's a bug in the current error detection logic). - val expression44 = parseNumericExpressionWithoutOptionalErrors("2(2)") - assertThat(expression44).hasStructureThatMatches { - multiplication { + @Test + fun testParse_numberNextToInlineSquareRoot_returnsExpWithImplicitMultiplication() { + val expression = parseNumericExpressionWithAllErrors("2√3") + + // Square roots are treated as markers for implicit multiplication. + assertThat(expression).hasStructureThatMatches { + multiplication(isImplicit = true) { leftOperand { constant { withValueThat().isIntegerThat().isEqualTo(2) } } rightOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(2) + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } } } } } } + } - val expression45 = parseNumericExpressionWithAllErrors("2sqrt(2)") - assertThat(expression45).hasStructureThatMatches { - multiplication { + @Test + fun testParse_twoAdjacentInlineSquareRoots_returnsExpWithImplicitMultiplication() { + val expression = parseNumericExpressionWithAllErrors("√2√3") + + // Square roots are treated as markers for implicit multiplication. + assertThat(expression).hasStructureThatMatches { + multiplication(isImplicit = true) { leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } } } rightOperand { functionCallTo(SQUARE_ROOT) { argument { constant { - withValueThat().isIntegerThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(3) } } } } } } + } - val expression46 = parseNumericExpressionWithAllErrors("2√2") - assertThat(expression46).hasStructureThatMatches { - multiplication { + @Test + fun testParse_numberNextToSquareRoot_returnsExpWithImplicitMultiplication() { + val expression = parseNumericExpressionWithAllErrors("2sqrt(2)") + + // Functions are treated as markers for implicit multiplication. + assertThat(expression).hasStructureThatMatches { + multiplication(isImplicit = true) { leftOperand { constant { withValueThat().isIntegerThat().isEqualTo(2) @@ -1344,30 +884,45 @@ class NumericExpressionParserTest { } } } + } - val expression47 = parseNumericExpressionWithoutOptionalErrors("(2)(2)") - assertThat(expression47).hasStructureThatMatches { - multiplication { + @Test + fun testParse_twoAdjacentSquareRoots_returnsExpWithImplicitMultiplication() { + val expression = parseNumericExpressionWithAllErrors("sqrt(2)sqrt(2)") + + // Functions are treated as markers for implicit multiplication. + assertThat(expression).hasStructureThatMatches { + multiplication(isImplicit = true) { leftOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(2) + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } } } } rightOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(2) + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } } } } } } + } - val expression48 = parseNumericExpressionWithoutOptionalErrors("sqrt(2)(2)") - assertThat(expression48).hasStructureThatMatches { - multiplication { + @Test + fun testParse_squareRootNextToNumber_withoutOptionalErrors_returnsExpWithImplicitMult() { + val expression = parseNumericExpressionWithoutOptionalErrors("sqrt(2)(3)") + + // The parser recognizes this case as implicit multiplication, but additional errors are + // triggered since the "(3)" has redundant parentheses. + assertThat(expression).hasStructureThatMatches { + multiplication(isImplicit = true) { leftOperand { functionCallTo(SQUARE_ROOT) { argument { @@ -1380,111 +935,382 @@ class NumericExpressionParserTest { rightOperand { group { constant { - withValueThat().isIntegerThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(3) } } } } } + } - val expression49 = parseNumericExpressionWithAllErrors("sqrt(2)sqrt(2)") - assertThat(expression49).hasStructureThatMatches { - multiplication { + @Test + fun testParse_multipleAdjacentInlineSquareRoots_returnsExpWithLeftAssociativeImplicitMult() { + val expression = parseNumericExpressionWithAllErrors("√2√3√4") + + // Implicit multiplication is left-associative, i.e.: (√2*√3)*√4. + assertThat(expression).hasStructureThatMatches { + multiplication(isImplicit = true) { leftOperand { + multiplication(isImplicit = true) { + leftOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + } + rightOperand { functionCallTo(SQUARE_ROOT) { argument { constant { - withValueThat().isIntegerThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(4) } } } } + } + } + } + + @Test + fun testParse_implicitMultiplicationAndAddition_returnsExpWithImplicitMultAtHigherPrecedence() { + val expression = parseNumericExpressionWithAllErrors("1+3√2") + + // Implicit multiplication is higher precedence so it's evaluated first. + assertThat(expression).hasStructureThatMatches { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } rightOperand { - functionCallTo(SQUARE_ROOT) { - argument { + multiplication(isImplicit = true) { + leftOperand { constant { - withValueThat().isIntegerThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + functionCallTo(SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } } } } } } } + } - val expression50 = parseNumericExpressionWithAllErrors("√2√2") - assertThat(expression50).hasStructureThatMatches { + @Test + fun testParse_implicitMultiplicationAndDivision_returnsExpWithSamePrecedence() { + // "Hard" order of operation problem loosely based on & other problems that can often stump + // people: https://www.basic-mathematics.com/hard-order-of-operations-problems.html, and that + // can also break parsers that incorrectly set implicit multiplication to a higher precedence. + val expression = parseNumericExpressionWithAllErrors("3÷2(3+4)*7") + + // The parser should ensure that implicit multiplication & explicit multiplication/division are + // the same precedence to ensure that evaluation is predictable. If implicit multiplication is + // higher precedence, then the expression above would be evaluated 0.0306 to rather than 73.5 + // (the correct value per multiple calculators). Below demonstrates this by showing that + // implicit multiplication follows the same associative rules as explicit + // multiplication/division (and not taking a higher-priority execution order). For simplicity, + // the expression above can be thought of as the equivalent: ((3÷2)*(3+4))*7. + assertThat(expression).hasStructureThatMatches { multiplication { leftOperand { - functionCallTo(SQUARE_ROOT) { - argument { + multiplication(isImplicit = true) { + leftOperand { + division { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + group { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(7) + } + } + } + } + } + + @Test + fun testParse_implicitMultAndExponents_noOptionalErrors_returnsExpWithImplicitMultEvaledSecond() { + val expression = parseNumericExpressionWithoutOptionalErrors("2^(3)(4)") + + // Implicit multiplication is lower precedent than exponentiation (and thus evaluated last). + // Note that the expression above violates the redundant parentheses check hence why optional + // errors are disabled. + assertThat(expression).hasStructureThatMatches { + multiplication(isImplicit = true) { + leftOperand { + exponentiation { + leftOperand { constant { withValueThat().isIntegerThat().isEqualTo(2) } } + rightOperand { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } } } rightOperand { - functionCallTo(SQUARE_ROOT) { - argument { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + + @Test + fun testParse_adjacentExponentsWithGroup_returnsExpWithImplicitMult() { + val expression = parseNumericExpressionWithAllErrors("2^3(4^5)") + + // Two adjacent exponentiations can be implicitly multiplied if one is grouped. + assertThat(expression).hasStructureThatMatches { + multiplication(isImplicit = true) { + leftOperand { + exponentiation { + leftOperand { constant { withValueThat().isIntegerThat().isEqualTo(2) } } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + rightOperand { + group { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + } } } } } + } - val expression51 = parseNumericExpressionWithoutOptionalErrors("(2)(2)(2)") - assertThat(expression51).hasStructureThatMatches { - multiplication { + @Test + fun testParse_complexExpression_returnsExpWithCorrectOrderOfOperations() { + val expression = parseNumericExpressionWithAllErrors("3*2-3+4^3*8/3*2+7") + + assertThat(expression).hasStructureThatMatches { + // To better visualize the precedence & order of operations, see this grouped version: + // (((3*2)-3)+((((4^3)*8)/3)*2))+7. + addition { leftOperand { - multiplication { + // ((3*2)-3)+((((4^3)*8)/3)*2) + addition { leftOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(2) + // (1*2)-3 + subtraction { + leftOperand { + // 3*2 + multiplication { + leftOperand { + // 3 + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + // 2 + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + rightOperand { + // 3 + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + rightOperand { + // (((4^3)*8)/3)*2 + multiplication { + leftOperand { + // ((4^3)*8)/3 + division { + leftOperand { + // (4^3)*8 + multiplication { + leftOperand { + // 4^3 + exponentiation { + leftOperand { + // 4 + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + rightOperand { + // 3 + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + rightOperand { + // 8 + constant { + withValueThat().isIntegerThat().isEqualTo(8) + } + } + } + } + rightOperand { + // 3 + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } } - } - } - rightOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(2) + rightOperand { + // 2 + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } } } } } } rightOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } + // 7 + constant { + withValueThat().isIntegerThat().isEqualTo(7) } } } } + } - val expression52 = parseNumericExpressionWithAllErrors("sqrt(2)sqrt(2)sqrt(2)") - assertThat(expression52).hasStructureThatMatches { - multiplication { + @Test + fun testParse_compoundExpressionWithMultipleOperations_returnsExpWithCorrectOperationOrder() { + val expression = parseNumericExpressionWithAllErrors("sqrt(1+2)(3-7^2)(5+-17)") + + assertThat(expression).hasStructureThatMatches { + // sqrt(1+2)(3-7^2)(5+-17) -> (sqrt(1+2)*(3-7^2))*(5+-17) + multiplication(isImplicit = true) { leftOperand { - multiplication { + // sqrt(1+2)*(3-7^2) + multiplication(isImplicit = true) { leftOperand { + // sqrt(1+2) functionCallTo(SQUARE_ROOT) { argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } } } } } rightOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) + // (3-7^2) + group { + subtraction { + // 3 + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + // 7^2 + exponentiation { + leftOperand { + // 7 + constant { + withValueThat().isIntegerThat().isEqualTo(7) + } + } + rightOperand { + // 2 + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } } } } @@ -1492,56 +1318,69 @@ class NumericExpressionParserTest { } } rightOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) + // (5+-17) + group { + addition { + leftOperand { + // 5 + constant { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + rightOperand { + // -17 + negation { + operand { + // 17 + constant { + withValueThat().isIntegerThat().isEqualTo(17) + } + } + } } } } } } } + } - val expression53 = parseNumericExpressionWithAllErrors("√2√2√2") - assertThat(expression53).hasStructureThatMatches { - multiplication { - leftOperand { + @Test + fun testParse_multiplicationOfNegations_returnsExpWithCorrectStructure() { + val expression = parseNumericExpressionWithAllErrors("-2*-3") + + // Note that the following structure is not the same as (-2)*(-2) since unary negation has + // lower precedence than multiplication, so it's computed as first with its operand being the + // multiplication expression. + assertThat(expression).hasStructureThatMatches { + negation { + operand { multiplication { leftOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } + constant { + withValueThat().isIntegerThat().isEqualTo(2) } } rightOperand { - functionCallTo(SQUARE_ROOT) { - argument { + negation { + operand { constant { - withValueThat().isIntegerThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(3) } } } } } } - rightOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } } } + } - val expression54 = parseNumericExpressionWithAllErrors("2*2/-4+7*2") - assertThat(expression54).hasStructureThatMatches { + @Test + fun testParse_multipleOperations_withSubsequentBinaryAndUnaryOps_returnsExpWithCorrectOpOrder() { + val expression = parseNumericExpressionWithAllErrors("2*2/-4+7*2") + + assertThat(expression).hasStructureThatMatches { // 2*2/-4+7*2 -> ((2*2)/(-4))+(7*2) addition { leftOperand { @@ -1596,83 +1435,117 @@ class NumericExpressionParserTest { } } } + } - val expression55 = parseNumericExpressionWithAllErrors("3/(1-2)") - assertThat(expression55).hasStructureThatMatches { - division { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - group { - subtraction { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - } + @Test + fun testParse_operationsWithNonIntegersAndGroups_returnsExpWithCorrectOperationOrder() { + val expression = parseNumericExpressionWithAllErrors("7*(3.14/0.76+8.4)^(3.8+1/(2+2/(7.4+1)))") - val expression56 = parseNumericExpressionWithoutOptionalErrors("(3)/(1-2)") - assertThat(expression56).hasStructureThatMatches { - division { + assertThat(expression).hasStructureThatMatches { + // 7*(3.14/0.76+8.4)^(3.8+1/(2+2/(7.4+1))) -> 7*(((3.14/0.76)+8.4))^(3.8+(1/(2+(2/(7.4+1))))) + multiplication { leftOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } + // 7 + constant { + withValueThat().isIntegerThat().isEqualTo(7) } } rightOperand { - group { - subtraction { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) + // (3.14/0.76)+8.4))^(3.8+(1/(2+(2/(7.4+1))) + exponentiation { + leftOperand { + // ((3.14/0.76)+8.4) + group { + addition { + leftOperand { + // 3.14/0.76 + division { + leftOperand { + // 3.14 + constant { + withValueThat().isIrrationalThat().isWithin(1e-5).of(3.14) + } + } + rightOperand { + // 0.76 + constant { + withValueThat().isIrrationalThat().isWithin(1e-5).of(0.76) + } + } + } + } + rightOperand { + // 8.4 + constant { + withValueThat().isIrrationalThat().isWithin(1e-5).of(8.4) + } + } } } } - } - } - } - } - - val expression57 = parseNumericExpressionWithoutOptionalErrors("3/((1-2))") - assertThat(expression57).hasStructureThatMatches { - division { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - group { - group { - subtraction { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(1) + rightOperand { + // (3.8+(1/(2+(2/(7.4+1))))) + group { + addition { + leftOperand { + // 3.8 + constant { + withValueThat().isIrrationalThat().isWithin(1e-5).of(3.8) + } } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) + rightOperand { + // 1/(2+(2/(7.4+1))) + division { + leftOperand { + // 1 + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + // (2+(2/(7.4+1))) + group { + addition { + leftOperand { + // 2 + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + // 2/(7.4+1) + division { + leftOperand { + // 2 + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + // (7.4+1) + group { + addition { + leftOperand { + // 7.4 + constant { + withValueThat().isIrrationalThat().isWithin(1e-5).of(7.4) + } + } + rightOperand { + // 1 + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + } + } + } + } + } + } + } + } + } } } } @@ -1681,35 +1554,33 @@ class NumericExpressionParserTest { } } } + } - // TODO: add others, including tests for malformed expressions throughout the parser & - // tokenizer. + @Test + fun testParse_twoSimilarExpressions_differedByWhitespace_areEqual() { + val expression1 = parseNumericExpressionWithAllErrors("3*2-3+4^3*8/3*2+7") + val expression2 = parseNumericExpressionWithAllErrors(" 3 * 2 - 3 + 4^ 3* 8/ 3 *2 + 7 ") + + // The parser should ensure that no positional information is kept (which result in the two + // expressions being equal). Note that not all expressions can be guaranteed to be exactly equal + // (particularly if they contain real values since rounding errors during parsing may cause + // inconsistencies). + assertThat(expression1).isEqualTo(expression2) } private companion object { - // TODO: fix helper API. - - private fun parseNumericExpressionWithoutOptionalErrors(expression: String): MathExpression { - val result = - parseNumericExpressionInternal( - expression, ErrorCheckingMode.REQUIRED_ONLY - ) - return (result as MathParsingResult.Success).result - } + private fun parseNumericExpressionWithoutOptionalErrors(expression: String): MathExpression = + parseNumericExpressionInternal(expression, ErrorCheckingMode.REQUIRED_ONLY) - private fun parseNumericExpressionWithAllErrors(expression: String): MathExpression { - val result = - parseNumericExpressionInternal( - expression, ErrorCheckingMode.ALL_ERRORS - ) - return (result as MathParsingResult.Success).result - } + private fun parseNumericExpressionWithAllErrors(expression: String): MathExpression = + parseNumericExpressionInternal(expression, ErrorCheckingMode.ALL_ERRORS) private fun parseNumericExpressionInternal( - expression: String, - errorCheckingMode: ErrorCheckingMode - ): MathParsingResult { - return MathExpressionParser.parseNumericExpression(expression, errorCheckingMode) + expression: String, errorCheckingMode: ErrorCheckingMode + ): MathExpression { + val result = MathExpressionParser.parseNumericExpression(expression, errorCheckingMode) + assertThat(result).isInstanceOf(MathParsingResult.Success::class.java) + return (result as MathParsingResult.Success).result } } } From 984e74019425da9e6161aa2b8ab777481c227c7d Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 21 Jan 2022 21:36:01 -0800 Subject: [PATCH 058/162] Finish algebraic expression tests. These largely rely on numeric expression tests (since they focus on verifying specific variable scenarios). --- .../math/AlgebraicExpressionParserTest.kt | 2158 +++++------------ 1 file changed, 620 insertions(+), 1538 deletions(-) diff --git a/utility/src/test/java/org/oppia/android/util/math/AlgebraicExpressionParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/AlgebraicExpressionParserTest.kt index 9a1957515bb..7950c52b336 100644 --- a/utility/src/test/java/org/oppia/android/util/math/AlgebraicExpressionParserTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/AlgebraicExpressionParserTest.kt @@ -11,388 +11,226 @@ import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingM import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult import org.robolectric.annotation.LooperMode -/** Tests for [MathExpressionParser]. */ +/** + * Tests for [MathExpressionParser]. + * + * This test suite specifically focuses on verifying expressions that include variables. It largely + * assumes that core properties around precedence and associativity, and general handling of numeric + * expressions are correct through the tests managed by [NumericExpressionParserTest]. This is a + * valid approach since these tests are aware that an implementation is shared between the two. In + * the event the implementations are forked in the future, this test suite should correspondingly be + * updated to include tests for the core portions of parsing. For the most part, this suite assumes + * that its tests properly verify that variables can be a step-in replacement for numbers when + * considering numeric expression tests (with some special exceptions around implicit multiplication + * to enable polynomial syntax). + * + * This suite does not test errors--see [MathExpressionParserTest] for those tests. Further, it does + * not test algebraic equations (see [AlgebraicEquationParserTest] for those tests). + * + * This test suite largely focuses on demonstrating polynomial syntax since that's the expected + * principal use of algebraic expressions and equations. + */ +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) class AlgebraicExpressionParserTest { - // TODO: finish docs. - @Test - fun testLotsOfCasesForAlgebraicExpression() { - // TODO: split this up - // TODO: add log string generation for expressions. - val expression1 = parseAlgebraicExpressionWithAllErrors("1") - assertThat(expression1).hasStructureThatMatches { - constant { - withValueThat().isIntegerThat().isEqualTo(1) - } - } + fun testParse_variable_returnsExpWithVariable() { + val expression = parseAlgebraicExpressionWithAllErrors("x") - val expression61 = parseAlgebraicExpressionWithAllErrors("x") - assertThat(expression61).hasStructureThatMatches { + assertThat(expression).hasStructureThatMatches { variable { withNameThat().isEqualTo("x") } } + } - val expression2 = parseAlgebraicExpressionWithAllErrors(" 2 ") - assertThat(expression2).hasStructureThatMatches { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - - val expression3 = parseAlgebraicExpressionWithAllErrors(" 2.5 ") - assertThat(expression3).hasStructureThatMatches { - constant { - withValueThat().isIrrationalThat().isWithin(1e-5).of(2.5) - } - } - - val expression62 = parseAlgebraicExpressionWithAllErrors(" y ") - assertThat(expression62).hasStructureThatMatches { - variable { - withNameThat().isEqualTo("y") - } - } + @Test + fun testParse_variablePlusInteger_returnsExpWithVariableBasedBinaryOperation() { + val expression = parseAlgebraicExpressionWithAllErrors("x+1") - val expression63 = parseAlgebraicExpressionWithAllErrors(" z x ") - assertThat(expression63).hasStructureThatMatches { - multiplication { + assertThat(expression).hasStructureThatMatches { + addition { leftOperand { variable { - withNameThat().isEqualTo("z") + withNameThat().isEqualTo("x") } } rightOperand { - variable { - withNameThat().isEqualTo("x") + constant { + withValueThat().isIntegerThat().isEqualTo(1) } } } } + } - val expression4 = parseAlgebraicExpressionWithoutOptionalErrors("2^3^2") - assertThat(expression4).hasStructureThatMatches { - exponentiation { + @Test + fun testParse_integerPlusVariable_returnsExpWithVariableBasedBinaryOperation() { + val expression = parseAlgebraicExpressionWithAllErrors("1+x") + + assertThat(expression).hasStructureThatMatches { + addition { leftOperand { constant { - withValueThat().isIntegerThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(1) } } rightOperand { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } + variable { + withNameThat().isEqualTo("x") } } } } + } - val expression23 = parseAlgebraicExpressionWithAllErrors("(2^3)^2") - assertThat(expression23).hasStructureThatMatches { - exponentiation { + @Test + fun testParse_variableMinusInteger_returnsExpWithVariableBasedBinaryOperation() { + val expression = parseAlgebraicExpressionWithAllErrors("x-1") + + assertThat(expression).hasStructureThatMatches { + subtraction { leftOperand { - group { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } + variable { + withNameThat().isEqualTo("x") } } rightOperand { constant { - withValueThat().isIntegerThat().isEqualTo(2) + withValueThat().isIntegerThat().isEqualTo(1) } } } } + } - val expression24 = parseAlgebraicExpressionWithAllErrors("512/32/4") - assertThat(expression24).hasStructureThatMatches { - division { + @Test + fun testParse_variableMinusInteger_mathSymbol_returnsExpWithVariableBasedBinaryOperation() { + val expression = parseAlgebraicExpressionWithAllErrors("x−1") + + assertThat(expression).hasStructureThatMatches { + subtraction { leftOperand { - division { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(512) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(32) - } - } + variable { + withNameThat().isEqualTo("x") } } rightOperand { constant { - withValueThat().isIntegerThat().isEqualTo(4) + withValueThat().isIntegerThat().isEqualTo(1) } } } } + } - val expression25 = parseAlgebraicExpressionWithAllErrors("512/(32/4)") - assertThat(expression25).hasStructureThatMatches { - division { + @Test + fun testParse_integerMinusVariable_returnsExpWithVariableBasedBinaryOperation() { + val expression = parseAlgebraicExpressionWithAllErrors("1-x") + + assertThat(expression).hasStructureThatMatches { + subtraction { leftOperand { constant { - withValueThat().isIntegerThat().isEqualTo(512) + withValueThat().isIntegerThat().isEqualTo(1) } } rightOperand { - group { - division { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(32) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } + variable { + withNameThat().isEqualTo("x") } } } } + } - val expression5 = parseAlgebraicExpressionWithAllErrors("sqrt(2)") - assertThat(expression5).hasStructureThatMatches { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } + @Test + fun testParse_integerMinusVariable_mathSymbol_returnsExpWithVariableBasedBinaryOperation() { + val expression = parseAlgebraicExpressionWithAllErrors("1−x") - val expression64 = parseAlgebraicExpressionWithoutOptionalErrors("xyz(2)") - assertThat(expression64).hasStructureThatMatches { - multiplication { + assertThat(expression).hasStructureThatMatches { + subtraction { leftOperand { - multiplication { - leftOperand { - multiplication { - leftOperand { - variable { - withNameThat().isEqualTo("x") - } - } - rightOperand { - variable { - withNameThat().isEqualTo("y") - } - } - } - } - rightOperand { - variable { - withNameThat().isEqualTo("z") - } - } + constant { + withValueThat().isIntegerThat().isEqualTo(1) } } rightOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } + variable { + withNameThat().isEqualTo("x") } } } } + } - val expression6 = parseAlgebraicExpressionWithAllErrors("732") - assertThat(expression6).hasStructureThatMatches { - constant { - withValueThat().isIntegerThat().isEqualTo(732) - } - } + @Test + fun testParse_variableTimesInteger_returnsExpWithVariableBasedBinaryOperation() { + val expression = parseAlgebraicExpressionWithAllErrors("x*2") - // Verify order of operations between higher & lower precedent operators. - val expression32 = parseAlgebraicExpressionWithAllErrors("3+4^5") - assertThat(expression32).hasStructureThatMatches { - addition { + assertThat(expression).hasStructureThatMatches { + multiplication { leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) + variable { + withNameThat().isEqualTo("x") } } rightOperand { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(5) - } - } + constant { + withValueThat().isIntegerThat().isEqualTo(2) } } } } + } - val expression7 = parseAlgebraicExpressionWithoutOptionalErrors("3*2-3+4^7*8/3*2+7") - assertThat(expression7).hasStructureThatMatches { - // To better visualize the precedence & order of operations, see this grouped version: - // (((3*2)-3)+((((4^7)*8)/3)*2))+7. - addition { + @Test + fun testParse_variableTimesInteger_mathSymbol_returnsExpWithVariableBasedBinaryOperation() { + val expression = parseAlgebraicExpressionWithAllErrors("x×2") + + assertThat(expression).hasStructureThatMatches { + multiplication { leftOperand { - // ((3*2)-3)+((((4^7)*8)/3)*2) - addition { - leftOperand { - // (1*2)-3 - subtraction { - leftOperand { - // 3*2 - multiplication { - leftOperand { - // 3 - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - // 2 - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - rightOperand { - // 3 - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - rightOperand { - // (((4^7)*8)/3)*2 - multiplication { - leftOperand { - // ((4^7)*8)/3 - division { - leftOperand { - // (4^7)*8 - multiplication { - leftOperand { - // 4^7 - exponentiation { - leftOperand { - // 4 - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - rightOperand { - // 7 - constant { - withValueThat().isIntegerThat().isEqualTo(7) - } - } - } - } - rightOperand { - // 8 - constant { - withValueThat().isIntegerThat().isEqualTo(8) - } - } - } - } - rightOperand { - // 3 - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - rightOperand { - // 2 - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } + variable { + withNameThat().isEqualTo("x") } } rightOperand { - // 7 constant { - withValueThat().isIntegerThat().isEqualTo(7) + withValueThat().isIntegerThat().isEqualTo(2) } } } } + } + + @Test + fun testParse_integerTimesVariable_returnsExpWithVariableBasedBinaryOperation() { + val expression = parseAlgebraicExpressionWithAllErrors("2*x") - val expression8 = parseAlgebraicExpressionWithAllErrors("(1+2)(3+4)") - assertThat(expression8).hasStructureThatMatches { + assertThat(expression).hasStructureThatMatches { multiplication { leftOperand { - group { - addition { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } + constant { + withValueThat().isIntegerThat().isEqualTo(2) } } rightOperand { - group { - addition { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } + variable { + withNameThat().isEqualTo("x") } } } } + } - val expression10 = parseAlgebraicExpressionWithAllErrors("2(1+2)") - assertThat(expression10).hasStructureThatMatches { + @Test + fun testParse_integerTimesVariable_mathSymbol_returnsExpWithVariableBasedBinaryOperation() { + val expression = parseAlgebraicExpressionWithAllErrors("2×x") + + assertThat(expression).hasStructureThatMatches { multiplication { leftOperand { constant { @@ -400,176 +238,189 @@ class AlgebraicExpressionParserTest { } } rightOperand { - group { - addition { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } + variable { + withNameThat().isEqualTo("x") } } } } + } - val expression12 = parseAlgebraicExpressionWithAllErrors("3sqrt(2)") - assertThat(expression12).hasStructureThatMatches { - multiplication { + @Test + fun testParse_variableDividedByInteger_returnsExpWithVariableBasedBinaryOperation() { + val expression = parseAlgebraicExpressionWithAllErrors("x/2") + + assertThat(expression).hasStructureThatMatches { + division { leftOperand { + variable { + withNameThat().isEqualTo("x") + } + } + rightOperand { constant { - withValueThat().isIntegerThat().isEqualTo(3) + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + + @Test + fun testParse_variableDividedByInteger_mathSymbol_returnsExpWithVariableBasedBinaryOperation() { + val expression = parseAlgebraicExpressionWithAllErrors("x÷2") + + assertThat(expression).hasStructureThatMatches { + division { + leftOperand { + variable { + withNameThat().isEqualTo("x") } } rightOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } + constant { + withValueThat().isIntegerThat().isEqualTo(2) } } } } + } - val expression65 = parseAlgebraicExpressionWithAllErrors("xsqrt(2)") - assertThat(expression65).hasStructureThatMatches { - multiplication { + @Test + fun testParse_integerDividedByVariable_returnsExpWithVariableBasedBinaryOperation() { + val expression = parseAlgebraicExpressionWithAllErrors("1/x") + + assertThat(expression).hasStructureThatMatches { + division { leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { variable { withNameThat().isEqualTo("x") } } + } + } + } + + @Test + fun testParse_integerDividedByVariable_mathSymbol_returnsExpWithVariableBasedBinaryOperation() { + val expression = parseAlgebraicExpressionWithAllErrors("1÷x") + + assertThat(expression).hasStructureThatMatches { + division { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } rightOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } + variable { + withNameThat().isEqualTo("x") } } } } + } - val expression13 = parseAlgebraicExpressionWithAllErrors("sqrt(2)*(1+2)*(3-2^5)") - assertThat(expression13).hasStructureThatMatches { - multiplication { + @Test + fun testParse_variableRaisedToInteger_returnsExpWithVariableBasedBinaryOperation() { + val expression = parseAlgebraicExpressionWithAllErrors("x^2") + + // This demonstrates basic quadratic polynomial syntax. + assertThat(expression).hasStructureThatMatches { + exponentiation { leftOperand { - multiplication { - leftOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - rightOperand { - group { - addition { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } + variable { + withNameThat().isEqualTo("x") } } rightOperand { - group { - subtraction { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(5) - } - } - } - } - } + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + + @Test + fun testParse_intRaisedToVariable_noOptionalErrors_returnsExpWithVariableBasedBinaryOperation() { + // Note that optional errors prohibit variables in exponents. + val expression = parseAlgebraicExpressionWithoutOptionalErrors("2^x") + + assertThat(expression).hasStructureThatMatches { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + variable { + withNameThat().isEqualTo("x") + } + } + } + } + } + + @Test + fun testParse_negatedVariable_returnsExpWithVariableBasedUnaryOperation() { + val expression = parseAlgebraicExpressionWithAllErrors("-x") + + assertThat(expression).hasStructureThatMatches { + negation { + operand { + variable { + withNameThat().isEqualTo("x") + } + } + } + } + } + + @Test + fun testParse_positiveVariable_withoutOptionalErrors_returnsExpWithVariableBasedUnaryOperation() { + // Note that optional errors prohibit unary positive operations. + val expression = parseAlgebraicExpressionWithoutOptionalErrors("+x") + + assertThat(expression).hasStructureThatMatches { + positive { + operand { + variable { + withNameThat().isEqualTo("x") } } } } + } + + @Test + fun testParse_variableInGroup_returnsExpWithVariableInGroup() { + val expression = parseAlgebraicExpressionWithAllErrors("2*(1+x)") - val expression58 = parseAlgebraicExpressionWithAllErrors("sqrt(2)(1+2)(3-2^5)") - assertThat(expression58).hasStructureThatMatches { + assertThat(expression).hasStructureThatMatches { multiplication { leftOperand { - multiplication { - leftOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - rightOperand { - group { - addition { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } + constant { + withValueThat().isIntegerThat().isEqualTo(2) } } rightOperand { group { - subtraction { + addition { leftOperand { constant { - withValueThat().isIntegerThat().isEqualTo(3) + withValueThat().isIntegerThat().isEqualTo(1) } } rightOperand { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(5) - } - } + variable { + withNameThat().isEqualTo("x") } } } @@ -577,1125 +428,360 @@ class AlgebraicExpressionParserTest { } } } + } - val expression14 = parseAlgebraicExpressionWithoutOptionalErrors("((3))") - assertThat(expression14).hasStructureThatMatches { - group { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(3) + @Test + fun testParse_inlineSquareRootOfX_returnsExpWithVariableInFunctionArgument() { + val expression = parseAlgebraicExpressionWithAllErrors("√x") + + assertThat(expression).hasStructureThatMatches { + functionCallTo(SQUARE_ROOT) { + argument { + variable { + withNameThat().isEqualTo("x") } } } } + } - val expression15 = parseAlgebraicExpressionWithoutOptionalErrors("++3") - assertThat(expression15).hasStructureThatMatches { - positive { - operand { - positive { - operand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } + @Test + fun testParse_squareRootOfX_returnsExpWithVariableInFunctionArgument() { + val expression = parseAlgebraicExpressionWithAllErrors("sqrt(x)") + + assertThat(expression).hasStructureThatMatches { + functionCallTo(SQUARE_ROOT) { + argument { + variable { + withNameThat().isEqualTo("x") + } + } + } + } + } + + @Test + fun testParse_integerAndVariable_returnsExpWithImplicitMultiplication() { + val expression = parseAlgebraicExpressionWithAllErrors("2x") + + // This also demonstrates basic polynomial syntax for linear equations. + assertThat(expression).hasStructureThatMatches { + multiplication(isImplicit = true) { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + variable { + withNameThat().isEqualTo("x") } } } } + } + + @Test + fun testParse_negatedIntegerAndVariable_returnsExpWithImplicitMultiplicationWithUnary() { + val expression = parseAlgebraicExpressionWithAllErrors("-2x") - val expression16 = parseAlgebraicExpressionWithoutOptionalErrors("--4") - assertThat(expression16).hasStructureThatMatches { + // Similar to the previous test, but this ensures negation ordering (relative to variables & + // implicit multiplication). + assertThat(expression).hasStructureThatMatches { negation { operand { - negation { - operand { + multiplication(isImplicit = true) { + leftOperand { constant { - withValueThat().isIntegerThat().isEqualTo(4) + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + variable { + withNameThat().isEqualTo("x") } } } } } } + } - val expression17 = parseAlgebraicExpressionWithAllErrors("1+-4") - assertThat(expression17).hasStructureThatMatches { - addition { + @Test + fun testParse_xInlineSquareRootOfInteger_returnsExpWithImplicitMultiplication() { + val expression = parseAlgebraicExpressionWithAllErrors("x√2") + + // A variable next to a square root indicates implicit multiplication. + assertThat(expression).hasStructureThatMatches { + multiplication(isImplicit = true) { leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(1) + variable { + withNameThat().isEqualTo("x") } } rightOperand { - negation { - operand { + functionCallTo(SQUARE_ROOT) { + argument { constant { - withValueThat().isIntegerThat().isEqualTo(4) + withValueThat().isIntegerThat().isEqualTo(2) } } } } } } + } - val expression18 = parseAlgebraicExpressionWithoutOptionalErrors("1++4") - assertThat(expression18).hasStructureThatMatches { - addition { + @Test + fun testParse_xSquareRootOfInteger_returnsExpWithImplicitMultiplication() { + val expression = parseAlgebraicExpressionWithAllErrors("xsqrt(2)") + + // Even with 'sqrt' and x being tightly together, the parser still sees a distinct variable and + // function. + assertThat(expression).hasStructureThatMatches { + multiplication(isImplicit = true) { leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(1) + variable { + withNameThat().isEqualTo("x") } } rightOperand { - positive { - operand { + functionCallTo(SQUARE_ROOT) { + argument { constant { - withValueThat().isIntegerThat().isEqualTo(4) + withValueThat().isIntegerThat().isEqualTo(2) } } } } } } + } - val expression19 = parseAlgebraicExpressionWithAllErrors("1--4") - assertThat(expression19).hasStructureThatMatches { - subtraction { + @Test + fun testParse_variableTimesVariable_same_returnsExpWithMultiedVariables() { + val expression = parseAlgebraicExpressionWithAllErrors("x*x") + + assertThat(expression).hasStructureThatMatches { + multiplication { leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(1) + variable { + withNameThat().isEqualTo("x") } } rightOperand { - negation { - operand { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } + variable { + withNameThat().isEqualTo("x") } } } } + } - val expression20 = parseAlgebraicExpressionWithAllErrors("√2 × 7 ÷ 4") - assertThat(expression20).hasStructureThatMatches { - division { - leftOperand { - multiplication { - leftOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(7) - } - } - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - - val expression21 = parseAlgebraicExpressionWithAllErrors("sqrt(2)sqrt(3)sqrt(4)") - // Note that this tree demonstrates left associativity. - assertThat(expression21).hasStructureThatMatches { - multiplication { - leftOperand { - multiplication { - leftOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - rightOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - } - } - rightOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - } - } - - val expression22 = parseAlgebraicExpressionWithAllErrors("(1+2)(3-7^2)(5+-17)") - assertThat(expression22).hasStructureThatMatches { - multiplication { - leftOperand { - multiplication { - leftOperand { - // 1+2 - group { - addition { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - rightOperand { - // 3-7^2 - group { - subtraction { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(7) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - } - } - } - rightOperand { - // 5+-17 - group { - addition { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(5) - } - } - rightOperand { - negation { - operand { - constant { - withValueThat().isIntegerThat().isEqualTo(17) - } - } - } - } - } - } - } - } - } - - val expression26 = parseAlgebraicExpressionWithAllErrors("3^-2") - assertThat(expression26).hasStructureThatMatches { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - negation { - operand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - - val expression27 = parseAlgebraicExpressionWithoutOptionalErrors("(3^-2)^(3^-2)") - assertThat(expression27).hasStructureThatMatches { - exponentiation { - leftOperand { - group { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - negation { - operand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - } - rightOperand { - group { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - negation { - operand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - } - } - } - - val expression28 = parseAlgebraicExpressionWithAllErrors("1-3^sqrt(4)") - assertThat(expression28).hasStructureThatMatches { - subtraction { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - rightOperand { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - } - } - } - } - - // "Hard" order of operation problems loosely based on & other problems that can often stump - // people: https://www.basic-mathematics.com/hard-order-of-operations-problems.html. - val expression29 = parseAlgebraicExpressionWithAllErrors("3÷2*(3+4)") - assertThat(expression29).hasStructureThatMatches { - multiplication { - leftOperand { - division { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - rightOperand { - group { - addition { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - } - } - } - - val expression59 = parseAlgebraicExpressionWithAllErrors("3÷2(3+4)") - assertThat(expression59).hasStructureThatMatches { - multiplication { - leftOperand { - division { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - rightOperand { - group { - addition { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - } - } - } - - val expression31 = parseAlgebraicExpressionWithoutOptionalErrors("(3)(4)(5)") - assertThat(expression31).hasStructureThatMatches { - multiplication { - leftOperand { - multiplication { - leftOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - rightOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - } - rightOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(5) - } - } - } - } - } - - val expression33 = parseAlgebraicExpressionWithoutOptionalErrors("2^(3)") - assertThat(expression33).hasStructureThatMatches { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - } - - // Verify that implicit multiple has lower precedence than exponentiation. - val expression34 = parseAlgebraicExpressionWithoutOptionalErrors("2^(3)(4)") - assertThat(expression34).hasStructureThatMatches { - multiplication { - leftOperand { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - } - rightOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - } - - val expression35 = parseAlgebraicExpressionWithoutOptionalErrors("2^(3)*2^2") - assertThat(expression35).hasStructureThatMatches { - multiplication { - leftOperand { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - } - rightOperand { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - - // An exponentiation can be a right operand of an implicit mult if it's grouped. - val expression36 = parseAlgebraicExpressionWithoutOptionalErrors("2^(3)(2^2)") - assertThat(expression36).hasStructureThatMatches { - multiplication { - leftOperand { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - } - rightOperand { - group { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - } - - val expression38 = parseAlgebraicExpressionWithoutOptionalErrors("2^3(4)*2^3") - assertThat(expression38).hasStructureThatMatches { - // 2^3(4)*2^3 - multiplication { - leftOperand { - // 2^3(4) - multiplication { - leftOperand { - // 2^3 - exponentiation { - leftOperand { - // 2 - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - // 3 - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - rightOperand { - // 4 - group { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - } - rightOperand { - // 2^3 - exponentiation { - leftOperand { - // 2 - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - // 3 - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - } - } - - val expression39 = parseAlgebraicExpressionWithAllErrors("-(1+2)") - assertThat(expression39).hasStructureThatMatches { - negation { - operand { - group { - addition { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - } - - // Should pass for algebra. - val expression66 = parseAlgebraicExpressionWithAllErrors("-2 x") - assertThat(expression66).hasStructureThatMatches { - negation { - operand { - multiplication { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - variable { - withNameThat().isEqualTo("x") - } - } - } - } - } - } - - val expression40 = parseAlgebraicExpressionWithAllErrors("-2 (1+2)") - assertThat(expression40).hasStructureThatMatches { - // The negation happens last for parity with other common calculators. - negation { - operand { - multiplication { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - group { - addition { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - } - } - } - - val expression41 = parseAlgebraicExpressionWithoutOptionalErrors("-2^3(4)") - assertThat(expression41).hasStructureThatMatches { - negation { - operand { - multiplication { - leftOperand { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - rightOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - } - } - } - - val expression43 = parseAlgebraicExpressionWithoutOptionalErrors("√2^2(3)") - assertThat(expression43).hasStructureThatMatches { - multiplication { - leftOperand { - exponentiation { - leftOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - rightOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - } - - val expression60 = parseAlgebraicExpressionWithoutOptionalErrors("√(2^2(3))") - assertThat(expression60).hasStructureThatMatches { - functionCallTo(SQUARE_ROOT) { - argument { - group { - multiplication { - leftOperand { - exponentiation { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - rightOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - } - } - } - } - - val expression42 = parseAlgebraicExpressionWithAllErrors("-2*-2") - // Note that the following structure is not the same as (-2)*(-2) since unary negation has - // higher precedence than multiplication, so it's first & recurses to include the entire - // multiplication expression. - assertThat(expression42).hasStructureThatMatches { - negation { - operand { - multiplication { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - negation { - operand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - } - } - - val expression44 = parseAlgebraicExpressionWithoutOptionalErrors("2(2)") - assertThat(expression44).hasStructureThatMatches { - multiplication { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - - val expression45 = parseAlgebraicExpressionWithAllErrors("2sqrt(2)") - assertThat(expression45).hasStructureThatMatches { - multiplication { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - - val expression46 = parseAlgebraicExpressionWithAllErrors("2√2") - assertThat(expression46).hasStructureThatMatches { - multiplication { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - - val expression47 = parseAlgebraicExpressionWithoutOptionalErrors("(2)(2)") - assertThat(expression47).hasStructureThatMatches { - multiplication { - leftOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - rightOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } + @Test + fun testParse_variableTimesVariable_returnsExpWithMultipleVariables() { + val expression = parseAlgebraicExpressionWithAllErrors("x*y") - val expression48 = parseAlgebraicExpressionWithoutOptionalErrors("sqrt(2)(2)") - assertThat(expression48).hasStructureThatMatches { + assertThat(expression).hasStructureThatMatches { multiplication { leftOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } + variable { + withNameThat().isEqualTo("x") } } rightOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } + variable { + withNameThat().isEqualTo("y") } } } } + } - val expression49 = parseAlgebraicExpressionWithAllErrors("sqrt(2)sqrt(2)") - assertThat(expression49).hasStructureThatMatches { - multiplication { - leftOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - rightOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } + @Test + fun testParse_variableNextToVariable_returnsExpWithImplicitMultiplication() { + val expression = parseAlgebraicExpressionWithAllErrors("xy") - val expression50 = parseAlgebraicExpressionWithAllErrors("√2√2") - assertThat(expression50).hasStructureThatMatches { - multiplication { + assertThat(expression).hasStructureThatMatches { + multiplication(isImplicit = true) { leftOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } + variable { + withNameThat().isEqualTo("x") } } rightOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } + variable { + withNameThat().isEqualTo("y") } } } } + } - val expression51 = parseAlgebraicExpressionWithoutOptionalErrors("(2)(2)(2)") - assertThat(expression51).hasStructureThatMatches { - multiplication { - leftOperand { - multiplication { - leftOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - rightOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - rightOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } + @Test + fun testParse_threeSubsequentVariables_returnsExpWithImplicitMultAndLeftAssociativity() { + val expression = parseAlgebraicExpressionWithAllErrors("xyz") - val expression52 = parseAlgebraicExpressionWithAllErrors("sqrt(2)sqrt(2)sqrt(2)") - assertThat(expression52).hasStructureThatMatches { - multiplication { - leftOperand { - multiplication { - leftOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } + // 'xyz' results in a right-associative implicit multiplication (i.e. (x*y)*z). + assertThat(expression).hasStructureThatMatches { + multiplication(isImplicit = true) { + leftOperand { + multiplication(isImplicit = true) { + leftOperand { + variable { + withNameThat().isEqualTo("x") } } rightOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } + variable { + withNameThat().isEqualTo("y") } } } } rightOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } + variable { + withNameThat().isEqualTo("z") } } } } + } - val expression53 = parseAlgebraicExpressionWithAllErrors("√2√2√2") - assertThat(expression53).hasStructureThatMatches { - multiplication { + @Test + fun testParse_threeSubsequentVariables_customVariables_returnsExpWithImplicitMult() { + val allowedVariables = listOf("i", "j", "k") + + val expression = parseAlgebraicExpressionWithAllErrors("ijk", allowedVariables) + + // Other variables can be used, too. + assertThat(expression).hasStructureThatMatches { + multiplication(isImplicit = true) { leftOperand { - multiplication { + multiplication(isImplicit = true) { leftOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } + variable { + withNameThat().isEqualTo("i") } } rightOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } + variable { + withNameThat().isEqualTo("j") } } } } rightOperand { - functionCallTo(SQUARE_ROOT) { - argument { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } + variable { + withNameThat().isEqualTo("k") } } } } + } - // Should pass for algebra. - val expression67 = parseAlgebraicExpressionWithAllErrors("2x^2y^-3") - assertThat(expression67).hasStructureThatMatches { - // 2x^2y^-3 -> (2*(x^2))*(y^(-3)) - multiplication { - // 2x^2 + @Test + fun testParse_fullQuadraticExpression_returnsExpWithCorrectValuesAndOrders() { + val expression = parseAlgebraicExpressionWithAllErrors("-8x^3-7.4x^2+x-12/√2") + + // This combines all of the distinct pieces tested earlier to demonstrate full polynomial + // syntax. + assertThat(expression).hasStructureThatMatches { + // -8x^3-7.4x^2+x-12√2 -> (((-(8*(x^3))) - (7.4*(x^2))) + x) - (12/√2) + subtraction { leftOperand { - multiplication { - // 2 + // ((-(8*(x^3))) - (7.4*(x^2))) + x + addition { leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - // x^2 - rightOperand { - exponentiation { - // x + // (-(8*(x^3))) - (7.4*(x^2)) + subtraction { leftOperand { - variable { - withNameThat().isEqualTo("x") + // -(8*(x^3)) + negation { + operand { + // 8*(x^3) + multiplication(isImplicit = true) { + leftOperand { + // 8 + constant { + withValueThat().isIntegerThat().isEqualTo(8) + } + } + rightOperand { + // x^3 + exponentiation { + leftOperand { + // x + variable { + withNameThat().isEqualTo("x") + } + } + rightOperand { + // 3 + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + } } } - // 2 rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) + // 7.4*(x^2) + multiplication(isImplicit = true) { + leftOperand { + // 7.4 + constant { + withValueThat().isIrrationalThat().isWithin(1e-5).of(7.4) + } + } + rightOperand { + // x^2 + exponentiation { + leftOperand { + // x + variable { + withNameThat().isEqualTo("x") + } + } + rightOperand { + // 2 + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } } } } } + rightOperand { + // x + variable { + withNameThat().isEqualTo("x") + } + } } } - // y^-3 rightOperand { - exponentiation { - // y + // 12/√2 + division { leftOperand { - variable { - withNameThat().isEqualTo("y") + // 12 + constant { + withValueThat().isIntegerThat().isEqualTo(12) } } - // -3 rightOperand { - negation { - // 3 - operand { + // √2 + functionCallTo(SQUARE_ROOT) { + argument { + // 2 constant { - withValueThat().isIntegerThat().isEqualTo(3) + withValueThat().isIntegerThat().isEqualTo(2) } } } @@ -1704,38 +790,130 @@ class AlgebraicExpressionParserTest { } } } + } - val expression54 = parseAlgebraicExpressionWithAllErrors("2*2/-4+7*2") - assertThat(expression54).hasStructureThatMatches { - // 2*2/-4+7*2 -> ((2*2)/(-4))+(7*2) - addition { + @Test + fun testParse_expressionWithMultipleVariableQuadraticTerms_returnsExpWithCorrectValsAndOrders() { + val expression = parseAlgebraicExpressionWithAllErrors("12x^2y^2-yz^2+yzx-731z") + + // This builds on the polynomial syntax demonstrated above by demonstrating multi-variable + // implicit multiplication for multi-dimensional polynomials. Note that this also demonstrates + // that exponents can imply multiplication when the base is a variable (as part of polynomial + // syntax) which is a functional difference from numeric-only expressions (where subsequent + // numeric exponents never imply multiplication). + assertThat(expression).hasStructureThatMatches { + // 12x^2y^2-yz^2+yzx-731z -> ((((12*(x^2))*(y^2))-(y*(z^2)))+((y*z)*x))-(731*z) + subtraction { leftOperand { - // 2*2/-4 - division { + // (((12*(x^2))*(y^2))-(y*(z^2)))+((y*z)*x) + addition { leftOperand { - // 2*2 - multiplication { + // ((12*(x^2))*(y^2))-(y*(z^2)) + subtraction { leftOperand { - // 2 - constant { - withValueThat().isIntegerThat().isEqualTo(2) + // (12*(x^2))*(y^2) + multiplication(isImplicit = true) { + leftOperand { + // 12*(x^2) + multiplication(isImplicit = true) { + leftOperand { + // 12 + constant { + withValueThat().isIntegerThat().isEqualTo(12) + } + } + rightOperand { + // x^2 + exponentiation { + leftOperand { + // x + variable { + withNameThat().isEqualTo("x") + } + } + rightOperand { + // 2 + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + rightOperand { + // y^2 + exponentiation { + leftOperand { + // y + variable { + withNameThat().isEqualTo("y") + } + } + rightOperand { + // 2 + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } } } rightOperand { - // 2 - constant { - withValueThat().isIntegerThat().isEqualTo(2) + // y*(z^2) + multiplication(isImplicit = true) { + leftOperand { + // y + variable { + withNameThat().isEqualTo("y") + } + } + rightOperand { + // z^2 + exponentiation { + leftOperand { + // z + variable { + withNameThat().isEqualTo("z") + } + } + rightOperand { + // 2 + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } } } } } rightOperand { - // -4 - negation { - // 4 - operand { - constant { - withValueThat().isIntegerThat().isEqualTo(4) + // (y*z)*x + multiplication(isImplicit = true) { + leftOperand { + // y*z + multiplication(isImplicit = true) { + leftOperand { + // y + variable { + withNameThat().isEqualTo("y") + } + } + rightOperand { + // z + variable { + withNameThat().isEqualTo("z") + } + } + } + } + rightOperand { + // x + variable { + withNameThat().isEqualTo("x") } } } @@ -1743,147 +921,51 @@ class AlgebraicExpressionParserTest { } } rightOperand { - // 7*2 - multiplication { + // 731*z + multiplication(isImplicit = true) { leftOperand { - // 7 + // 731 constant { - withValueThat().isIntegerThat().isEqualTo(7) + withValueThat().isIntegerThat().isEqualTo(731) } } rightOperand { - // 2 - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - - val expression55 = parseAlgebraicExpressionWithAllErrors("3/(1-2)") - assertThat(expression55).hasStructureThatMatches { - division { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - group { - subtraction { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - } - - val expression56 = parseAlgebraicExpressionWithoutOptionalErrors("(3)/(1-2)") - assertThat(expression56).hasStructureThatMatches { - division { - leftOperand { - group { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - rightOperand { - group { - subtraction { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - } - } - - val expression57 = parseAlgebraicExpressionWithoutOptionalErrors("3/((1-2))") - assertThat(expression57).hasStructureThatMatches { - division { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - group { - group { - subtraction { - leftOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - rightOperand { - constant { - withValueThat().isIntegerThat().isEqualTo(2) - } - } + // z + variable { + withNameThat().isEqualTo("z") } } } } } } - - // TODO: add others, including tests for malformed expressions throughout the parser & - // tokenizer. } private companion object { - // TODO: fix helper API. - private fun parseAlgebraicExpressionWithoutOptionalErrors( - expression: String, - allowedVariables: List = listOf("x", "y", "z") + expression: String, allowedVariables: List = listOf("x", "y", "z") ): MathExpression { - val result = - parseAlgebraicExpressionInternal( - expression, ErrorCheckingMode.REQUIRED_ONLY, allowedVariables - ) - return (result as MathParsingResult.Success).result + return parseAlgebraicExpressionInternal( + expression, ErrorCheckingMode.REQUIRED_ONLY, allowedVariables + ) } private fun parseAlgebraicExpressionWithAllErrors( - expression: String, - allowedVariables: List = listOf("x", "y", "z") + expression: String, allowedVariables: List = listOf("x", "y", "z") ): MathExpression { - val result = - parseAlgebraicExpressionInternal( - expression, ErrorCheckingMode.ALL_ERRORS, allowedVariables - ) - return (result as MathParsingResult.Success).result + return parseAlgebraicExpressionInternal( + expression, ErrorCheckingMode.ALL_ERRORS, allowedVariables + ) } private fun parseAlgebraicExpressionInternal( - expression: String, - errorCheckingMode: ErrorCheckingMode, - allowedVariables: List = listOf("x", "y", "z") - ): MathParsingResult { - return MathExpressionParser.parseAlgebraicExpression( + expression: String, errorCheckingMode: ErrorCheckingMode, allowedVariables: List + ): MathExpression { + val result = MathExpressionParser.parseAlgebraicExpression( expression, allowedVariables, errorCheckingMode ) + assertThat(result).isInstanceOf(MathParsingResult.Success::class.java) + return (result as MathParsingResult.Success).result } } } From 042a1adfbbe80daaebb950950bc8f91c2ad48883 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Mon, 24 Jan 2022 15:21:15 -0800 Subject: [PATCH 059/162] Add missing tests for better coverage. --- .../android/util/math/MathExpressionParser.kt | 22 +++++---------- .../util/math/MathExpressionParserTest.kt | 27 ++++++++++++++++++- 2 files changed, 32 insertions(+), 17 deletions(-) diff --git a/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt b/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt index e0afccf30e4..25a40605beb 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt @@ -303,13 +303,8 @@ class MathExpressionParser private constructor(private val parseContext: ParseCo parseContext.hasNextTokenOfType() || parseContext.hasNextTokenOfType() } ?: SpacesBetweenNumbersError.toFailure() - is FunctionName, is LeftParenthesisSymbol, is SquareRootSymbol -> + is FunctionName, is LeftParenthesisSymbol, is SquareRootSymbol, is VariableName -> parseGenericTermWithoutUnaryWithoutNumber() - is VariableName -> { - if (parseContext is AlgebraicExpressionContext) { - parseGenericTermWithoutUnaryWithoutNumber() - } else VariableInNumericExpressionError.toFailure() - } is DivideSymbol, is ExponentiationSymbol, is MultiplySymbol -> { val previousToken = parseContext.getPreviousToken() when { @@ -528,9 +523,7 @@ class MathExpressionParser private constructor(private val parseContext: ParseCo private fun parseVariable(): MathParsingResult { val variableNameResult = - parseContext.consumeTokenOfType().maybeFail { - if (!parseContext.allowsVariables()) GenericError else null - }.maybeFail { variableName -> + parseContext.consumeTokenOfType().maybeFail { variableName -> return@maybeFail if (parseContext.hasMoreTokens()) { when (val nextToken = parseContext.peekToken()) { is PositiveInteger -> @@ -654,9 +647,12 @@ class MathExpressionParser private constructor(private val parseContext: ParseCo } is IncompleteFunctionName -> nextToken.toError() is InvalidToken -> nextToken.toError() + is VariableName -> if (parseContext !is AlgebraicExpressionContext) { + VariableInNumericExpressionError + } else GenericError is PositiveInteger, is PositiveRealNumber, is DivideSymbol, is ExponentiationSymbol, is FunctionName, is MinusSymbol, is MultiplySymbol, is PlusSymbol, is SquareRootSymbol, - is VariableName, null -> GenericError + null -> GenericError } } else null } @@ -687,8 +683,6 @@ class MathExpressionParser private constructor(private val parseContext: ParseCo abstract val errorCheckingMode: ErrorCheckingMode - abstract fun allowsVariables(): Boolean - fun hasMoreTokens(): Boolean = tokens.hasNext() fun peekToken(): Token? = tokens.peek() @@ -725,8 +719,6 @@ class MathExpressionParser private constructor(private val parseContext: ParseCo rawExpression: String, override val errorCheckingMode: ErrorCheckingMode ) : ParseContext(rawExpression) { - // Numeric expressions never allow variables. - override fun allowsVariables(): Boolean = false } class AlgebraicExpressionContext( @@ -736,8 +728,6 @@ class MathExpressionParser private constructor(private val parseContext: ParseCo override val errorCheckingMode: ErrorCheckingMode ) : ParseContext(rawExpression) { fun allowsVariable(variableName: String): Boolean = variableName in allowedVariables - - override fun allowsVariables(): Boolean = true } } diff --git a/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt index 4bfdb4199da..5a37e71a372 100644 --- a/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt @@ -737,7 +737,8 @@ class MathExpressionParserTest { Iteration("var_directly_in_exp", "subExp=x"), Iteration("var_directly_in_sub_exp", "subExp=(1+x)"), Iteration("var_directly_in_nested_exp", "subExp=3^x"), - Iteration("var_directly_in_sqrt", "subExp=sqrt(x)") + Iteration("var_directly_in_sqrt", "subExp=sqrt(x)"), + Iteration("var_in_unary", "subExp=-x") ) fun testParseAlgExp_powersWithVariableExpressions_returnsExponentIsVariableExpressionError() { val expression = "2^$subExp" @@ -794,6 +795,14 @@ class MathExpressionParserTest { assertThat(error).isNestedExponents() } + @Test + fun testParseNumExp_nestedExponents_withUnary_returnsNestedExponentsError() { + val error = expectFailureWhenParsingAlgebraicExpression("2^-3^4") + + // This covers a slightly different case than the above. + assertThat(error).isNestedExponents() + } + @Test fun testParseAlgExp_nestedExponents_optionalErrorsDisabled_doesNotFail() { // This doesn't trigger a failure when optional errors are disabled. @@ -841,6 +850,14 @@ class MathExpressionParserTest { assertThat(error).isVariableInNumericExpression() } + @Test + fun testParseNumExp_addVariable_afterNumber_returnsVariableInNumericExpressionError() { + val error = expectFailureWhenParsingNumericExpression("2x") + + // This covers a slightly different case than the above. + assertThat(error).isVariableInNumericExpression() + } + @Test fun testParseAlgExp_addUnsupportedVariable_returnsDisabledVariablesInUseErrorWithDetails() { val allowedVariables = listOf("x", "y") @@ -1050,6 +1067,14 @@ class MathExpressionParserTest { assertThat(error).isGenericError() } + @Test + fun testParseNumExp_trailingEquals_returnsGenericError() { + val error = expectFailureWhenParsingNumericExpression("+=") + + // Expressions can't end with an equals sign. + assertThat(error).isGenericError() + } + private companion object { private val BINARY_SYMBOL_TO_OPERATOR_MAP = mapOf( "*" to MULTIPLY, From 8ead9bf822b7b73fe511000b3e18b5b445ee6fa7 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Mon, 24 Jan 2022 17:49:23 -0800 Subject: [PATCH 060/162] Add KDocs & test exemptions. --- scripts/assets/test_file_exemptions.textproto | 2 + .../testing/math/MathParsingErrorSubject.kt | 219 +++++++++++++++++- .../android/util/math/MathExpressionParser.kt | 123 +++++++++- .../android/util/math/MathParsingError.kt | 169 ++++++++++++++ 4 files changed, 493 insertions(+), 20 deletions(-) diff --git a/scripts/assets/test_file_exemptions.textproto b/scripts/assets/test_file_exemptions.textproto index e7b076af277..f1715400516 100644 --- a/scripts/assets/test_file_exemptions.textproto +++ b/scripts/assets/test_file_exemptions.textproto @@ -658,6 +658,7 @@ exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/Fracti exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/RealSubject.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/PolynomialSubject.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt" +exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/MathParsingErrorSubject.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/MathEquationSubject.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/TokenSubject.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/mockito/MockitoKotlinHelper.kt" @@ -724,6 +725,7 @@ exempted_file_path: "utility/src/main/java/org/oppia/android/util/logging/fireba exempted_file_path: "utility/src/main/java/org/oppia/android/util/logging/firebase/LogReportingModule.kt" exempted_file_path: "utility/src/main/java/org/oppia/android/util/math/FloatExtensions.kt" exempted_file_path: "utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt" +exempted_file_path: "utility/src/main/java/org/oppia/android/util/math/MathParsingError.kt" exempted_file_path: "utility/src/main/java/org/oppia/android/util/networking/ConnectionStatus.kt" exempted_file_path: "utility/src/main/java/org/oppia/android/util/networking/NetworkConnectionDebugUtil.kt" exempted_file_path: "utility/src/main/java/org/oppia/android/util/networking/NetworkConnectionDebugUtilModule.kt" diff --git a/testing/src/main/java/org/oppia/android/testing/math/MathParsingErrorSubject.kt b/testing/src/main/java/org/oppia/android/testing/math/MathParsingErrorSubject.kt index 5db1b5f61d9..53c8bad235f 100644 --- a/testing/src/main/java/org/oppia/android/testing/math/MathParsingErrorSubject.kt +++ b/testing/src/main/java/org/oppia/android/testing/math/MathParsingErrorSubject.kt @@ -9,6 +9,13 @@ import com.google.common.truth.Truth.assertAbout import com.google.common.truth.Truth.assertThat import org.oppia.android.app.model.MathBinaryOperation import org.oppia.android.testing.math.MathExpressionSubject.Companion.assertThat +import org.oppia.android.testing.math.MathParsingErrorSubject.Companion.assertThat +import org.oppia.android.testing.math.MathParsingErrorSubject.MultipleRedundantParenthesesSubject.Companion.assertThat +import org.oppia.android.testing.math.MathParsingErrorSubject.NoVariableOrNumberAfterBinaryOperatorSubject.Companion.assertThat +import org.oppia.android.testing.math.MathParsingErrorSubject.NoVariableOrNumberBeforeBinaryOperatorSubject.Companion.assertThat +import org.oppia.android.testing.math.MathParsingErrorSubject.NumberAfterVariableSubject.Companion.assertThat +import org.oppia.android.testing.math.MathParsingErrorSubject.RedundantParenthesesForIndividualTermsSubject.Companion.assertThat +import org.oppia.android.testing.math.MathParsingErrorSubject.SubsequentBinaryOperatorsSubject.Companion.assertThat import org.oppia.android.testing.math.RealSubject.Companion.assertThat import org.oppia.android.util.math.MathParsingError import org.oppia.android.util.math.MathParsingError.DisabledVariablesInUseError @@ -36,104 +43,168 @@ import org.oppia.android.util.math.MathParsingError.UnbalancedParenthesesError import org.oppia.android.util.math.MathParsingError.UnnecessarySymbolsError import org.oppia.android.util.math.MathParsingError.VariableInNumericExpressionError -// TODO: file issue to add tests. +// TODO(#4132): file issue to add tests. -class MathParsingErrorSubject( +/** + * Truth subject for verifying properties of [MathParsingError]s. + * + * Call [assertThat] to create the subject. + */ +class MathParsingErrorSubject private constructor( metadata: FailureMetadata, private val actual: MathParsingError ) : Subject(metadata, actual) { + /** Verifies that the [MathParsingError] being tested is a [SpacesBetweenNumbersError]. */ fun isSpacesBetweenNumbers() { assertThat(actual).isEqualTo(SpacesBetweenNumbersError) } + /** Verifies that the [MathParsingError] being tested is an [UnbalancedParenthesesError]. */ fun isUnbalancedParentheses() { assertThat(actual).isEqualTo(UnbalancedParenthesesError) } + /** + * Verifies that the [MathParsingError] being tested is a [SingleRedundantParenthesesError], and + * returns a [SingleRedundantParenthesesSubject] to test its specific attributes. + */ fun isSingleRedundantParenthesesThat(): SingleRedundantParenthesesSubject { return SingleRedundantParenthesesSubject.assertThat(verifyAsType()) } + /** + * Verifies that the [MathParsingError] being tested is a [MultipleRedundantParenthesesError], and + * returns a [MultipleRedundantParenthesesSubject] to test its specific attributes. + */ fun isMultipleRedundantParenthesesThat(): MultipleRedundantParenthesesSubject { return MultipleRedundantParenthesesSubject.assertThat(verifyAsType()) } + /** + * Verifies that the [MathParsingError] being tested is a + * [RedundantParenthesesForIndividualTermsError], and returns a + * [RedundantParenthesesForIndividualTermsSubject] to test its specific attributes. + */ fun isRedundantIndividualTermsParensThat(): RedundantParenthesesForIndividualTermsSubject { return RedundantParenthesesForIndividualTermsSubject.assertThat(verifyAsType()) } + /** + * Verifies that the [MathParsingError] being tested is an [UnnecessarySymbolsError], and returns + * a [StringSubject] to verify the symbol's specific value. + */ fun isUnnecessarySymbolWithSymbolThat(): StringSubject { return assertThat(verifyAsType().invalidSymbol) } + /** + * Verifies that the [MathParsingError] being tested is a [NumberAfterVariableError], and returns + * a [NumberAfterVariableSubject] to test its specific attributes. + */ fun isNumberAfterVariableThat(): NumberAfterVariableSubject { return NumberAfterVariableSubject.assertThat(verifyAsType()) } + /** + * Verifies that the [MathParsingError] being tested is a [SubsequentBinaryOperatorsError], and + * returns a [SubsequentBinaryOperatorsSubject] to test its specific attributes. + */ fun isSubsequentBinaryOperatorsThat(): SubsequentBinaryOperatorsSubject { return SubsequentBinaryOperatorsSubject.assertThat(verifyAsType()) } + /** Verifies that the [MathParsingError] being tested is a [SubsequentUnaryOperatorsError]. */ fun isSubsequentUnaryOperators() { assertThat(actual).isEqualTo(SubsequentUnaryOperatorsError) } + /** + * Verifies that the [MathParsingError] being tested is a + * [NoVariableOrNumberBeforeBinaryOperatorError], and returns a + * [NoVariableOrNumberBeforeBinaryOperatorSubject] to test its specific attributes. + */ fun isNoVarOrNumBeforeBinaryOperatorThat(): NoVariableOrNumberBeforeBinaryOperatorSubject { return NoVariableOrNumberBeforeBinaryOperatorSubject.assertThat(verifyAsType()) } + /** + * Verifies that the [MathParsingError] being tested is a + * [NoVariableOrNumberAfterBinaryOperatorError], and returns a + * [NoVariableOrNumberAfterBinaryOperatorSubject] to test its specific attributes. + */ fun isNoVariableOrNumberAfterBinaryOperatorThat(): NoVariableOrNumberAfterBinaryOperatorSubject { return NoVariableOrNumberAfterBinaryOperatorSubject.assertThat(verifyAsType()) } + /** + * Verifies that the [MathParsingError] being tested is an [ExponentIsVariableExpressionError]. + */ fun isExponentIsVariableExpression() { assertThat(actual).isEqualTo(ExponentIsVariableExpressionError) } + /** Verifies that the [MathParsingError] being tested is an [ExponentTooLargeError]. */ fun isExponentTooLarge() { assertThat(actual).isEqualTo(ExponentTooLargeError) } + /** Verifies that the [MathParsingError] being tested is a [NestedExponentsError]. */ fun isNestedExponents() { assertThat(actual).isEqualTo(NestedExponentsError) } + /** Verifies that the [MathParsingError] being tested is a [HangingSquareRootError]. */ fun isHangingSquareRoot() { assertThat(actual).isEqualTo(HangingSquareRootError) } + /** Verifies that the [MathParsingError] being tested is a [TermDividedByZeroError]. */ fun isTermDividedByZero() { assertThat(actual).isEqualTo(TermDividedByZeroError) } + /** Verifies that the [MathParsingError] being tested is a [VariableInNumericExpressionError]. */ fun isVariableInNumericExpression() { assertThat(actual).isEqualTo(VariableInNumericExpressionError) } + /** + * Verifies that the [MathParsingError] being tested is a [DisabledVariablesInUseError], and + * returns an [IterableSubject] to verify the specific disallowed variables in use. + */ fun isDisabledVariablesInUseWithVariablesThat(): IterableSubject { return assertThat(verifyAsType().variables) } + /** Verifies that the [MathParsingError] being tested is an [EquationIsMissingEqualsError]. */ fun isEquationIsMissingEquals() { assertThat(actual).isEqualTo(EquationIsMissingEqualsError) } + /** Verifies that the [MathParsingError] being tested is an [EquationHasTooManyEqualsError]. */ fun isEquationHasTooManyEquals() { assertThat(actual).isEqualTo(EquationHasTooManyEqualsError) } + /** Verifies that the [MathParsingError] being tested is an [EquationMissingLhsOrRhsError]. */ fun isEquationMissingLhsOrRhs() { assertThat(actual).isEqualTo(EquationMissingLhsOrRhsError) } + /** + * Verifies that the [MathParsingError] being tested is a [InvalidFunctionInUseError], and returns + * a [StringSubject] to verify the specific function name in used. + */ fun isInvalidFunctionInUseWithNameThat(): StringSubject { return assertThat(verifyAsType().functionName) } + /** Verifies that the [MathParsingError] being tested is a [FunctionNameIncompleteError]. */ fun isFunctionNameIncomplete() { assertThat(actual).isEqualTo(FunctionNameIncompleteError) } + /** Verifies that the [MathParsingError] being tested is a [GenericError]. */ fun isGenericError() { assertThat(actual).isEqualTo(GenericError) } @@ -143,15 +214,32 @@ class MathParsingErrorSubject( return actual as T } - class SingleRedundantParenthesesSubject( + /** + * Truth subject for verifying properties of [SingleRedundantParenthesesError]s. + * + * Call [assertThat] to create the subject. + */ + class SingleRedundantParenthesesSubject private constructor( metadata: FailureMetadata, private val actual: SingleRedundantParenthesesError ) : Subject(metadata, actual) { + /** + * Returns a [StringSubject] to test the value of + * [SingleRedundantParenthesesError.rawExpression] for the error being tested by this subject. + */ fun hasRawExpressionThat(): StringSubject = assertThat(actual.rawExpression) + /** + * Returns a [MathExpressionSubject] to test the value of + * [SingleRedundantParenthesesError.expression] for the error being tested by this subject. + */ fun hasExpressionThat(): MathExpressionSubject = assertThat(actual.expression) companion object { + /** + * Returns a new [SingleRedundantParenthesesSubject] to verify aspects of the specified + * [SingleRedundantParenthesesError] value. + */ internal fun assertThat( actual: SingleRedundantParenthesesError ): SingleRedundantParenthesesSubject { @@ -160,15 +248,33 @@ class MathParsingErrorSubject( } } - class MultipleRedundantParenthesesSubject( + /** + * Truth subject for verifying properties of [MultipleRedundantParenthesesError]s. + * + * Call [assertThat] to create the subject. + */ + class MultipleRedundantParenthesesSubject private constructor( metadata: FailureMetadata, private val actual: MultipleRedundantParenthesesError ) : Subject(metadata, actual) { + /** + * Returns a [StringSubject] to test the value of + * [MultipleRedundantParenthesesError.rawExpression] for the error being tested by this subject. + */ fun hasRawExpressionThat(): StringSubject = assertThat(actual.rawExpression) + /** + * Returns a [MathExpressionSubject] to test the value of + * [MultipleRedundantParenthesesError.expression] for the error being tested by this + * subject. + */ fun hasExpressionThat(): MathExpressionSubject = assertThat(actual.expression) companion object { + /** + * Returns a new [MultipleRedundantParenthesesSubject] to verify aspects of the specified + * [MultipleRedundantParenthesesError] value. + */ internal fun assertThat( actual: MultipleRedundantParenthesesError ): MultipleRedundantParenthesesSubject { @@ -177,15 +283,34 @@ class MathParsingErrorSubject( } } - class RedundantParenthesesForIndividualTermsSubject( + /** + * Truth subject for verifying properties of [RedundantParenthesesForIndividualTermsError]s. + * + * Call [assertThat] to create the subject. + */ + class RedundantParenthesesForIndividualTermsSubject private constructor( metadata: FailureMetadata, private val actual: RedundantParenthesesForIndividualTermsError ) : Subject(metadata, actual) { + /** + * Returns a [StringSubject] to test the value of + * [RedundantParenthesesForIndividualTermsError.rawExpression] for the error being tested by + * this subject. + */ fun hasRawExpressionThat(): StringSubject = assertThat(actual.rawExpression) + /** + * Returns a [MathExpressionSubject] to test the value of + * [RedundantParenthesesForIndividualTermsError.expression] for the error being tested by this + * subject. + */ fun hasExpressionThat(): MathExpressionSubject = assertThat(actual.expression) companion object { + /** + * Returns a new [RedundantParenthesesForIndividualTermsSubject] to verify aspects of the + * specified [RedundantParenthesesForIndividualTermsError] value. + */ internal fun assertThat( actual: RedundantParenthesesForIndividualTermsError ): RedundantParenthesesForIndividualTermsSubject { @@ -194,29 +319,63 @@ class MathParsingErrorSubject( } } - class NumberAfterVariableSubject( + /** + * Truth subject for verifying properties of [NumberAfterVariableError]s. + * + * Call [assertThat] to create the subject. + */ + class NumberAfterVariableSubject private constructor( metadata: FailureMetadata, private val actual: NumberAfterVariableError ) : Subject(metadata, actual) { + /** + * Returns a [RealSubject] to test the value of [NumberAfterVariableError.number] for the error + * being tested by this subject. + */ fun hasNumberThat(): RealSubject = assertThat(actual.number) + /** + * Returns a [StringSubject] to test the value of [NumberAfterVariableError.variable] for the + * error being tested by this subject. + */ fun hasVariableThat(): StringSubject = assertThat(actual.variable) companion object { + /** + * Returns a new [NumberAfterVariableSubject] to verify aspects of the specified + * [NumberAfterVariableError] value. + */ internal fun assertThat(actual: NumberAfterVariableError): NumberAfterVariableSubject = assertAbout(::NumberAfterVariableSubject).that(actual) } } - class SubsequentBinaryOperatorsSubject( + /** + * Truth subject for verifying properties of [SubsequentBinaryOperatorsError]s. + * + * Call [assertThat] to create the subject. + */ + class SubsequentBinaryOperatorsSubject private constructor( metadata: FailureMetadata, private val actual: SubsequentBinaryOperatorsError ) : Subject(metadata, actual) { + /** + * Returns a [StringSubject] to test the value of [SubsequentBinaryOperatorsError.operator1] for + * the error being tested by this subject. + */ fun hasFirstOperatorThat(): StringSubject = assertThat(actual.operator1) + /** + * Returns a [StringSubject] to test the value of [SubsequentBinaryOperatorsError.operator2] for + * the error being tested by this subject. + */ fun hasSecondOperatorThat(): StringSubject = assertThat(actual.operator2) companion object { + /** + * Returns a new [SubsequentBinaryOperatorsSubject] to verify aspects of the + * specified [SubsequentBinaryOperatorsError] value. + */ internal fun assertThat( actual: SubsequentBinaryOperatorsError ): SubsequentBinaryOperatorsSubject { @@ -225,16 +384,35 @@ class MathParsingErrorSubject( } } - class NoVariableOrNumberBeforeBinaryOperatorSubject( + /** + * Truth subject for verifying properties of [NoVariableOrNumberBeforeBinaryOperatorError]s. + * + * Call [assertThat] to create the subject. + */ + class NoVariableOrNumberBeforeBinaryOperatorSubject private constructor( metadata: FailureMetadata, private val actual: NoVariableOrNumberBeforeBinaryOperatorError ) : Subject(metadata, actual) { + /** + * Returns a [ComparableSubject] to test the value of + * [NoVariableOrNumberBeforeBinaryOperatorError.operator] for the error being tested by this + * subject. + */ fun hasOperatorThat(): ComparableSubject = assertThat(actual.operator) + /** + * Returns a [StringSubject] to test the value of + * [NoVariableOrNumberBeforeBinaryOperatorError.operatorSymbol] for the error being tested by + * this subject. + */ fun hasOperatorSymbolThat(): StringSubject = assertThat(actual.operatorSymbol) companion object { + /** + * Returns a new [NoVariableOrNumberBeforeBinaryOperatorSubject] to verify aspects of the + * specified [NoVariableOrNumberBeforeBinaryOperatorError] value. + */ internal fun assertThat( actual: NoVariableOrNumberBeforeBinaryOperatorError ): NoVariableOrNumberBeforeBinaryOperatorSubject { @@ -243,16 +421,35 @@ class MathParsingErrorSubject( } } - class NoVariableOrNumberAfterBinaryOperatorSubject( + /** + * Truth subject for verifying properties of [NoVariableOrNumberAfterBinaryOperatorError]s. + * + * Call [assertThat] to create the subject. + */ + class NoVariableOrNumberAfterBinaryOperatorSubject private constructor( metadata: FailureMetadata, private val actual: NoVariableOrNumberAfterBinaryOperatorError ) : Subject(metadata, actual) { + /** + * Returns a [ComparableSubject] to test the value of + * [NoVariableOrNumberAfterBinaryOperatorError.operator] for the error being tested by this + * subject. + */ fun hasOperatorThat(): ComparableSubject = assertThat(actual.operator) + /** + * Returns a [StringSubject] to test the value of + * [NoVariableOrNumberAfterBinaryOperatorError.operatorSymbol] for the error being tested by + * this subject. + */ fun hasOperatorSymbolThat(): StringSubject = assertThat(actual.operatorSymbol) companion object { + /** + * Returns a new [NoVariableOrNumberAfterBinaryOperatorSubject] to verify aspects of the + * specified [NoVariableOrNumberAfterBinaryOperatorError] value. + */ internal fun assertThat( actual: NoVariableOrNumberAfterBinaryOperatorError ): NoVariableOrNumberAfterBinaryOperatorSubject { @@ -262,6 +459,10 @@ class MathParsingErrorSubject( } companion object { + /** + * Returns a new [MathParsingErrorSubject] to verify aspects of the specified [MathParsingError] + * value. + */ fun assertThat(actual: MathParsingError): MathParsingErrorSubject = assertAbout(::MathParsingErrorSubject).that(actual) } diff --git a/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt b/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt index 25a40605beb..41bc60243d5 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt @@ -67,15 +67,23 @@ import org.oppia.android.util.math.MathParsingError.EquationHasTooManyEqualsErro import org.oppia.android.util.math.MathParsingError.EquationIsMissingEqualsError import org.oppia.android.util.math.PeekableIterator.Companion.toPeekableIterator +/** + * Parser for numeric expressions, algebraic expressions, and algebraic equations. + * + * Note that this parser is guaranteed to be LL(1), and to perform a series of robust error checks + * against invalid string expressions. The implementation is specifically designed to ensure an + * LL(1) grammar for both simplicity and long-term maintainability (as it's likely additional + * functionality will need to be added to the language). + * + * To use the parser: + * - Call [parseNumericExpression] for numeric expressions + * - Call [parseAlgebraicExpression] for algebraic expressions + * - Call [parseAlgebraicEquation] for algebraic equations + * + * For the formal grammar specification, see: + * https://docs.google.com/document/d/1JMpbjqRqdEpye67HvDoqBo_rtScY9oEaB7SwKBBspss/edit#bookmark=id.wtmim9gp20a6. + */ class MathExpressionParser private constructor(private val parseContext: ParseContext) { - // TODO: - // - Add helpers to reduce overall parser length. - // - Integrate with new errors & update the routines to not rely on exceptions except in actual exceptional cases. Make sure optional errors can be disabled (for testing purposes). - // - Rename this to be a generic parser, update the public API, add documentation, remove the old classes, and split up the big test routines into actual separate tests. - - // TODO: implement specific errors. - // TODO: verify remaining GenericErrors are correct. - // TODO: document that 'generic' means either 'numeric' or 'algebraic' (ie that the expression is syntactically the same between both grammars). // TODO: document that one design goal is keeping the grammar for this parser as LL(1) & why. @@ -86,6 +94,13 @@ class MathExpressionParser private constructor(private val parseContext: ParseCo } } + /** + * Returns a parsed [MathParsingResult] of [MathExpression] from the current [ParseContext]. + * + * Note that 'generic' here and elsewhere means that it can either be a 'numeric' or 'algebraic' + * expression (the specifics are handled lower in the parsing call tree). Generic methods are used + * to share common parsing logic to reduce the overall size of the parser. + */ private fun parseGenericExpressionGrammar(): MathParsingResult { // generic_expression_grammar = generic_expression ; return parseGenericExpression().maybeFail { expression -> checkForLearnerErrors(expression) } @@ -675,16 +690,24 @@ class MathExpressionParser private constructor(private val parseContext: ParseCo private fun InvalidToken.toFailure(): MathParsingResult = toError().toFailure() + /** + * Specification of context while parsing math expressions and equations. + * + * @property rawExpression the whole raw math expression/equation currently being parsed + */ private sealed class ParseContext(val rawExpression: String) { - val tokens: PeekableIterator by lazy { + private val tokens: PeekableIterator by lazy { MathTokenizer.tokenize(rawExpression).toPeekableIterator() } private var previousToken: Token? = null + /** Specifies the [ErrorCheckingMode] for the current parsing context. */ abstract val errorCheckingMode: ErrorCheckingMode + /** Returns whether there are more [Token]s to parse. */ fun hasMoreTokens(): Boolean = tokens.hasNext() + /** Returns the next [Token] available to parse. */ fun peekToken(): Token? = tokens.peek() /** @@ -695,8 +718,13 @@ class MathExpressionParser private constructor(private val parseContext: ParseCo */ fun getPreviousToken(): Token? = previousToken + /** Returns whether the next available token is type [T] (implies there is a token to parse). */ inline fun hasNextTokenOfType(): Boolean = peekToken() is T + /** + * Consumes the next [Token] (which is assumed to be type [T], otherwise the error provided by + * [missingError] is used) and returns the result. + */ inline fun consumeTokenOfType( missingError: () -> MathParsingError = { GenericError } ): MathParsingResult { @@ -707,42 +735,81 @@ class MathExpressionParser private constructor(private val parseContext: ParseCo } ?: missingError().toFailure() } + /** Returns the raw string sub-expression corresponding to the specified [Token]. */ fun extractSubexpression(token: Token): String { return rawExpression.substring(token.startIndex, token.endIndex) } + /** Returns the raw string sub-expression corresponding to the specified [MathExpression]. */ fun extractSubexpression(expression: MathExpression): String { return rawExpression.substring(expression.parseStartIndex, expression.parseEndIndex) } + /** The [ParseContext] corresponding to parsing numeric expressions. */ class NumericExpressionContext( rawExpression: String, override val errorCheckingMode: ErrorCheckingMode - ) : ParseContext(rawExpression) { - } + ) : ParseContext(rawExpression) + /** + * The [ParseContext] corresponding to parsing algebraic expressions & equations. + * + * @property isPartOfEquation whether this context is part of parsing an equation + * @property allowedVariables the list of variables allowed to be used within this context + */ class AlgebraicExpressionContext( rawExpression: String, val isPartOfEquation: Boolean, private val allowedVariables: List, override val errorCheckingMode: ErrorCheckingMode ) : ParseContext(rawExpression) { + /** Returns whether the specified variable is allowed to be used per this context. */ fun allowsVariable(variableName: String): Boolean = variableName in allowedVariables } } companion object { + /** The level of error detection strictness that should be enabled during parsing. */ enum class ErrorCheckingMode { + /** + * Indicates that only only irrecoverable errors should be detected. + * + * See the documentation for specific [MathParsingError]s to determine which are + * irrecoverable. + */ REQUIRED_ONLY, + + /** + * Indicates that both irrecoverable and optional errors should be detected (the strictest + * setting). + * + * Note that 'optional' errors are those that correspond to syntaxes that can still be + * correctly represented as a math expression or equation (but may indicate a learner + * misunderstanding). + * + * See the documentation for specific [MathParsingError]s to determine which are optional. + */ ALL_ERRORS } + /** The result of attempting to parse a raw math expression or equation. */ sealed class MathParsingResult { + /** Indicates that the parse was successful with a value of [result]. */ data class Success(val result: T) : MathParsingResult() + /** Indicates that the parse failed with the specified [error]. */ data class Failure(val error: MathParsingError) : MathParsingResult() } + /** + * Parses a [rawExpression] as a numeric expression + * + * Note that the returned expression will have all of its parsing information stripped. + * + * @param errorCheckingMode specifies what level of error detection should be enabled during + * parsing. The default is [ErrorCheckingMode.ALL_ERRORS]. + * @return the result of attempting to parse the specified numeric expression + */ fun parseNumericExpression( rawExpression: String, errorCheckingMode: ErrorCheckingMode = ErrorCheckingMode.ALL_ERRORS @@ -752,6 +819,17 @@ class MathExpressionParser private constructor(private val parseContext: ParseCo .map { it.stripParseInfo() } } + /** + * Parses a [rawExpression] as an algebraic expression + * + * Note that the returned expression will have all of its parsing information stripped. + * + * @param allowedVariables the list of case-sensitive variables allowed in the expression (any + * variables encountered that are not within the list will result in an error) + * @param errorCheckingMode specifies what level of error detection should be enabled during + * parsing. The default is [ErrorCheckingMode.ALL_ERRORS]. + * @return the result of attempting to parse the specified algebraic expression + */ fun parseAlgebraicExpression( rawExpression: String, allowedVariables: List, @@ -762,6 +840,17 @@ class MathExpressionParser private constructor(private val parseContext: ParseCo ).parseGenericExpressionGrammar().map { it.stripParseInfo() } } + /** + * Parses a [rawExpression] as an algebraic equation + * + * Note that the returned expression will have all of its parsing information stripped. + * + * @param allowedVariables the list of case-sensitive variables allowed in the expression (any + * variables encountered that are not within the list will result in an error) + * @param errorCheckingMode specifies what level of error detection should be enabled during + * parsing. The default is [ErrorCheckingMode.ALL_ERRORS]. + * @return the result of attempting to parse the specified algebraic equation + */ fun parseAlgebraicEquation( rawExpression: String, allowedVariables: List, @@ -906,11 +995,23 @@ class MathExpressionParser private constructor(private val parseContext: ParseCo } } + /** + * Represents the right-hand side of a binary operation. + * + * @property operator the operator corresponding to the operation + * @property rhsResult the pending result for parsing the right-hand side + * @property isImplicit whether this is an implicit operation (such as implicit multiplication) + */ private data class BinaryOperationRhs( val operator: MathBinaryOperation.Operator, val rhsResult: MathParsingResult, val isImplicit: Boolean = false ) { + /** + * Returns the result of combining the left & right-hand sides of the operation into a single + * [MathExpression] representing the entire binary operation (or a failure if either the + * left-hand or right-hand sides failed). + */ fun computeBinaryOperationExpression( lhsResult: MathParsingResult ): MathParsingResult { diff --git a/utility/src/main/java/org/oppia/android/util/math/MathParsingError.kt b/utility/src/main/java/org/oppia/android/util/math/MathParsingError.kt index ec19c7d745b..4fead75b5f2 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathParsingError.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathParsingError.kt @@ -4,70 +4,239 @@ import org.oppia.android.app.model.MathBinaryOperation import org.oppia.android.app.model.MathExpression import org.oppia.android.app.model.Real +/** + * An error that can be encountered while trying to parse a raw math expression or equation. + * + * All possible errors are subclasses to this sealed class. Further, this class has a dedicated + * Truth test subject that can be used for nicer testing. + */ sealed class MathParsingError { + /** + * Indicates that the user put spaces between two subsequent errors (e.g. '2 2'). + * + * This is an irrecoverable errors since implicit multiplication between numbers is expressly + * prohibited. + */ object SpacesBetweenNumbersError : MathParsingError() + /** + * Indicates that the user didn't finish a parenthetical group, e.g. '(2' or '2)'. + * + * This is an irrecoverable error. + */ object UnbalancedParenthesesError : MathParsingError() + /** + * Indicates that the entire expression has redundant parentheses, e.g. '(2)'. + * + * This is an optional error. + * + * @property rawExpression the raw string expression that has the extra parentheses + * @property expression the parsed sub-expression that has the extra parentheses + */ data class SingleRedundantParenthesesError( val rawExpression: String, val expression: MathExpression ) : MathParsingError() + /** + * Indicates that the entire expression or a sub-term has multiple redundant parentheses, e.g. + * '((2))' or '((2)) + 1'. + * + * This is an optional error. + * + * @property rawExpression the raw string expression that has the extra parentheses + * @property expression the parsed sub-expression that has the extra parentheses + */ data class MultipleRedundantParenthesesError( val rawExpression: String, val expression: MathExpression ) : MathParsingError() + /** + * Indicates that a sub-term of the expression has unnecessary parentheses, e.g. '(2) + 1'. + * + * This is an optional error. + * + * @property rawExpression the raw string expression that has the extra parentheses + * @property expression the parsed sub-expression that has the extra parentheses + */ data class RedundantParenthesesForIndividualTermsError( val rawExpression: String, val expression: MathExpression ) : MathParsingError() + /** + * Indicates that an invalid symbol was encountered while parsing, e.g. '@'. + * + * This is an irrecoverable error. + * + * @property invalidSymbol the raw invalid symbol that was encountered during parsing + */ data class UnnecessarySymbolsError(val invalidSymbol: String) : MathParsingError() + /** + * Indicates that a number was encountered to the right of a variable, e.g. 'x2'. + * + * This is an irrecoverable error since the grammar specifically prohibits implicit multiplication + * of numbers on the right side. + * + * @property number the number that was parsed on the right side of the variable + * @property variable the variable to whose right is a number + */ data class NumberAfterVariableError(val number: Real, val variable: String) : MathParsingError() + /** + * Indicates that two binary operators were encountered with nothing between, e.g. '1 +* 2'. + * + * This is an irrecoverable error. + * + * @property operator1 the first (left) operator encountered + * @property operator2 the second (right) operator encountered + */ data class SubsequentBinaryOperatorsError( val operator1: String, val operator2: String ) : MathParsingError() + /** + * Indicates that two unary operators were encountered with nothing between, e.g. '--2'. + * + * This is an irrecoverable error. + */ object SubsequentUnaryOperatorsError : MathParsingError() + /** + * Indicates that a binary operator was encountered without a left-hand operand, e.g. '*2'. + * + * This is a generally an irrecoverable error. + * + * Note that operators that are both unary and binary (e.g. '-') cannot trigger this error since + * such cases will be correctly interpreted as a unary operation. + * + * Further, this error has one optional case for unary plus operators (i.e. with strict error + * checking '+2' will result in this error, but is otherwise valid in non-strict mode). + * + * @property operator the operator to whose left is no operand + * @property operatorSymbol the raw symbol used to represent [operator] (which can't be assumed as + * any particular value since multiple symbols can correspond to a single operator) + */ data class NoVariableOrNumberBeforeBinaryOperatorError( val operator: MathBinaryOperation.Operator, val operatorSymbol: String ) : MathParsingError() + /** + * Indicates that a binary operator was encountered without a right-hand operand, e.g. '2+'. + * + * This is an irrecoverable error. + * + * @property operator the operator to whose right is no operand + * @property operatorSymbol the raw symbol used to represent [operator] (which can't be assumed as + * any particular value since multiple symbols can correspond to a single operator) + */ data class NoVariableOrNumberAfterBinaryOperatorError( val operator: MathBinaryOperation.Operator, val operatorSymbol: String ) : MathParsingError() + /** + * Indicates that an exponent has a variable in its power, e.g. '2^x'. + * + * This is an optional error to help enforce polynomial syntax. + */ object ExponentIsVariableExpressionError : MathParsingError() + /** + * Indicates that an exponent's power is too large, e.g. '3^1000'. + * + * This is an optional error to help avoid calculation overflow for certain answer classifiers. + */ object ExponentTooLargeError : MathParsingError() + /** + * Indicates that an exponent's power is another exponent, e.g. '2^3^4'. + * + * This is an optional error to help avoid calculation overflow or potential learner mistakes. + */ object NestedExponentsError : MathParsingError() + /** + * Indicates that no value was found after a square root, e.g. '2√'. + * + * This is an irrecoverable error. + */ object HangingSquareRootError : MathParsingError() + /** + * Indicates that a value is being divided by zero, e.g. '2/0'. + * + * This is an optional error that helps avoid automatic failure in classifiers. Note that the + * absence of this error does not guarantee the expression has no divide-by-zeros (since it only + * performs a non-evaluative cursory check). + */ object TermDividedByZeroError : MathParsingError() + /** + * Indicates that a variable was encountered in a numeric-only expression, e.g. '1+3-x'. + * + * This is an irrecoverable error. + */ object VariableInNumericExpressionError : MathParsingError() + /** + * Indicates that one or more non-whitelisted variables were encountered in an algebraic + * expression. + * + * This is an optional error. + * + * @param variables the list of variables from the expression that aren't allowed + */ data class DisabledVariablesInUseError(val variables: List) : MathParsingError() + /** + * Indicates that an algebraic equation is missing an equals sign, e.g. '4 x'. + * + * This is an irrecoverable error. + */ object EquationIsMissingEqualsError: MathParsingError() + /** + * Indicates that an algebraic equation has too many equals signs, e.g. '4 == x'. + * + * This is an irrecoverable error. + */ object EquationHasTooManyEqualsError: MathParsingError() + /** + * Indicates that an algebraic equation is missing either its left or right side, e.g. '4=' and + * '=x'. + * + * This is an irrecoverable error. + */ object EquationMissingLhsOrRhsError : MathParsingError() + /** + * Indicates that a recognized disabled function was used, e.g. 'abs(x)'. + * + * This is an irrecoverable error since the proto structure for math expressions is strictly + * limited to supported functions. + * + * @param functionName the name of the used prohibited function + */ data class InvalidFunctionInUseError(val functionName: String) : MathParsingError() + /** + * Indicates that a function name was started, but not completed, e.g.: 'sqr(2)'. + * + * This is an irrecoverable error. + */ object FunctionNameIncompleteError : MathParsingError() + /** + * Indicates a generic error that wasn't specifically recognized as any of the others. + * + * This is an irrecoverable error, though it may be triggered by trying to find optional errors. + */ object GenericError : MathParsingError() } From aeec35cca2cab12a78d0bf15f3dd79c4372d8624 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Mon, 24 Jan 2022 17:55:57 -0800 Subject: [PATCH 061/162] Lint fixes. --- .../oppia/android/util/math/MathExpressionParser.kt | 6 +++--- .../org/oppia/android/util/math/MathParsingError.kt | 4 ++-- .../util/math/AlgebraicEquationParserTest.kt | 5 +++-- .../util/math/AlgebraicExpressionParserTest.kt | 10 +++++++--- .../android/util/math/MathExpressionParserTest.kt | 13 ++++++++----- .../util/math/NumericExpressionParserTest.kt | 3 ++- 6 files changed, 25 insertions(+), 16 deletions(-) diff --git a/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt b/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt index 41bc60243d5..2cd252618ea 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt @@ -23,6 +23,8 @@ import org.oppia.android.app.model.Real import org.oppia.android.util.math.MathExpressionParser.ParseContext.AlgebraicExpressionContext import org.oppia.android.util.math.MathExpressionParser.ParseContext.NumericExpressionContext import org.oppia.android.util.math.MathParsingError.DisabledVariablesInUseError +import org.oppia.android.util.math.MathParsingError.EquationHasTooManyEqualsError +import org.oppia.android.util.math.MathParsingError.EquationIsMissingEqualsError import org.oppia.android.util.math.MathParsingError.EquationMissingLhsOrRhsError import org.oppia.android.util.math.MathParsingError.ExponentIsVariableExpressionError import org.oppia.android.util.math.MathParsingError.ExponentTooLargeError @@ -61,11 +63,9 @@ import org.oppia.android.util.math.MathTokenizer.Companion.Token.PositiveRealNum import org.oppia.android.util.math.MathTokenizer.Companion.Token.RightParenthesisSymbol import org.oppia.android.util.math.MathTokenizer.Companion.Token.SquareRootSymbol import org.oppia.android.util.math.MathTokenizer.Companion.Token.VariableName +import org.oppia.android.util.math.PeekableIterator.Companion.toPeekableIterator import kotlin.math.absoluteValue import org.oppia.android.app.model.MathUnaryOperation.Operator as UnaryOperator -import org.oppia.android.util.math.MathParsingError.EquationHasTooManyEqualsError -import org.oppia.android.util.math.MathParsingError.EquationIsMissingEqualsError -import org.oppia.android.util.math.PeekableIterator.Companion.toPeekableIterator /** * Parser for numeric expressions, algebraic expressions, and algebraic equations. diff --git a/utility/src/main/java/org/oppia/android/util/math/MathParsingError.kt b/utility/src/main/java/org/oppia/android/util/math/MathParsingError.kt index 4fead75b5f2..8bb587c0660 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathParsingError.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathParsingError.kt @@ -199,14 +199,14 @@ sealed class MathParsingError { * * This is an irrecoverable error. */ - object EquationIsMissingEqualsError: MathParsingError() + object EquationIsMissingEqualsError : MathParsingError() /** * Indicates that an algebraic equation has too many equals signs, e.g. '4 == x'. * * This is an irrecoverable error. */ - object EquationHasTooManyEqualsError: MathParsingError() + object EquationHasTooManyEqualsError : MathParsingError() /** * Indicates that an algebraic equation is missing either its left or right side, e.g. '4=' and diff --git a/utility/src/test/java/org/oppia/android/util/math/AlgebraicEquationParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/AlgebraicEquationParserTest.kt index 01f0aeddc27..fa0ccedbb17 100644 --- a/utility/src/test/java/org/oppia/android/util/math/AlgebraicEquationParserTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/AlgebraicEquationParserTest.kt @@ -1,7 +1,7 @@ package org.oppia.android.util.math -import com.google.common.truth.Truth.assertThat import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat import org.junit.Test import org.junit.runner.RunWith import org.oppia.android.app.model.MathEquation @@ -258,7 +258,8 @@ class AlgebraicEquationParserTest { private companion object { private fun parseAlgebraicEquation( - expression: String, allowedVariables: List = listOf("x", "y", "z") + expression: String, + allowedVariables: List = listOf("x", "y", "z") ): MathEquation { val result = MathExpressionParser.parseAlgebraicEquation( expression, allowedVariables, ErrorCheckingMode.ALL_ERRORS diff --git a/utility/src/test/java/org/oppia/android/util/math/AlgebraicExpressionParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/AlgebraicExpressionParserTest.kt index 7950c52b336..be190a7a520 100644 --- a/utility/src/test/java/org/oppia/android/util/math/AlgebraicExpressionParserTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/AlgebraicExpressionParserTest.kt @@ -943,7 +943,8 @@ class AlgebraicExpressionParserTest { private companion object { private fun parseAlgebraicExpressionWithoutOptionalErrors( - expression: String, allowedVariables: List = listOf("x", "y", "z") + expression: String, + allowedVariables: List = listOf("x", "y", "z") ): MathExpression { return parseAlgebraicExpressionInternal( expression, ErrorCheckingMode.REQUIRED_ONLY, allowedVariables @@ -951,7 +952,8 @@ class AlgebraicExpressionParserTest { } private fun parseAlgebraicExpressionWithAllErrors( - expression: String, allowedVariables: List = listOf("x", "y", "z") + expression: String, + allowedVariables: List = listOf("x", "y", "z") ): MathExpression { return parseAlgebraicExpressionInternal( expression, ErrorCheckingMode.ALL_ERRORS, allowedVariables @@ -959,7 +961,9 @@ class AlgebraicExpressionParserTest { } private fun parseAlgebraicExpressionInternal( - expression: String, errorCheckingMode: ErrorCheckingMode, allowedVariables: List + expression: String, + errorCheckingMode: ErrorCheckingMode, + allowedVariables: List ): MathExpression { val result = MathExpressionParser.parseAlgebraicExpression( expression, allowedVariables, errorCheckingMode diff --git a/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt index 5a37e71a372..198d24551b4 100644 --- a/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt @@ -993,7 +993,6 @@ class MathExpressionParserTest { val expression = "$func(0.5+1)" val error = expectFailureWhenParsingAlgebraicExpression(expression) - // Starting a detected function but not completing it should result in an incomplete name error. assertThat(error).isFunctionNameIncomplete() } @@ -1092,7 +1091,8 @@ class MathExpressionParserTest { ) private fun expectSuccessWhenParsingNumericExpression( - expression: String, errorCheckingMode: ErrorCheckingMode = ALL_ERRORS + expression: String, + errorCheckingMode: ErrorCheckingMode = ALL_ERRORS ) { expectSuccessfulParsingResult(parseNumericExpression(expression, errorCheckingMode)) } @@ -1112,7 +1112,8 @@ class MathExpressionParserTest { } private fun expectFailureWhenParsingAlgebraicExpression( - expression: String, allowedVariables: List = listOf("x", "y", "z") + expression: String, + allowedVariables: List = listOf("x", "y", "z") ): MathParsingError { return expectFailingParsingResult(parseAlgebraicExpression(expression, allowedVariables)) } @@ -1126,13 +1127,15 @@ class MathExpressionParserTest { } private fun parseNumericExpression( - expression: String, errorCheckingMode: ErrorCheckingMode = ALL_ERRORS + expression: String, + errorCheckingMode: ErrorCheckingMode = ALL_ERRORS ): MathParsingResult { return MathExpressionParser.parseNumericExpression(expression, errorCheckingMode) } private fun parseAlgebraicExpression( - expression: String, allowedVariables: List, + expression: String, + allowedVariables: List, errorCheckingMode: ErrorCheckingMode = ALL_ERRORS ): MathParsingResult { return MathExpressionParser.parseAlgebraicExpression( diff --git a/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt index 42971dc5d57..b34df9875f6 100644 --- a/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt @@ -1576,7 +1576,8 @@ class NumericExpressionParserTest { parseNumericExpressionInternal(expression, ErrorCheckingMode.ALL_ERRORS) private fun parseNumericExpressionInternal( - expression: String, errorCheckingMode: ErrorCheckingMode + expression: String, + errorCheckingMode: ErrorCheckingMode ): MathExpression { val result = MathExpressionParser.parseNumericExpression(expression, errorCheckingMode) assertThat(result).isInstanceOf(MathParsingResult.Success::class.java) From 57e6f5b889e621a1ba83630df950a2e289e86805 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Mon, 24 Jan 2022 18:26:26 -0800 Subject: [PATCH 062/162] Remove temporary TODOs. --- .../java/org/oppia/android/util/math/MathExpressionParser.kt | 3 --- 1 file changed, 3 deletions(-) diff --git a/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt b/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt index 2cd252618ea..20b44cee192 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt @@ -84,9 +84,6 @@ import org.oppia.android.app.model.MathUnaryOperation.Operator as UnaryOperator * https://docs.google.com/document/d/1JMpbjqRqdEpye67HvDoqBo_rtScY9oEaB7SwKBBspss/edit#bookmark=id.wtmim9gp20a6. */ class MathExpressionParser private constructor(private val parseContext: ParseContext) { - // TODO: document that 'generic' means either 'numeric' or 'algebraic' (ie that the expression is syntactically the same between both grammars). - // TODO: document that one design goal is keeping the grammar for this parser as LL(1) & why. - private fun parseGenericEquationGrammar(): MathParsingResult { // generic_equation_grammar = generic_equation ; return parseGenericEquation().maybeFail { equation -> From 07be5960af2d894773bbb8c2263ffd8bfda50100 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 26 Jan 2022 15:43:52 -0800 Subject: [PATCH 063/162] Add tests. --- .../oppia/android/testing/math/RealSubject.kt | 16 +- .../org/oppia/android/util/math/BUILD.bazel | 18 +- .../android/util/math/FractionExtensions.kt | 10 +- .../oppia/android/util/math/FractionParser.kt | 72 + .../oppia/android/util/math/RealExtensions.kt | 79 +- .../org/oppia/android/util/math/BUILD.bazel | 58 +- .../math/ExpressionToLatexConverterTest.kt | 215 +++ .../util/math/ExpressionToLatexTest.kt | 123 -- .../util/math/FractionExtensionsTest.kt | 887 +++++++++++ .../util/math/MathExpressionExtensionsTest.kt | 97 ++ .../math/NumericExpressionEvaluatorTest.kt | 239 +++ .../util/math/NumericExpressionParserTest.kt | 90 +- .../android/util/math/RealExtensionsTest.kt | 1385 ++++++++++++++++- 13 files changed, 3112 insertions(+), 177 deletions(-) create mode 100644 utility/src/main/java/org/oppia/android/util/math/FractionParser.kt create mode 100644 utility/src/test/java/org/oppia/android/util/math/ExpressionToLatexConverterTest.kt delete mode 100644 utility/src/test/java/org/oppia/android/util/math/ExpressionToLatexTest.kt create mode 100644 utility/src/test/java/org/oppia/android/util/math/MathExpressionExtensionsTest.kt create mode 100644 utility/src/test/java/org/oppia/android/util/math/NumericExpressionEvaluatorTest.kt diff --git a/testing/src/main/java/org/oppia/android/testing/math/RealSubject.kt b/testing/src/main/java/org/oppia/android/testing/math/RealSubject.kt index bb8a180b306..908bc7be6e2 100644 --- a/testing/src/main/java/org/oppia/android/testing/math/RealSubject.kt +++ b/testing/src/main/java/org/oppia/android/testing/math/RealSubject.kt @@ -22,15 +22,17 @@ import org.oppia.android.testing.math.FractionSubject.Companion.assertThat */ class RealSubject private constructor( metadata: FailureMetadata, - private val actual: Real + private val actual: Real? ) : LiteProtoSubject(metadata, actual) { + private val nonNullActual by lazy { checkNotNull(actual) { "Expected real to be non-null" } } + /** * Returns a [FractionSubject] to test [Real.getRational]. This will fail if the [Real] pertaining * to this subject is not of type rational. */ fun isRationalThat(): FractionSubject { verifyTypeToBe(Real.RealTypeCase.RATIONAL) - return assertThat(actual.rational) + return assertThat(nonNullActual.rational) } /** @@ -39,7 +41,7 @@ class RealSubject private constructor( */ fun isIrrationalThat(): DoubleSubject { verifyTypeToBe(Real.RealTypeCase.IRRATIONAL) - return assertThat(actual.irrational) + return assertThat(nonNullActual.irrational) } /** @@ -48,17 +50,17 @@ class RealSubject private constructor( */ fun isIntegerThat(): IntegerSubject { verifyTypeToBe(Real.RealTypeCase.INTEGER) - return assertThat(actual.integer) + return assertThat(nonNullActual.integer) } private fun verifyTypeToBe(expected: Real.RealTypeCase) { - assertWithMessage("Expected real type to be $expected, not: ${actual.realTypeCase}") - .that(actual.realTypeCase) + assertWithMessage("Expected real type to be $expected, not: ${nonNullActual.realTypeCase}") + .that(nonNullActual.realTypeCase) .isEqualTo(expected) } companion object { /** Returns a new [RealSubject] to verify aspects of the specified [Real] value. */ - fun assertThat(actual: Real): RealSubject = assertAbout(::RealSubject).that(actual) + fun assertThat(actual: Real?): RealSubject = assertAbout(::RealSubject).that(actual) } } diff --git a/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel b/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel index 1f2e9313754..00b17035b03 100644 --- a/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel @@ -33,12 +33,12 @@ kt_android_library( ) kt_android_library( - name = "parser", + name = "math_expression_parser", srcs = [ "MathExpressionParser.kt", ], visibility = [ - "//:oppia_testing_visibility", + "//:oppia_api_visibility", ], deps = [ ":parsing_error", @@ -49,6 +49,20 @@ kt_android_library( ], ) +kt_android_library( + name = "fraction_parser", + srcs = [ + "FractionParser.kt", + ], + visibility = [ + "//:oppia_api_visibility", + ], + deps = [ + "//model/src/main/proto:math_java_proto_lite", + "//utility/src/main/java/org/oppia/android/util/extensions:string_extensions", + ], +) + kt_android_library( name = "tokenizer", srcs = [ diff --git a/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt index 7457b329777..77e1e7ef420 100644 --- a/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt @@ -106,7 +106,7 @@ fun Fraction.toImproperForm(): Fraction { } /** Returns the inverse improper fraction representation of this fraction. */ -fun Fraction.toInvertedImproperForm(): Fraction { +private fun Fraction.toInvertedImproperForm(): Fraction { return toImproperForm().let { improper -> improper.toBuilder().apply { numerator = improper.denominator @@ -188,7 +188,8 @@ operator fun Fraction.div(rhs: Fraction): Fraction { return this * rhs.toInvertedImproperForm() } -fun Fraction.pow(exp: Int): Fraction { +// TODO: document 0^0 case. +infix fun Fraction.pow(exp: Int): Fraction { return when { exp == 0 -> { Fraction.newBuilder().apply { @@ -198,11 +199,11 @@ fun Fraction.pow(exp: Int): Fraction { } exp == 1 -> this // x^-2 == 1/(x^2). - exp < 1 -> pow(-exp).toInvertedImproperForm().toProperForm() + exp < 1 -> (this pow -exp).toInvertedImproperForm().toProperForm() else -> { // i > 1 var newValue = this for (i in 1 until exp) newValue *= this - return newValue + return newValue.toProperForm() } } } @@ -218,6 +219,7 @@ fun Int.toWholeNumberFraction(): Fraction { }.build() } + /** Returns the greatest common divisor between two integers. */ private fun gcd(x: Int, y: Int): Int { return if (y == 0) x else gcd(y, x % y) diff --git a/utility/src/main/java/org/oppia/android/util/math/FractionParser.kt b/utility/src/main/java/org/oppia/android/util/math/FractionParser.kt new file mode 100644 index 00000000000..e5f30a36fbc --- /dev/null +++ b/utility/src/main/java/org/oppia/android/util/math/FractionParser.kt @@ -0,0 +1,72 @@ +package org.oppia.android.util.math + +import org.oppia.android.app.model.Fraction +import org.oppia.android.domain.util.normalizeWhitespace + +/** String parser for [Fraction]s. */ +class FractionParser private constructor() { + companion object { + private val wholeNumberOnlyRegex = """^-? ?(\d+)$""".toRegex() + private val fractionOnlyRegex = """^-? ?(\d+) ?/ ?(\d+)$""".toRegex() + private val mixedNumberRegex = """^-? ?(\d+) (\d+) ?/ ?(\d+)$""".toRegex() + + /** + * Returns a [Fraction] parse from the specified raw text string. + * + * Unlike [tryParseFraction] this function will throw if the provided text is invalid. + */ + fun parseFraction(text: String): Fraction { + return tryParseFraction(text) + ?: throw IllegalArgumentException("Incorrectly formatted fraction: $text") + } + + /** + * Returns a [Fraction] parse from the specified raw text string, or null if the provided text + * doesn't correctly represent a fraction. + */ + fun tryParseFraction(text: String): Fraction? { + // Normalize whitespace to ensure that answer follows a simpler subset of possible patterns. + val inputText: String = text.normalizeWhitespace() + return parseMixedNumber(inputText) + ?: parseRegularFraction(inputText) + ?: parseWholeNumber(inputText) + } + + private fun parseMixedNumber(inputText: String): Fraction? { + val mixedNumberMatch = mixedNumberRegex.matchEntire(inputText) ?: return null + val (_, wholeNumberText, numeratorText, denominatorText) = + mixedNumberMatch.groupValues + return Fraction.newBuilder() + .setIsNegative(isInputNegative(inputText)) + .setWholeNumber(wholeNumberText.toInt()) + .setNumerator(numeratorText.toInt()) + .setDenominator(denominatorText.toInt()) + .build() + } + + private fun parseRegularFraction(inputText: String): Fraction? { + val fractionOnlyMatch = fractionOnlyRegex.matchEntire(inputText) ?: return null + val (_, numeratorText, denominatorText) = fractionOnlyMatch.groupValues + // Fraction-only numbers imply no whole number. + return Fraction.newBuilder() + .setIsNegative(isInputNegative(inputText)) + .setNumerator(numeratorText.toInt()) + .setDenominator(denominatorText.toInt()) + .build() + } + + private fun parseWholeNumber(inputText: String): Fraction? { + val wholeNumberMatch = wholeNumberOnlyRegex.matchEntire(inputText) ?: return null + val (_, wholeNumberText) = wholeNumberMatch.groupValues + // Whole number fractions imply '0/1' fractional parts. + return Fraction.newBuilder() + .setIsNegative(isInputNegative(inputText)) + .setWholeNumber(wholeNumberText.toInt()) + .setNumerator(0) + .setDenominator(1) + .build() + } + + private fun isInputNegative(inputText: String): Boolean = inputText.startsWith("-") + } +} diff --git a/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt index 11251f1060c..8fbff687a66 100644 --- a/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt @@ -105,7 +105,15 @@ operator fun Real.div(rhs: Real): Real { ) } -fun Real.pow(rhs: Real): Real { +// TODO: document that roots represents the real value representation vs. principal root. Also, +// document 0^0 case per https://stackoverflow.com/a/19955996. +// Rules: +// - Anything involving a double always becomes a double. +// - Int^Int stays int unless it's negative (then it becomes a fraction) +// - Int^Fraction is treated as a fraction power & root (it becomes fraction or double) +// - Fraction^Int always yields a fraction +// - Fraction^Fraction yields a fraction or double (depending on the denominator root) +infix fun Real.pow(rhs: Real): Real { // Powers can really only be effectively done via floats or whole-number only fractions. return when (realTypeCase) { RATIONAL -> { @@ -113,11 +121,11 @@ fun Real.pow(rhs: Real): Real { when (rhs.realTypeCase) { // Anything raised by a fraction is pow'd by the numerator and rooted by the denominator. RATIONAL -> rhs.rational.toImproperForm().let { power -> - rational.pow(power.numerator).root(power.denominator, power.isNegative) + (rational pow power.numerator).root(power.denominator, power.isNegative) } IRRATIONAL -> recompute { it.setIrrational(rational.pow(rhs.irrational)) } - INTEGER -> recompute { it.setRational(rational.pow(rhs.integer)) } - REALTYPE_NOT_SET, null -> throw Exception("Invalid real: $rhs.") + INTEGER -> recompute { it.setRational(rational pow rhs.integer) } + REALTYPE_NOT_SET, null -> throw IllegalStateException("Invalid real: $rhs.") } } IRRATIONAL -> { @@ -126,7 +134,7 @@ fun Real.pow(rhs: Real): Real { RATIONAL -> recompute { it.setIrrational(irrational.pow(rhs.rational)) } IRRATIONAL -> recompute { it.setIrrational(irrational.pow(rhs.irrational)) } INTEGER -> recompute { it.setIrrational(irrational.pow(rhs.integer)) } - REALTYPE_NOT_SET, null -> throw Exception("Invalid real: $rhs.") + REALTYPE_NOT_SET, null -> throw IllegalStateException("Invalid real: $rhs.") } } INTEGER -> { @@ -135,16 +143,16 @@ fun Real.pow(rhs: Real): Real { // An integer raised to a fraction can use the same approach as above (fraction raised to // fraction) by treating the integer as a whole number fraction. RATIONAL -> rhs.rational.toImproperForm().let { power -> - integer.toWholeNumberFraction() - .pow(power.numerator) - .root(power.denominator, power.isNegative) + (integer.toWholeNumberFraction() pow power.numerator).root( + power.denominator, power.isNegative + ) } IRRATIONAL -> recompute { it.setIrrational(integer.toDouble().pow(rhs.irrational)) } INTEGER -> integer.pow(rhs.integer) - REALTYPE_NOT_SET, null -> throw Exception("Invalid real: $rhs.") + REALTYPE_NOT_SET, null -> throw IllegalStateException("Invalid real: $rhs.") } } - REALTYPE_NOT_SET, null -> throw Exception("Invalid real: $this.") + REALTYPE_NOT_SET, null -> throw IllegalStateException("Invalid real: $this.") } } @@ -153,7 +161,7 @@ fun sqrt(real: Real): Real { RATIONAL -> sqrt(real.rational) IRRATIONAL -> real.recompute { it.setIrrational(kotlin.math.sqrt(real.irrational)) } INTEGER -> sqrt(real.integer) - REALTYPE_NOT_SET, null -> throw Exception("Invalid real: $real.") + REALTYPE_NOT_SET, null -> throw IllegalStateException("Invalid real: $real.") } } @@ -199,7 +207,7 @@ private fun Int.divide(rhs: Int): Real = Real.newBuilder().apply { isNegative = (lhs < 0) xor (rhs < 0) numerator = kotlin.math.abs(lhs) denominator = kotlin.math.abs(rhs) - }.build() + }.build().toProperForm() } }.build() @@ -208,9 +216,9 @@ private fun Fraction.pow(rhs: Double): Double = toDouble().pow(rhs) private fun Int.pow(exp: Int): Real { return when { - exp == 0 -> Real.newBuilder().apply { integer = 0 }.build() + exp == 0 -> Real.newBuilder().apply { integer = 1 }.build() exp == 1 -> Real.newBuilder().apply { integer = this@pow }.build() - exp < 0 -> Real.newBuilder().apply { rational = toWholeNumberFraction().pow(exp) }.build() + exp < 0 -> Real.newBuilder().apply { rational = toWholeNumberFraction() pow exp }.build() else -> { // exp > 1 var computed = this @@ -223,7 +231,7 @@ private fun Int.pow(exp: Int): Real { private fun sqrt(fraction: Fraction): Real = fraction.root(base = 2, invert = false) private fun Fraction.root(base: Int, invert: Boolean): Real { - check(base > 1) { "Expected base of 2 or higher, not: $base" } + check(base > 0) { "Expected base of 1 or higher, not: $base" } val adjustedFraction = toImproperForm() val adjustedNum = @@ -253,16 +261,39 @@ private fun sqrt(int: Int): Real = root(int, base = 2) private fun root(int: Int, base: Int): Real { // First, check if the integer is a root. Base reference for possible methods: // https://www.researchgate.net/post/How-to-decide-if-a-given-number-will-have-integer-square-root-or-not. - check(base > 1) { "Expected base of 2 or higher, not: $base" } - check((int < 0 && base.isOdd()) || int >= 0) { "Radicand results in imaginary number: $int" } - - if (int == 1) { - // 1^x is always 1. + if (int == 0 && base == 0) { + // This is considered a conventional identity per https://stackoverflow.com/a/19955996 that + // doesn't match mathematics definitions (but it does bring parity with the system's pow() + // function). return Real.newBuilder().apply { integer = 1 }.build() } + check(base > 0) { "Expected base of 1 or higher, not: $base" } + check((int < 0 && base.isOdd()) || int >= 0) { "Radicand results in imaginary number: $int" } + + when { + int == 0 -> { + // 0^x is always zero. + return Real.newBuilder().apply { + integer = 0 + }.build() + } + int == 1 || int == 0 || base == 0 -> { + // 1^x and x^0 are always 1. + return Real.newBuilder().apply { + integer = 1 + }.build() + } + base == 1 -> { + // x^1 is always x. + return Real.newBuilder().apply { + integer = int + }.build() + } + } + val radicand = int.absoluteValue var potentialRoot = base while (potentialRoot.pow(base).integer < radicand) { @@ -319,7 +350,7 @@ private fun combine( } INTEGER -> lhs.recompute { it.setRational(leftRationalRightIntegerOp(lhs.rational, rhs.integer)) } - REALTYPE_NOT_SET, null -> throw Exception("Invalid real: $rhs.") + REALTYPE_NOT_SET, null -> throw IllegalStateException("Invalid real: $rhs.") } } IRRATIONAL -> { @@ -337,7 +368,7 @@ private fun combine( lhs.recompute { it.setIrrational(leftIrrationalRightIntegerOp(lhs.irrational, rhs.integer)) } - REALTYPE_NOT_SET, null -> throw Exception("Invalid real: $rhs.") + REALTYPE_NOT_SET, null -> throw IllegalStateException("Invalid real: $rhs.") } } INTEGER -> { @@ -350,9 +381,9 @@ private fun combine( it.setIrrational(leftIntegerRightIrrationalOp(lhs.integer, rhs.irrational)) } INTEGER -> leftIntegerRightIntegerOp(lhs.integer, rhs.integer) - REALTYPE_NOT_SET, null -> throw Exception("Invalid real: $rhs.") + REALTYPE_NOT_SET, null -> throw IllegalStateException("Invalid real: $rhs.") } } - REALTYPE_NOT_SET, null -> throw Exception("Invalid real: $lhs.") + REALTYPE_NOT_SET, null -> throw IllegalStateException("Invalid real: $lhs.") } } diff --git a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel index 5e11de885bb..a557f383254 100644 --- a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel @@ -19,7 +19,7 @@ oppia_android_test( "//third_party:junit_junit", "//third_party:org_robolectric_robolectric", "//third_party:robolectric_android-all", - "//utility/src/main/java/org/oppia/android/util/math:parser", + "//utility/src/main/java/org/oppia/android/util/math:math_expression_parser", ], ) @@ -38,15 +38,15 @@ oppia_android_test( "//third_party:junit_junit", "//third_party:org_robolectric_robolectric", "//third_party:robolectric_android-all", - "//utility/src/main/java/org/oppia/android/util/math:parser", + "//utility/src/main/java/org/oppia/android/util/math:math_expression_parser", ], ) oppia_android_test( - name = "ExpressionToLatexTest", - srcs = ["ExpressionToLatexTest.kt"], + name = "ExpressionToLatexConverterTest", + srcs = ["ExpressionToLatexConverterTest.kt"], custom_package = "org.oppia.android.util.math", - test_class = "org.oppia.android.util.math.ExpressionToLatexTest", + test_class = "org.oppia.android.util.math.ExpressionToLatexConverterTest", test_manifest = "//utility:test_manifest", deps = [ "//model/src/main/proto:math_java_proto_lite", @@ -58,7 +58,7 @@ oppia_android_test( "//third_party:junit_junit", "//third_party:org_robolectric_robolectric", "//third_party:robolectric_android-all", - "//utility/src/main/java/org/oppia/android/util/math:parser", + "//utility/src/main/java/org/oppia/android/util/math:math_expression_parser", ], ) @@ -98,6 +98,25 @@ oppia_android_test( ], ) +oppia_android_test( + name = "MathExpressionExtensionsTest", + srcs = ["MathExpressionExtensionsTest.kt"], + custom_package = "org.oppia.android.util.math", + test_class = "org.oppia.android.util.math.MathExpressionExtensionsTest", + test_manifest = "//utility:test_manifest", + deps = [ + "//model/src/main/proto:math_java_proto_lite", + "//testing/src/main/java/org/oppia/android/testing/math:real_subject", + "//third_party:androidx_test_ext_junit", + "//third_party:com_google_truth_truth", + "//third_party:junit_junit", + "//third_party:org_robolectric_robolectric", + "//third_party:robolectric_android-all", + "//utility/src/main/java/org/oppia/android/util/math:extensions", + "//utility/src/main/java/org/oppia/android/util/math:math_expression_parser", + ], +) + oppia_android_test( name = "MathExpressionParserTest", srcs = ["MathExpressionParserTest.kt"], @@ -114,7 +133,7 @@ oppia_android_test( "//third_party:junit_junit", "//third_party:org_robolectric_robolectric", "//third_party:robolectric_android-all", - "//utility/src/main/java/org/oppia/android/util/math:parser", + "//utility/src/main/java/org/oppia/android/util/math:math_expression_parser", ], ) @@ -138,6 +157,25 @@ oppia_android_test( ], ) +oppia_android_test( + name = "NumericExpressionEvaluatorTest", + srcs = ["NumericExpressionEvaluatorTest.kt"], + custom_package = "org.oppia.android.util.math", + test_class = "org.oppia.android.util.math.NumericExpressionEvaluatorTest", + test_manifest = "//utility:test_manifest", + deps = [ + "//model/src/main/proto:math_java_proto_lite", + "//testing:assertion_helpers", + "//testing/src/main/java/org/oppia/android/testing/math:real_subject", + "//third_party:androidx_test_ext_junit", + "//third_party:com_google_truth_truth", + "//third_party:junit_junit", + "//third_party:org_robolectric_robolectric", + "//third_party:robolectric_android-all", + "//utility/src/main/java/org/oppia/android/util/math:math_expression_parser", + ], +) + oppia_android_test( name = "NumericExpressionParserTest", srcs = ["NumericExpressionParserTest.kt"], @@ -153,7 +191,7 @@ oppia_android_test( "//third_party:junit_junit", "//third_party:org_robolectric_robolectric", "//third_party:robolectric_android-all", - "//utility/src/main/java/org/oppia/android/util/math:parser", + "//utility/src/main/java/org/oppia/android/util/math:math_expression_parser", ], ) @@ -219,12 +257,14 @@ oppia_android_test( deps = [ "//model/src/main/proto:math_java_proto_lite", "//testing", + "//testing/src/main/java/org/oppia/android/testing/junit:oppia_parameterized_test_runner", + "//testing/src/main/java/org/oppia/android/testing/junit:parameterized_robolectric_test_runner", "//testing/src/main/java/org/oppia/android/testing/math:real_subject", - "//third_party:androidx_test_ext_junit", "//third_party:com_google_truth_truth", "//third_party:junit_junit", "//third_party:org_robolectric_robolectric", "//third_party:robolectric_android-all", "//utility/src/main/java/org/oppia/android/util/math:extensions", + "//utility/src/main/java/org/oppia/android/util/math:fraction_parser", ], ) diff --git a/utility/src/test/java/org/oppia/android/util/math/ExpressionToLatexConverterTest.kt b/utility/src/test/java/org/oppia/android/util/math/ExpressionToLatexConverterTest.kt new file mode 100644 index 00000000000..87be7c78451 --- /dev/null +++ b/utility/src/test/java/org/oppia/android/util/math/ExpressionToLatexConverterTest.kt @@ -0,0 +1,215 @@ +package org.oppia.android.util.math + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.oppia.android.app.model.MathEquation +import org.oppia.android.app.model.MathExpression +import org.oppia.android.testing.math.MathEquationSubject.Companion.assertThat +import org.oppia.android.testing.math.MathExpressionSubject.Companion.assertThat +import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode +import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult +import org.robolectric.annotation.LooperMode + +/** Tests for [ExpressionToLatexConverter]. */ +// FunctionName: test names are conventionally named with underscores. +// SameParameterValue: tests should have specific context included/excluded for readability. +@Suppress("FunctionName", "SameParameterValue") +@RunWith(AndroidJUnit4::class) +@LooperMode(LooperMode.Mode.PAUSED) +class ExpressionToLatexConverterTest { + @Test + fun testConvert_numericExp_number_returnsConstantLatex() { + val exp = parseNumericExpressionWithAllErrors("1") + + assertThat(exp).convertsToLatexStringThat().isEqualTo("1") + } + + @Test + fun testConvert_numericExp_unaryPlus_withoutOptionalErrors_returnLatexWithUnaryPlus() { + val exp = parseNumericExpressionWithoutOptionalErrors("+1") + + assertThat(exp).convertsToLatexStringThat().isEqualTo("+1") + } + + @Test + fun testConvert_numericExp_unaryMinus_returnLatexWithUnaryMinus() { + val exp = parseNumericExpressionWithAllErrors("-1") + + assertThat(exp).convertsToLatexStringThat().isEqualTo("-1") + } + + @Test + fun testConvert_numericExp_addition_returnsLatexWithAddition() { + val exp = parseNumericExpressionWithAllErrors("1+2") + + assertThat(exp).convertsToLatexStringThat().isEqualTo("1 + 2") + } + + @Test + fun testConvert_numericExp_subtraction_returnsLatexWithSubtract() { + val exp = parseNumericExpressionWithAllErrors("1-2") + + assertThat(exp).convertsToLatexStringThat().isEqualTo("1 - 2") + } + + @Test + fun testConvert_numericExp_multiplication_returnsLatexWithMultiplication() { + val exp = parseNumericExpressionWithAllErrors("2*3") + + assertThat(exp).convertsToLatexStringThat().isEqualTo("2 \\times 3") + } + + @Test + fun testConvert_numericExp_division_returnsLatexWithDivision() { + val exp = parseNumericExpressionWithAllErrors("2/3") + + assertThat(exp).convertsToLatexStringThat().isEqualTo("2 \\div 3") + } + + @Test + fun testConvert_numericExp_division_divAsFraction_returnsLatexWithFraction() { + val exp = parseNumericExpressionWithAllErrors("2/3") + + assertThat(exp).convertsWithFractionsToLatexStringThat().isEqualTo("\\frac{2}{3}") + } + + @Test + fun testConvert_numericExp_multipleDivisions_divAsFraction_returnsLatexWithFractions() { + val exp = parseNumericExpressionWithAllErrors("2/3/4") + + assertThat(exp).convertsWithFractionsToLatexStringThat().isEqualTo("\\frac{\\frac{2}{3}}{4}") + } + + @Test + fun testConvert_numericExp_exponent_returnsLatexWithExponent() { + val exp = parseNumericExpressionWithAllErrors("2^3") + + assertThat(exp).convertsToLatexStringThat().isEqualTo("2 ^ {3}") + } + + @Test + fun testConvert_numericExp_inlineSquareRoot_returnsLatexWithSquareRoot() { + val exp = parseNumericExpressionWithAllErrors("√2") + + assertThat(exp).convertsToLatexStringThat().isEqualTo("\\sqrt{2}") + } + + @Test + fun testConvert_numericExp_inlineSquareRoot_operationArg_returnsLatexWithSquareRoot() { + val exp = parseNumericExpressionWithAllErrors("√(1+2)") + + assertThat(exp).convertsToLatexStringThat().isEqualTo("\\sqrt{(1 + 2)}") + } + + @Test + fun testConvert_numericExp_squareRoot_returnsLatexWithSquareRoot() { + val exp = parseNumericExpressionWithAllErrors("sqrt(2)") + + assertThat(exp).convertsToLatexStringThat().isEqualTo("\\sqrt{2}") + } + + @Test + fun testConvert_numericExp_squareRoot_operationArg_returnsLatexWithSquareRoot() { + val exp = parseNumericExpressionWithAllErrors("sqrt(1+2)") + + assertThat(exp).convertsToLatexStringThat().isEqualTo("\\sqrt{1 + 2}") + } + + @Test + fun testConvert_numericExp_parentheses_returnsLatexWithGroup() { + val exp = parseNumericExpressionWithAllErrors("2/(3+4)") + + assertThat(exp).convertsToLatexStringThat().isEqualTo("2 \\div (3 + 4)") + } + + @Test + fun testConvert_numericExp_exponentToGroup_returnsCorrectlyWrappedLatex() { + val exp = parseNumericExpressionWithAllErrors("2^(7-3)") + + assertThat(exp).convertsToLatexStringThat().isEqualTo("2 ^ {(7 - 3)}") + } + + @Test + fun testConvert_algebraicExp_variable_returnsVariableLatex() { + val exp = parseAlgebraicExpressionWithAllErrors("x") + + assertThat(exp).convertsToLatexStringThat().isEqualTo("x") + } + + @Test + fun testConvert_algebraicExp_twoX_returnsLatexWithImplicitMultiplication() { + val exp = parseAlgebraicExpressionWithAllErrors("2x") + + assertThat(exp).convertsToLatexStringThat().isEqualTo("2x") + } + + @Test + fun testConvert_algebraicEq_xEqualsOne_returnsLatexWithEquals() { + val exp = parseAlgebraicEquationWithAllErrors("x=1") + + assertThat(exp).convertsToLatexStringThat().isEqualTo("x = 1") + } + + @Test + fun testConvert_algebraicEq_complexExpression_returnsCorrectLatex() { + val exp = parseAlgebraicEquationWithAllErrors("(x+1)(x-2)=(x^3+2x^2-5x-6)/(x+3)") + + assertThat(exp) + .convertsToLatexStringThat() + .isEqualTo("(x + 1)(x - 2) = (x ^ {3} + 2x ^ {2} - 5x - 6) \\div (x + 3)") + } + + @Test + fun testConvert_algebraicEq_complexExpression_divAsFraction_returnsCorrectLatex() { + val exp = parseAlgebraicEquationWithAllErrors("(x+1)(x-2)=(x^3+2x^2-5x-6)/(x+3)") + + assertThat(exp) + .convertsWithFractionsToLatexStringThat() + .isEqualTo("(x + 1)(x - 2) = \\frac{(x ^ {3} + 2x ^ {2} - 5x - 6)}{(x + 3)}") + } + + private companion object { + private fun parseNumericExpressionWithoutOptionalErrors(expression: String): MathExpression { + return parseNumericExpressionInternal(expression, ErrorCheckingMode.REQUIRED_ONLY) + } + + private fun parseNumericExpressionWithAllErrors(expression: String): MathExpression { + return parseNumericExpressionInternal(expression, ErrorCheckingMode.ALL_ERRORS) + } + + private fun parseAlgebraicExpressionWithAllErrors( + expression: String, + allowedVariables: List = listOf("x", "y", "z") + ): MathExpression { + return MathExpressionParser.parseAlgebraicExpression( + expression, allowedVariables, + ErrorCheckingMode.ALL_ERRORS + ).getExpectedSuccess() + } + + private fun parseNumericExpressionInternal( + expression: String, errorCheckingMode: ErrorCheckingMode + ): MathExpression { + return MathExpressionParser.parseNumericExpression( + expression, errorCheckingMode + ).getExpectedSuccess() + } + + private fun parseAlgebraicEquationWithAllErrors( + expression: String, + allowedVariables: List = listOf("x", "y", "z") + ): MathEquation { + return MathExpressionParser.parseAlgebraicEquation( + expression, allowedVariables, + ErrorCheckingMode.ALL_ERRORS + ).getExpectedSuccess() + } + + private inline fun MathParsingResult.getExpectedSuccess(): T { + assertThat(this).isInstanceOf(MathParsingResult.Success::class.java) + return (this as MathParsingResult.Success).result + } + } +} diff --git a/utility/src/test/java/org/oppia/android/util/math/ExpressionToLatexTest.kt b/utility/src/test/java/org/oppia/android/util/math/ExpressionToLatexTest.kt deleted file mode 100644 index e56db9a7500..00000000000 --- a/utility/src/test/java/org/oppia/android/util/math/ExpressionToLatexTest.kt +++ /dev/null @@ -1,123 +0,0 @@ -package org.oppia.android.util.math - -import androidx.test.ext.junit.runners.AndroidJUnit4 -import org.junit.Test -import org.junit.runner.RunWith -import org.oppia.android.app.model.MathEquation -import org.oppia.android.app.model.MathExpression -import org.oppia.android.testing.math.MathEquationSubject.Companion.assertThat -import org.oppia.android.testing.math.MathExpressionSubject.Companion.assertThat -import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode -import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult -import org.robolectric.annotation.LooperMode - -/** Tests for [MathExpressionParser]. */ -@RunWith(AndroidJUnit4::class) -@LooperMode(LooperMode.Mode.PAUSED) -class ExpressionToLatexTest { - @Test - fun testLatex() { - // TODO: split up & move to separate test suites. Finish test cases. - - val exp1 = parseNumericExpressionWithAllErrors("1") - assertThat(exp1).convertsToLatexStringThat().isEqualTo("1") - - val exp2 = parseNumericExpressionWithAllErrors("1+2") - assertThat(exp2).convertsToLatexStringThat().isEqualTo("1 + 2") - - val exp3 = parseNumericExpressionWithAllErrors("1*2") - assertThat(exp3).convertsToLatexStringThat().isEqualTo("1 \\times 2") - - val exp4 = parseNumericExpressionWithAllErrors("1/2") - assertThat(exp4).convertsToLatexStringThat().isEqualTo("1 \\div 2") - - val exp5 = parseNumericExpressionWithAllErrors("1/2") - assertThat(exp5).convertsWithFractionsToLatexStringThat().isEqualTo("\\frac{1}{2}") - - val exp10 = parseNumericExpressionWithAllErrors("√2") - assertThat(exp10).convertsToLatexStringThat().isEqualTo("\\sqrt{2}") - - val exp11 = parseNumericExpressionWithAllErrors("√(1/2)") - assertThat(exp11).convertsToLatexStringThat().isEqualTo("\\sqrt{(1 \\div 2)}") - - val exp6 = parseAlgebraicExpressionWithAllErrors("x+y") - assertThat(exp6).convertsToLatexStringThat().isEqualTo("x + y") - - val exp7 = parseAlgebraicExpressionWithoutOptionalErrors("x^(1/y)") - assertThat(exp7).convertsToLatexStringThat().isEqualTo("x ^ {(1 \\div y)}") - - val exp8 = parseAlgebraicExpressionWithoutOptionalErrors("x^(1/y)") - assertThat(exp8).convertsWithFractionsToLatexStringThat().isEqualTo("x ^ {(\\frac{1}{y})}") - - val exp9 = parseAlgebraicExpressionWithoutOptionalErrors("x^y^z") - assertThat(exp9).convertsWithFractionsToLatexStringThat().isEqualTo("x ^ {y ^ {z}}") - - val eq1 = - parseAlgebraicEquationWithAllErrors( - "7a^2+b^2+c^2=0", allowedVariables = listOf("a", "b", "c") - ) - assertThat(eq1).convertsToLatexStringThat().isEqualTo("7a ^ {2} + b ^ {2} + c ^ {2} = 0") - - val eq2 = parseAlgebraicEquationWithAllErrors("sqrt(1+x)/x=1") - assertThat(eq2).convertsToLatexStringThat().isEqualTo("\\sqrt{1 + x} \\div x = 1") - - val eq3 = parseAlgebraicEquationWithAllErrors("sqrt(1+x)/x=1") - assertThat(eq3) - .convertsWithFractionsToLatexStringThat() - .isEqualTo("\\frac{\\sqrt{1 + x}}{x} = 1") - } - - private companion object { - // TODO: fix helper API. - - private fun parseNumericExpressionWithAllErrors(expression: String): MathExpression { - val result = - MathExpressionParser.parseNumericExpression(expression, ErrorCheckingMode.ALL_ERRORS) - return (result as MathParsingResult.Success).result - } - - private fun parseAlgebraicExpressionWithoutOptionalErrors( - expression: String, - allowedVariables: List = listOf("x", "y", "z") - ): MathExpression { - val result = - parseAlgebraicExpressionInternal( - expression, ErrorCheckingMode.REQUIRED_ONLY, allowedVariables - ) - return (result as MathParsingResult.Success).result - } - - private fun parseAlgebraicExpressionWithAllErrors( - expression: String, - allowedVariables: List = listOf("x", "y", "z") - ): MathExpression { - val result = - parseAlgebraicExpressionInternal( - expression, ErrorCheckingMode.ALL_ERRORS, allowedVariables - ) - return (result as MathParsingResult.Success).result - } - - private fun parseAlgebraicExpressionInternal( - expression: String, - errorCheckingMode: ErrorCheckingMode, - allowedVariables: List = listOf("x", "y", "z") - ): MathParsingResult { - return MathExpressionParser.parseAlgebraicExpression( - expression, allowedVariables, errorCheckingMode - ) - } - - private fun parseAlgebraicEquationWithAllErrors( - expression: String, - allowedVariables: List = listOf("x", "y", "z") - ): MathEquation { - val result = - MathExpressionParser.parseAlgebraicEquation( - expression, allowedVariables, - ErrorCheckingMode.ALL_ERRORS - ) - return (result as MathParsingResult.Success).result - } - } -} diff --git a/utility/src/test/java/org/oppia/android/util/math/FractionExtensionsTest.kt b/utility/src/test/java/org/oppia/android/util/math/FractionExtensionsTest.kt index 48121da69d7..3913e7b6dda 100644 --- a/utility/src/test/java/org/oppia/android/util/math/FractionExtensionsTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/FractionExtensionsTest.kt @@ -26,6 +26,17 @@ class FractionExtensionsTest { denominator = 1 }.build() + private val ONE_FRACTION = Fraction.newBuilder().apply { + wholeNumber = 1 + denominator = 1 + }.build() + + private val NEGATIVE_ONE_FRACTION = Fraction.newBuilder().apply { + isNegative = true + wholeNumber = 1 + denominator = 1 + }.build() + private val ONE_HALF_FRACTION = Fraction.newBuilder().apply { numerator = 1 denominator = 2 @@ -37,6 +48,17 @@ class FractionExtensionsTest { denominator = 2 }.build() + private val ONE_THIRD_FRACTION = Fraction.newBuilder().apply { + numerator = 1 + denominator = 3 + }.build() + + private val NEGATIVE_ONE_THIRD_FRACTION = Fraction.newBuilder().apply { + isNegative = true + numerator = 1 + denominator = 3 + }.build() + private val ONE_AND_ONE_HALF_FRACTION = Fraction.newBuilder().apply { wholeNumber = 1 numerator = 1 @@ -183,6 +205,57 @@ class FractionExtensionsTest { assertThat(result).isTrue() } + @Test + fun testToWholeNumber_zeroFraction_returnsZero() { + val result = ZERO_FRACTION.toWholeNumber() + + assertThat(result).isEqualTo(0) + } + + @Test + fun testToWholeNumber_negativeZeroFraction_returnsZero() { + val result = NEGATIVE_ZERO_FRACTION.toWholeNumber() + + assertThat(result).isEqualTo(0) + } + + @Test + fun testToWholeNumber_two_returnsTwo() { + val result = TWO_FRACTION.toWholeNumber() + + assertThat(result).isEqualTo(2) + } + + @Test + fun testToWholeNumber_negativeTwo_returnsNegativeTwo() { + val result = NEGATIVE_TWO_FRACTION.toWholeNumber() + + assertThat(result).isEqualTo(-2) + } + + @Test + fun testToWholeNumber_oneHalf_returnsZero() { + val result = ONE_HALF_FRACTION.toWholeNumber() + + assertThat(result).isEqualTo(0) + } + + @Test + fun testToWholeNumber_oneAndOneHalf_returnsOne() { + val result = ONE_AND_ONE_HALF_FRACTION.toWholeNumber() + + assertThat(result).isEqualTo(1) + } + + @Test + fun testToWholeNumber_threeOnes_returnsZero() { + val result = THREE_ONES_FRACTION.toWholeNumber() + + // Even though the fraction is technically equivalent to '3', it being in improper form results + // in there not technically being a whole number component. + assertThat(result).isEqualTo(0) + } + @Test fun testToDouble_zeroFraction_returnsZero() { val result = ZERO_FRACTION.toDouble() @@ -374,6 +447,80 @@ class FractionExtensionsTest { assertThrows(ArithmeticException::class) { zeroDenominatorFraction.toSimplestForm() } } + @Test + fun testToProperForm_zeroFraction_returnsZero() { + val result = ZERO_FRACTION.toProperForm() + + assertThat(result).isEqualTo(ZERO_FRACTION) + } + + @Test + fun testToProperForm_two_returnsTwo() { + val result = TWO_FRACTION.toProperForm() + + assertThat(result).isEqualTo(TWO_FRACTION) + } + + @Test + fun testToProperForm_threeOnes_returnsThree() { + val result = THREE_ONES_FRACTION.toProperForm() + + // Correctly extract the '3' numerator to being a whole number. + assertThat(result).hasNegativePropertyThat().isFalse() + assertThat(result).hasNumeratorThat().isEqualTo(0) + assertThat(result).hasDenominatorThat().isEqualTo(1) + assertThat(result).hasWholeNumberThat().isEqualTo(3) + } + + @Test + fun testToProperForm_oneHalf_returnsOneHalf() { + val result = ONE_HALF_FRACTION.toProperForm() + + assertThat(result).isEqualTo(ONE_HALF_FRACTION) + } + + @Test + fun testToProperForm_oneAndOneHalf_returnsOneAndOneHalf() { + val result = ONE_AND_ONE_HALF_FRACTION.toProperForm() + + // 1 1/2 is already in proper form. + assertThat(result).isEqualTo(ONE_AND_ONE_HALF_FRACTION) + } + + @Test + fun testToProperForm_threeHalves_returnsOneAndOneHalf() { + val result = THREE_HALVES_FRACTION.toProperForm() + + // 3/2 -> 1 1/2. + assertThat(result).isEqualTo(ONE_AND_ONE_HALF_FRACTION) + } + + @Test + fun testToProperForm_largeNegativeImproperFraction_reducesToSimplestProperFraction() { + val largeImproperFraction = Fraction.newBuilder().apply { + isNegative = true + numerator = 1650 + denominator = 209 + }.build() + + val result = largeImproperFraction.toProperForm() + + // Unlike toSimplestForm, toProperForm also extracts a whole number after reducing to the + // simplest denominator. + assertThat(result).hasNegativePropertyThat().isTrue() + assertThat(result).hasNumeratorThat().isEqualTo(17) + assertThat(result).hasDenominatorThat().isEqualTo(19) + assertThat(result).hasWholeNumberThat().isEqualTo(7) + } + + @Test + fun testToProperForm_zeroDenominator_throwsException() { + val zeroDenominatorFraction = Fraction.getDefaultInstance() + + // Converting to simplest form results in a divide by zero in this case. + assertThrows(ArithmeticException::class) { zeroDenominatorFraction.toProperForm() } + } + @Test fun testToImproperForm_zero_returnsZeroFraction() { val result = ZERO_FRACTION.toImproperForm() @@ -527,4 +674,744 @@ class FractionExtensionsTest { assertThat(result).hasDenominatorThat().isEqualTo(2) assertThat(result).hasWholeNumberThat().isEqualTo(1) } + + @Test + fun testPlus_zeroAndZero_returnsZero() { + val lhsFraction = ZERO_FRACTION + val rhsFraction = ZERO_FRACTION + + val result = lhsFraction + rhsFraction + + assertThat(result).isEqualTo(ZERO_FRACTION) + } + + @Test + fun testPlus_oneAndZero_returnsOne() { + val lhsFraction = ZERO_FRACTION + val rhsFraction = ONE_FRACTION + + val result = lhsFraction + rhsFraction + + assertThat(result).isEqualTo(ONE_FRACTION) + } + + @Test + fun testPlus_oneHalfAndOneHalf_returnsOne() { + val lhsFraction = ONE_HALF_FRACTION + val rhsFraction = ONE_HALF_FRACTION + + val result = lhsFraction + rhsFraction + + assertThat(result).isEqualTo(ONE_FRACTION) + } + + @Test + fun testPlus_oneHalfAndNegativeOneHalf_returnsZero() { + val lhsFraction = ONE_HALF_FRACTION + val rhsFraction = NEGATIVE_ONE_HALF_FRACTION + + val result = lhsFraction + rhsFraction + + assertThat(result).isEqualTo(ZERO_FRACTION) + } + + @Test + fun testPlus_oneThirdAndOneHalf_returnsFiveSixths() { + val lhsFraction = ONE_THIRD_FRACTION + val rhsFraction = ONE_HALF_FRACTION + + val result = lhsFraction + rhsFraction + + assertThat(result).hasNegativePropertyThat().isFalse() + assertThat(result).hasNumeratorThat().isEqualTo(5) + assertThat(result).hasDenominatorThat().isEqualTo(6) + assertThat(result).hasWholeNumberThat().isEqualTo(0) + } + + @Test + fun testPlus_oneHalfAndOneThird_returnsFiveSixths() { + val lhsFraction = ONE_HALF_FRACTION + val rhsFraction = ONE_THIRD_FRACTION + + val result = lhsFraction + rhsFraction + + // Demonstrate commutativity, i.e.: a+b=b+a. + assertThat(result).hasNegativePropertyThat().isFalse() + assertThat(result).hasNumeratorThat().isEqualTo(5) + assertThat(result).hasDenominatorThat().isEqualTo(6) + assertThat(result).hasWholeNumberThat().isEqualTo(0) + } + + @Test + fun testPlus_twentyFiveThirtiethsAndFiveSevenths_returnsOneAndTwentyThreeFortyTwos() { + val lhsFraction = Fraction.newBuilder().apply { + numerator = 25 + denominator = 30 + }.build() + val rhsFraction = Fraction.newBuilder().apply { + numerator = 5 + denominator = 7 + }.build() + + val result = lhsFraction + rhsFraction + + assertThat(result).hasNegativePropertyThat().isFalse() + assertThat(result).hasNumeratorThat().isEqualTo(23) + assertThat(result).hasDenominatorThat().isEqualTo(42) + assertThat(result).hasWholeNumberThat().isEqualTo(1) + } + + @Test + fun testPlus_negativeOneAndOneThird_returnsNegativeTwoThirds() { + val lhsFraction = NEGATIVE_ONE_FRACTION + val rhsFraction = ONE_THIRD_FRACTION + + val result = lhsFraction + rhsFraction + + // Effectively subtracting fractions via addition should work as expected. + assertThat(result).hasNegativePropertyThat().isTrue() + assertThat(result).hasNumeratorThat().isEqualTo(2) + assertThat(result).hasDenominatorThat().isEqualTo(3) + assertThat(result).hasWholeNumberThat().isEqualTo(0) + } + + @Test + fun testPlus_oneAndNegativeOneThird_returnsTwoThirds() { + val lhsFraction = ONE_FRACTION + val rhsFraction = NEGATIVE_ONE_THIRD_FRACTION + + val result = lhsFraction + rhsFraction + + // Effectively subtracting fractions via addition should work as expected. + assertThat(result).hasNegativePropertyThat().isFalse() + assertThat(result).hasNumeratorThat().isEqualTo(2) + assertThat(result).hasDenominatorThat().isEqualTo(3) + assertThat(result).hasWholeNumberThat().isEqualTo(0) + } + + @Test + fun testPlus_negativeOneAndNegativeOneThird_returnsNegativeOneAndOneThird() { + val lhsFraction = NEGATIVE_ONE_FRACTION + val rhsFraction = NEGATIVE_ONE_THIRD_FRACTION + + val result = lhsFraction + rhsFraction + + // Negative addition should work as expected. + assertThat(result).hasNegativePropertyThat().isTrue() + assertThat(result).hasNumeratorThat().isEqualTo(1) + assertThat(result).hasDenominatorThat().isEqualTo(3) + assertThat(result).hasWholeNumberThat().isEqualTo(1) + } + + @Test + fun testMinus_zeroAndZero_returnsZero() { + val lhsFraction = ZERO_FRACTION + val rhsFraction = ZERO_FRACTION + + val result = lhsFraction - rhsFraction + + assertThat(result).isEqualTo(ZERO_FRACTION) + } + + @Test + fun testMinus_oneAndZero_returnsOne() { + val lhsFraction = ONE_FRACTION + val rhsFraction = ZERO_FRACTION + + val result = lhsFraction - rhsFraction + + assertThat(result).isEqualTo(ONE_FRACTION) + } + + @Test + fun testMinus_oneHalfAndOneHalf_returnsZero() { + val lhsFraction = ONE_HALF_FRACTION + val rhsFraction = ONE_HALF_FRACTION + + val result = lhsFraction - rhsFraction + + assertThat(result).isEqualTo(ZERO_FRACTION) + } + + @Test + fun testMinus_oneHalfAndNegativeOneHalf_returnsOne() { + val lhsFraction = ONE_HALF_FRACTION + val rhsFraction = NEGATIVE_ONE_HALF_FRACTION + + val result = lhsFraction - rhsFraction + + // Minus a negative fraction should turn into regular addition. + assertThat(result).isEqualTo(ONE_FRACTION) + } + + @Test + fun testMinus_oneThirdAndOneHalf_returnsNegativeOneSixth() { + val lhsFraction = ONE_THIRD_FRACTION + val rhsFraction = ONE_HALF_FRACTION + + val result = lhsFraction - rhsFraction + + assertThat(result).hasNegativePropertyThat().isTrue() + assertThat(result).hasNumeratorThat().isEqualTo(1) + assertThat(result).hasDenominatorThat().isEqualTo(6) + assertThat(result).hasWholeNumberThat().isEqualTo(0) + } + + @Test + fun testMinus_oneHalfAndOneThird_returnsOneSixth() { + val lhsFraction = ONE_HALF_FRACTION + val rhsFraction = ONE_THIRD_FRACTION + + val result = lhsFraction - rhsFraction + + // Demonstrate anticommutativity, i.e.: a-b=-(b-a). + assertThat(result).hasNegativePropertyThat().isFalse() + assertThat(result).hasNumeratorThat().isEqualTo(1) + assertThat(result).hasDenominatorThat().isEqualTo(6) + assertThat(result).hasWholeNumberThat().isEqualTo(0) + } + + @Test + fun testMinus_twentyFiveThirtiethsAndTwentyThreeSevenths_returnsNegTwoAndNineteenFortyTwos() { + val lhsFraction = Fraction.newBuilder().apply { + numerator = 25 + denominator = 30 + }.build() + val rhsFraction = Fraction.newBuilder().apply { + numerator = 23 + denominator = 7 + }.build() + + val result = lhsFraction - rhsFraction + + // Verify that the result of subtraction results in a properly formed fraction. + assertThat(result).hasNegativePropertyThat().isTrue() + assertThat(result).hasNumeratorThat().isEqualTo(19) + assertThat(result).hasDenominatorThat().isEqualTo(42) + assertThat(result).hasWholeNumberThat().isEqualTo(2) + } + + @Test + fun testMinus_negativeOneAndOneThird_returnsNegativeOneAndOneThird() { + val lhsFraction = NEGATIVE_ONE_FRACTION + val rhsFraction = ONE_THIRD_FRACTION + + val result = lhsFraction - rhsFraction + + assertThat(result).hasNegativePropertyThat().isTrue() + assertThat(result).hasNumeratorThat().isEqualTo(1) + assertThat(result).hasDenominatorThat().isEqualTo(3) + assertThat(result).hasWholeNumberThat().isEqualTo(1) + } + + @Test + fun testMinus_oneAndNegativeOneThird_returnsOneAndOneThird() { + val lhsFraction = ONE_FRACTION + val rhsFraction = NEGATIVE_ONE_THIRD_FRACTION + + val result = lhsFraction - rhsFraction + + assertThat(result).hasNegativePropertyThat().isFalse() + assertThat(result).hasNumeratorThat().isEqualTo(1) + assertThat(result).hasDenominatorThat().isEqualTo(3) + assertThat(result).hasWholeNumberThat().isEqualTo(1) + } + + @Test + fun testMinus_negativeOneAndNegativeOneThird_returnsNegativeTwoThirds() { + val lhsFraction = NEGATIVE_ONE_FRACTION + val rhsFraction = NEGATIVE_ONE_THIRD_FRACTION + + val result = lhsFraction - rhsFraction + + assertThat(result).hasNegativePropertyThat().isTrue() + assertThat(result).hasNumeratorThat().isEqualTo(2) + assertThat(result).hasDenominatorThat().isEqualTo(3) + assertThat(result).hasWholeNumberThat().isEqualTo(0) + } + + @Test + fun testTimes_zeroAndZero_returnsZero() { + val lhsFraction = ZERO_FRACTION + val rhsFraction = ZERO_FRACTION + + val result = lhsFraction * rhsFraction + + assertThat(result).isEqualTo(ZERO_FRACTION) + } + + @Test + fun testTimes_oneAndZero_returnsZero() { + val lhsFraction = ONE_FRACTION + val rhsFraction = ZERO_FRACTION + + val result = lhsFraction * rhsFraction + + assertThat(result).isEqualTo(ZERO_FRACTION) + } + + @Test + fun testTimes_oneAndOne_returnsOne() { + val lhsFraction = ONE_FRACTION + val rhsFraction = ONE_FRACTION + + val result = lhsFraction * rhsFraction + + assertThat(result).isEqualTo(ONE_FRACTION) + } + + @Test + fun testTimes_twoAndOne_returnsTwo() { + val lhsFraction = TWO_FRACTION + val rhsFraction = ONE_FRACTION + + val result = lhsFraction * rhsFraction + + assertThat(result).isEqualTo(TWO_FRACTION) + } + + @Test + fun testTimes_oneHalfAndOneThird_returnsOneSixth() { + val lhsFraction = ONE_HALF_FRACTION + val rhsFraction = ONE_THIRD_FRACTION + + val result = lhsFraction * rhsFraction + + assertThat(result).hasNegativePropertyThat().isFalse() + assertThat(result).hasNumeratorThat().isEqualTo(1) + assertThat(result).hasDenominatorThat().isEqualTo(6) + assertThat(result).hasWholeNumberThat().isEqualTo(0) + } + + @Test + fun testTimes_oneThirdAndOneHalf_returnsOneSixth() { + val lhsFraction = ONE_THIRD_FRACTION + val rhsFraction = ONE_HALF_FRACTION + + val result = lhsFraction * rhsFraction + + // Demonstrate commutativity, i.e.: a*b=b*a. + assertThat(result).hasNegativePropertyThat().isFalse() + assertThat(result).hasNumeratorThat().isEqualTo(1) + assertThat(result).hasDenominatorThat().isEqualTo(6) + assertThat(result).hasWholeNumberThat().isEqualTo(0) + } + + @Test + fun testTimes_sevenHalvesAndTwentyFifteenths_returnsFourAndTwoThirds() { + val lhsFraction = Fraction.newBuilder().apply { + numerator = 7 + denominator = 2 + }.build() + val rhsFraction = Fraction.newBuilder().apply { + numerator = 20 + denominator = 15 + }.build() + + val result = lhsFraction * rhsFraction + + // Demonstrate that the multiplied result is a fully properly form fraction. + assertThat(result).hasNegativePropertyThat().isFalse() + assertThat(result).hasNumeratorThat().isEqualTo(2) + assertThat(result).hasDenominatorThat().isEqualTo(3) + assertThat(result).hasWholeNumberThat().isEqualTo(4) + } + + @Test + fun testTimes_negativeTwoAndOneThird_returnsNegativeTwoThirds() { + val lhsFraction = NEGATIVE_TWO_FRACTION + val rhsFraction = ONE_THIRD_FRACTION + + val result = lhsFraction * rhsFraction + + assertThat(result).hasNegativePropertyThat().isTrue() + assertThat(result).hasNumeratorThat().isEqualTo(2) + assertThat(result).hasDenominatorThat().isEqualTo(3) + assertThat(result).hasWholeNumberThat().isEqualTo(0) + } + + @Test + fun testTimes_twoAndNegativeOneThird_returnsNegativeTwoThirds() { + val lhsFraction = TWO_FRACTION + val rhsFraction = NEGATIVE_ONE_THIRD_FRACTION + + val result = lhsFraction * rhsFraction + + assertThat(result).hasNegativePropertyThat().isTrue() + assertThat(result).hasNumeratorThat().isEqualTo(2) + assertThat(result).hasDenominatorThat().isEqualTo(3) + assertThat(result).hasWholeNumberThat().isEqualTo(0) + } + + @Test + fun testTimes_negativeTwoAndNegativeOneThird_returnsTwoThirds() { + val lhsFraction = NEGATIVE_TWO_FRACTION + val rhsFraction = NEGATIVE_ONE_THIRD_FRACTION + + val result = lhsFraction * rhsFraction + + // The negatives cancel out during multiplication. + assertThat(result).hasNegativePropertyThat().isFalse() + assertThat(result).hasNumeratorThat().isEqualTo(2) + assertThat(result).hasDenominatorThat().isEqualTo(3) + assertThat(result).hasWholeNumberThat().isEqualTo(0) + } + + @Test + fun testDivides_zeroAndZero_throwsException() { + val lhsFraction = ZERO_FRACTION + val rhsFraction = ZERO_FRACTION + + assertThrows(Exception::class) { lhsFraction / rhsFraction } + } + + @Test + fun testDivides_oneAndZero_throwsException() { + val lhsFraction = ONE_FRACTION + val rhsFraction = ZERO_FRACTION + + assertThrows(Exception::class) { lhsFraction / rhsFraction } + } + + @Test + fun testDivides_twoAndZero_throwsException() { + val lhsFraction = TWO_FRACTION + val rhsFraction = ZERO_FRACTION + + assertThrows(Exception::class) { lhsFraction / rhsFraction } + } + + @Test + fun testDivides_twoAndOne_returnsTwo() { + val lhsFraction = TWO_FRACTION + val rhsFraction = ONE_FRACTION + + val result = lhsFraction / rhsFraction + + assertThat(result).isEqualTo(TWO_FRACTION) + } + + @Test + fun testDivides_oneHalfAndOneThird_returnsOneAndOneHalf() { + val lhsFraction = ONE_HALF_FRACTION + val rhsFraction = ONE_THIRD_FRACTION + + val result = lhsFraction / rhsFraction + + // (1/2)/(1/3)=3/2=1 1/2. + assertThat(result).isEqualTo(ONE_AND_ONE_HALF_FRACTION) + } + + @Test + fun testDivides_oneThirdAndOneHalf_returnsTwoThirds() { + val lhsFraction = ONE_THIRD_FRACTION + val rhsFraction = ONE_HALF_FRACTION + + val result = lhsFraction / rhsFraction + + // Demonstrate anticommutativity, i.e.: a/b=1/(b/a). + assertThat(result).hasNegativePropertyThat().isFalse() + assertThat(result).hasNumeratorThat().isEqualTo(2) + assertThat(result).hasDenominatorThat().isEqualTo(3) + assertThat(result).hasWholeNumberThat().isEqualTo(0) + } + + @Test + fun testDivides_fourThirdsAndTenThirtyFifths_returnsFourAndTwoThirds() { + val lhsFraction = Fraction.newBuilder().apply { + numerator = 4 + denominator = 3 + }.build() + val rhsFraction = Fraction.newBuilder().apply { + numerator = 10 + denominator = 35 + }.build() + + val result = lhsFraction / rhsFraction + + // Demonstrate that the divided result is a fully properly form fraction. + assertThat(result).hasNegativePropertyThat().isFalse() + assertThat(result).hasNumeratorThat().isEqualTo(2) + assertThat(result).hasDenominatorThat().isEqualTo(3) + assertThat(result).hasWholeNumberThat().isEqualTo(4) + } + + @Test + fun testDivides_negativeTwoAndOneThird_returnsNegativeSix() { + val lhsFraction = NEGATIVE_TWO_FRACTION + val rhsFraction = ONE_THIRD_FRACTION + + val result = lhsFraction / rhsFraction + + assertThat(result).hasNegativePropertyThat().isTrue() + assertThat(result).hasNumeratorThat().isEqualTo(0) + assertThat(result).hasDenominatorThat().isEqualTo(1) + assertThat(result).hasWholeNumberThat().isEqualTo(6) + } + + @Test + fun testDivides_twoAndNegativeOneThird_returnsNegativeSix() { + val lhsFraction = TWO_FRACTION + val rhsFraction = NEGATIVE_ONE_THIRD_FRACTION + + val result = lhsFraction / rhsFraction + + assertThat(result).hasNegativePropertyThat().isTrue() + assertThat(result).hasNumeratorThat().isEqualTo(0) + assertThat(result).hasDenominatorThat().isEqualTo(1) + assertThat(result).hasWholeNumberThat().isEqualTo(6) + } + + @Test + fun testDivides_negativeTwoAndNegativeOneThird_returnsSix() { + val lhsFraction = NEGATIVE_TWO_FRACTION + val rhsFraction = NEGATIVE_ONE_THIRD_FRACTION + + val result = lhsFraction / rhsFraction + + // The negatives cancel out during division. + assertThat(result).hasNegativePropertyThat().isFalse() + assertThat(result).hasNumeratorThat().isEqualTo(0) + assertThat(result).hasDenominatorThat().isEqualTo(1) + assertThat(result).hasWholeNumberThat().isEqualTo(6) + } + + @Test + fun testPow_zeroToZero_returnsOne() { + val fraction = ZERO_FRACTION + + val result = fraction pow 0 + + // See pow's documentation for specifics. + assertThat(result).isEqualTo(ONE_FRACTION) + } + + @Test + fun testPow_oneToZero_returnsOne() { + val fraction = ONE_FRACTION + + val result = fraction pow 0 + + assertThat(result).isEqualTo(ONE_FRACTION) + } + + @Test + fun testPow_oneToOne_returnsOne() { + val fraction = ONE_FRACTION + + val result = fraction pow 1 + + assertThat(result).isEqualTo(ONE_FRACTION) + } + + @Test + fun testPow_twoToZero_returnsOne() { + val fraction = TWO_FRACTION + + val result = fraction pow 0 + + assertThat(result).isEqualTo(ONE_FRACTION) + } + + @Test + fun testPow_twoToOne_returnsTwo() { + val fraction = TWO_FRACTION + + val result = fraction pow 1 + + assertThat(result).isEqualTo(TWO_FRACTION) + } + + @Test + fun testPow_twoToTwo_returnsFour() { + val fraction = TWO_FRACTION + + val result = fraction pow 2 + + assertThat(result).hasNegativePropertyThat().isFalse() + assertThat(result).hasNumeratorThat().isEqualTo(0) + assertThat(result).hasDenominatorThat().isEqualTo(1) + assertThat(result).hasWholeNumberThat().isEqualTo(4) + } + + @Test + fun testPow_oneThirdToTwo_returnsOneNinth() { + val fraction = ONE_THIRD_FRACTION + + val result = fraction pow 2 + + assertThat(result).hasNegativePropertyThat().isFalse() + assertThat(result).hasNumeratorThat().isEqualTo(1) + assertThat(result).hasDenominatorThat().isEqualTo(9) + assertThat(result).hasWholeNumberThat().isEqualTo(0) + } + + @Test + fun testPow_negativeOneThirdToTwo_returnsOneNinth() { + val fraction = NEGATIVE_ONE_THIRD_FRACTION + + val result = fraction pow 2 + + // The negative sign is lost since the power is even. + assertThat(result).hasNegativePropertyThat().isFalse() + assertThat(result).hasNumeratorThat().isEqualTo(1) + assertThat(result).hasDenominatorThat().isEqualTo(9) + assertThat(result).hasWholeNumberThat().isEqualTo(0) + } + + @Test + fun testPow_negativeOneThirdToThree_returnsNegativeOneTwentySeventh() { + val fraction = NEGATIVE_ONE_THIRD_FRACTION + + val result = fraction pow 3 + + // The negative sign is preserved since the power is odd. + assertThat(result).hasNegativePropertyThat().isTrue() + assertThat(result).hasNumeratorThat().isEqualTo(1) + assertThat(result).hasDenominatorThat().isEqualTo(27) + assertThat(result).hasWholeNumberThat().isEqualTo(0) + } + + @Test + fun testPow_twoToNegativeTwo_returnsOneFourth() { + val fraction = TWO_FRACTION + + val result = fraction pow -2 + + assertThat(result).hasNegativePropertyThat().isFalse() + assertThat(result).hasNumeratorThat().isEqualTo(1) + assertThat(result).hasDenominatorThat().isEqualTo(4) + assertThat(result).hasWholeNumberThat().isEqualTo(0) + } + + @Test + fun testPow_oneThirdToNegativeThree_returnsTwentySeven() { + val fraction = ONE_THIRD_FRACTION + + val result = fraction pow -3 + + // The negative sign is preserved since the power is odd. + assertThat(result).hasNegativePropertyThat().isFalse() + assertThat(result).hasNumeratorThat().isEqualTo(0) + assertThat(result).hasDenominatorThat().isEqualTo(1) + assertThat(result).hasWholeNumberThat().isEqualTo(27) + } + + @Test + fun testPow_negativeOneThirdToNegativeTwo_returnsNine() { + val fraction = NEGATIVE_ONE_THIRD_FRACTION + + val result = fraction pow -2 + + // The negative sign is lost since the power is even. + assertThat(result).hasNegativePropertyThat().isFalse() + assertThat(result).hasNumeratorThat().isEqualTo(0) + assertThat(result).hasDenominatorThat().isEqualTo(1) + assertThat(result).hasWholeNumberThat().isEqualTo(9) + } + + @Test + fun testPow_negativeOneThirdToNegativeThree_returnsNegativeTwentySeven() { + val fraction = NEGATIVE_ONE_THIRD_FRACTION + + val result = fraction pow -3 + + // The negative sign is preserved since the power is odd. + assertThat(result).hasNegativePropertyThat().isTrue() + assertThat(result).hasNumeratorThat().isEqualTo(0) + assertThat(result).hasDenominatorThat().isEqualTo(1) + assertThat(result).hasWholeNumberThat().isEqualTo(27) + } + + @Test + fun testPow_fourSeventhsCubed_returnsSixtyFourThreeHundredFortyThirds() { + val fraction = Fraction.newBuilder().apply { + numerator = 4 + denominator = 7 + }.build() + + val result = fraction pow 3 + + // Verify that the numerator is also correctly multiplied. + assertThat(result).hasNegativePropertyThat().isFalse() + assertThat(result).hasNumeratorThat().isEqualTo(64) + assertThat(result).hasDenominatorThat().isEqualTo(343) + assertThat(result).hasWholeNumberThat().isEqualTo(0) + } + + @Test + fun testPow_twentyOneTwelfthsToNegativeThree_returnsFiveAndTwentyThreeSixtyFourths() { + val fraction = Fraction.newBuilder().apply { + numerator = 12 + denominator = 21 + }.build() + + val result = fraction pow -3 + + // Verify that the resulting value is in fully proper form. + assertThat(result).hasNegativePropertyThat().isFalse() + assertThat(result).hasNumeratorThat().isEqualTo(23) + assertThat(result).hasDenominatorThat().isEqualTo(64) + assertThat(result).hasWholeNumberThat().isEqualTo(5) + } + + @Test + fun testToWholeNumberFraction_zero_returnsZeroFraction() { + val wholeNumber = 0 + + val fraction = wholeNumber.toWholeNumberFraction() + + assertThat(fraction).isEqualTo(ZERO_FRACTION) + } + + @Test + fun testToWholeNumberFraction_one_returnsOneFraction() { + val wholeNumber = 1 + + val fraction = wholeNumber.toWholeNumberFraction() + + assertThat(fraction).isEqualTo(ONE_FRACTION) + } + + @Test + fun testToWholeNumberFraction_twentyThree_returnsTwentyThreeFraction() { + val wholeNumber = 23 + + val fraction = wholeNumber.toWholeNumberFraction() + + assertThat(fraction).hasNegativePropertyThat().isFalse() + assertThat(fraction).hasNumeratorThat().isEqualTo(0) + assertThat(fraction).hasDenominatorThat().isEqualTo(1) + assertThat(fraction).hasWholeNumberThat().isEqualTo(23) + } + + @Test + fun testToWholeNumberFraction_negativeZero_returnsZeroFraction() { + val wholeNumber = -0 + + val fraction = wholeNumber.toWholeNumberFraction() + + assertThat(fraction).isEqualTo(ZERO_FRACTION) + } + + @Test + fun testToWholeNumberFraction_negativeOne_returnsNegativeOneFraction() { + val wholeNumber = -1 + + val fraction = wholeNumber.toWholeNumberFraction() + + assertThat(fraction).isEqualTo(NEGATIVE_ONE_FRACTION) + } + + @Test + fun testToWholeNumberFraction_negativeTwentyThree_returnsNegativeTwentyThreeFraction() { + val wholeNumber = -23 + + val fraction = wholeNumber.toWholeNumberFraction() + + assertThat(fraction).hasNegativePropertyThat().isTrue() + assertThat(fraction).hasNumeratorThat().isEqualTo(0) + assertThat(fraction).hasDenominatorThat().isEqualTo(1) + assertThat(fraction).hasWholeNumberThat().isEqualTo(23) + } } diff --git a/utility/src/test/java/org/oppia/android/util/math/MathExpressionExtensionsTest.kt b/utility/src/test/java/org/oppia/android/util/math/MathExpressionExtensionsTest.kt new file mode 100644 index 00000000000..0a81df14c54 --- /dev/null +++ b/utility/src/test/java/org/oppia/android/util/math/MathExpressionExtensionsTest.kt @@ -0,0 +1,97 @@ +package org.oppia.android.util.math + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.oppia.android.app.model.MathEquation +import org.oppia.android.app.model.MathExpression +import org.oppia.android.testing.math.RealSubject.Companion.assertThat +import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode.ALL_ERRORS +import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult +import org.oppia.android.util.math.MathExpressionParser.Companion.parseAlgebraicExpression +import org.oppia.android.util.math.MathExpressionParser.Companion.parseNumericExpression +import org.robolectric.annotation.LooperMode + +/** + * Tests for [MathExpression] and [MathEquation] extensions. + * + * Note that this suite only verifies that the extensions work at a high-level. More specific + * verifications for operations like LaTeX conversion and expression evaluation are part of more + * targeted test suites such as [ExpressionToLatexConverterTest] and + * [NumericExpressionEvaluatorTest]. + */ +// FunctionName: test names are conventionally named with underscores. +// SameParameterValue: tests should have specific context included/excluded for readability. +@Suppress("FunctionName", "SameParameterValue") +@RunWith(AndroidJUnit4::class) +@LooperMode(LooperMode.Mode.PAUSED) +class MathExpressionExtensionsTest { + @Test + fun testToRawLatex_algebraicExpression_divNotAsFraction_returnsLatexStringWithDivision() { + val expression = parseAlgebraicExpression("(x^2+7x-y)/2") + + val latex = expression.toRawLatex(divAsFraction = false) + + assertThat(latex).isEqualTo("(x ^ {2} + 7x - y) \\div 2") + } + + @Test + fun testToRawLatex_algebraicExpression_divAsFraction_returnsLatexStringWithFraction() { + val expression = parseAlgebraicExpression("(x^2+7x-y)/2") + + val latex = expression.toRawLatex(divAsFraction = true) + + assertThat(latex).isEqualTo("\\frac{(x ^ {2} + 7x - y)}{2}") + } + + @Test + fun testToRawLatex_algebraicEquation_divNotAsFraction_returnsLatexStringWithDivisions() { + val equation = parseAlgebraicEquation("y/2=(x^2+x-7)/(2x)") + + val latex = equation.toRawLatex(divAsFraction = false) + + assertThat(latex).isEqualTo("y \\div 2 = (x ^ {2} + x - 7) \\div (2x)") + } + + @Test + fun testToRawLatex_algebraicEquation_divAsFraction_returnsLatexStringWithFractions() { + val equation = parseAlgebraicEquation("y/2=(x^2+x-7)/(2x)") + + val latex = equation.toRawLatex(divAsFraction = true) + + assertThat(latex).isEqualTo("\\frac{y}{2} = \\frac{(x ^ {2} + x - 7)}{(2x)}") + } + + @Test + fun testEvaluateAsNumericExpression_numericExpression_returnsCorrectValue() { + val expression = parseNumericExpression("7*(3.14/0.76+8.4)^(3.8+1/(2+2/(7.4+1)))") + + val result = expression.evaluateAsNumericExpression() + + assertThat(result).isIrrationalThat().isWithin(1e-5).of(322194.700361352) + } + + private companion object { + private fun parseNumericExpression(expression: String): MathExpression { + return parseNumericExpression(expression, ALL_ERRORS).retrieveExpectedSuccessfulResult() + } + + private fun parseAlgebraicExpression(expression: String): MathExpression { + return parseAlgebraicExpression( + expression, allowedVariables = listOf("x", "y", "z"), ALL_ERRORS + ).retrieveExpectedSuccessfulResult() + } + + private fun parseAlgebraicEquation(expression: String): MathEquation { + return MathExpressionParser.parseAlgebraicEquation( + expression, allowedVariables = listOf("x", "y", "z"), ALL_ERRORS + ).retrieveExpectedSuccessfulResult() + } + + private fun MathParsingResult.retrieveExpectedSuccessfulResult(): T { + assertThat(this).isInstanceOf(MathParsingResult.Success::class.java) + return (this as MathParsingResult.Success).result + } + } +} diff --git a/utility/src/test/java/org/oppia/android/util/math/NumericExpressionEvaluatorTest.kt b/utility/src/test/java/org/oppia/android/util/math/NumericExpressionEvaluatorTest.kt new file mode 100644 index 00000000000..a16afda5a0e --- /dev/null +++ b/utility/src/test/java/org/oppia/android/util/math/NumericExpressionEvaluatorTest.kt @@ -0,0 +1,239 @@ +package org.oppia.android.util.math + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.oppia.android.app.model.MathExpression +import org.oppia.android.testing.math.RealSubject.Companion.assertThat +import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode +import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode.ALL_ERRORS +import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode.REQUIRED_ONLY +import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult +import org.oppia.android.util.math.MathExpressionParser.Companion.parseAlgebraicExpression +import org.oppia.android.util.math.NumericExpressionEvaluator.Companion.evaluate +import org.robolectric.annotation.LooperMode + +/** + * Tests for [NumericExpressionEvaluator]. + * + * This test suite is primarily focused on verifying high-level behaviors of the evaluator. More + * specific tests exist for the sub-implementation pieces of the evaluator in [RealExtensionsTest] + * and [FractionExtensionsTest], and more complicated expression evaluation can be seen in + * [NumericExpressionParserTest]. + */ +// FunctionName: test names are conventionally named with underscores. +// SameParameterValue: tests should have specific context included/excluded for readability. +@Suppress("FunctionName", "SameParameterValue") +@RunWith(AndroidJUnit4::class) +@LooperMode(LooperMode.Mode.PAUSED) +class NumericExpressionEvaluatorTest { + @Test + fun testEvaluate_defaultExpression_returnsNull() { + val expression = MathExpression.getDefaultInstance() + + val result = expression.evaluate() + + // Default expressions have nothing to evaluate. + assertThat(result).isNull() + } + + @Test + fun testEvaluate_constantExpression_returnsConstant() { + val expression = parseNumericExpression("2") + + val result = expression.evaluate() + + assertThat(result).isIntegerThat().isEqualTo(2) + } + + @Test + fun testEvaluate_variableExpression_returnsNull() { + val expression = parseAlgebraicExpression("2x") + + val result = expression.evaluate() + + // Cannot evaluate variables. + assertThat(result).isNull() + } + + @Test + fun testEvaluate_onePlusTwo_returnsThree() { + val expression = parseNumericExpression("1+2") + + val result = expression.evaluate() + + assertThat(result).isIntegerThat().isEqualTo(3) + } + + @Test + fun testEvaluate_oneMinusTwo_returnsNegativeOne() { + val expression = parseNumericExpression("1-2") + + val result = expression.evaluate() + + assertThat(result).isIntegerThat().isEqualTo(-1) + } + + @Test + fun testEvaluate_twoTimesSeven_returnsFourteen() { + val expression = parseNumericExpression("2*7") + + val result = expression.evaluate() + + assertThat(result).isIntegerThat().isEqualTo(14) + } + + @Test + fun testEvaluate_fourDividedByTwo_returnsTwo() { + val expression = parseNumericExpression("4/2") + + val result = expression.evaluate() + + assertThat(result).isIntegerThat().isEqualTo(2) + } + + @Test + fun testEvaluate_oneDividedByTwo_returnsOneHalfFraction() { + val expression = parseNumericExpression("1/2") + + val result = expression.evaluate() + + assertThat(result).isRationalThat().apply { + hasNegativePropertyThat().isFalse() + hasWholeNumberThat().isEqualTo(0) + hasNumeratorThat().isEqualTo(1) + hasDenominatorThat().isEqualTo(2) + } + } + + @Test + fun testEvaluate_minusOne_returnsMinusOne() { + val expression = parseNumericExpression("-2") + + val result = expression.evaluate() + + assertThat(result).isIntegerThat().isEqualTo(-2) + } + + @Test + fun testEvaluate_plusTwo_returnsTwo() { + val expression = parseNumericExpression("+2", errorCheckingMode = REQUIRED_ONLY) + + val result = expression.evaluate() + + assertThat(result).isIntegerThat().isEqualTo(2) + } + + @Test + fun testEvaluate_minusGroupOneMinusTwo_returnsOne() { + val expression = parseNumericExpression("-(1-2)") + + val result = expression.evaluate() + + assertThat(result).isIntegerThat().isEqualTo(1) + } + + @Test + fun testEvaluate_plusGroupOneMinusTwo_returnsMinusOne() { + val expression = parseNumericExpression("+(1-2)", errorCheckingMode = REQUIRED_ONLY) + + val result = expression.evaluate() + + assertThat(result).isIntegerThat().isEqualTo(-1) + } + + @Test + fun testEvaluate_twoTimesNegativeSeven_returnsNegativeFourteen() { + val expression = parseNumericExpression("2*-7") + + val result = expression.evaluate() + + assertThat(result).isIntegerThat().isEqualTo(-14) + } + + @Test + fun testEvaluate_oneDividedByGroupOfOnePlusTwo_returnsOneThirdFraction() { + val expression = parseNumericExpression("1/(1+2)") + + val result = expression.evaluate() + + assertThat(result).isRationalThat().apply { + hasNegativePropertyThat().isFalse() + hasWholeNumberThat().isEqualTo(0) + hasNumeratorThat().isEqualTo(1) + hasDenominatorThat().isEqualTo(3) + } + } + + @Test + fun testEvaluate_twoRaisedToThree_returnsEight() { + val expression = parseNumericExpression("2^3") + + val result = expression.evaluate() + + assertThat(result).isIntegerThat().isEqualTo(8) + } + + @Test + fun testEvaluate_groupOneDividedByTwoRaisedToNegativeThree_returnsEightFraction() { + val expression = parseNumericExpression("1/(2^-3)") + + val result = expression.evaluate() + + assertThat(result).isRationalThat().apply { + hasNegativePropertyThat().isFalse() + hasWholeNumberThat().isEqualTo(8) + hasNumeratorThat().isEqualTo(0) + hasDenominatorThat().isEqualTo(1) + } + } + + @Test + fun testEvaluate_rootOfTwo_returnsSquareRootOfTwoDecimal() { + val expression = parseNumericExpression("√2") + + val result = expression.evaluate() + + assertThat(result).isIrrationalThat().isWithin(1e-5).of(1.414213562) + } + + @Test + fun testEvaluate_rootOfGroupTwoRaisedToTwo_returnsTwoInteger() { + val expression = parseNumericExpression("√(2^2)") + + val result = expression.evaluate() + + assertThat(result).isIntegerThat().isEqualTo(2) + } + + @Test + fun testEvaluate_threeRaisedToOneDividedByTwo_returnsSquareRootOfThreeDecimal() { + val expression = parseNumericExpression("3^(1/2)") + + val result = expression.evaluate() + + assertThat(result).isIrrationalThat().isWithin(1e-5).of(1.732050808) + } + + private companion object { + private fun parseNumericExpression( + expression: String, errorCheckingMode: ErrorCheckingMode = ALL_ERRORS + ): MathExpression { + return MathExpressionParser.parseNumericExpression( + expression, errorCheckingMode + ).retrieveExpectedSuccessfulResult() + } + + private fun parseAlgebraicExpression(expression: String): MathExpression { + return parseAlgebraicExpression( + expression, allowedVariables = listOf("x", "y", "z"), ALL_ERRORS + ).retrieveExpectedSuccessfulResult() + } + + private fun MathParsingResult.retrieveExpectedSuccessfulResult(): T { + assertThat(this).isInstanceOf(MathParsingResult.Success::class.java) + return (this as MathParsingResult.Success).result + } + } +} diff --git a/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt index afea2ad7908..bd236e905dc 100644 --- a/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt @@ -10,7 +10,6 @@ import org.oppia.android.testing.math.MathExpressionSubject.Companion.assertThat import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult import org.robolectric.annotation.LooperMode -import kotlin.math.sqrt /** * Tests for [MathExpressionParser]. @@ -20,6 +19,11 @@ import kotlin.math.sqrt * both operator associativity and precedence). This suite does not cover errors (see * [MathExpressionParserTest] for those tests), nor algebraic expressions (see * [AlgebraicExpressionParserTest]). + * + * Further, many of the tests also verify that the expression evaluates to the correct value. This + * suite's goal is not to test that the evaluator works functionally but, rather, that it works + * practically. There are targeted tests designed to fail for the evaluator if issues are + * introduced (see [NumericExpressionEvaluatorTest]). */ // FunctionName: test names are conventionally named with underscores. @Suppress("FunctionName") @@ -36,6 +40,7 @@ class NumericExpressionParserTest { withValueThat().isIntegerThat().isEqualTo(1) } } + assertThat(expression).evaluatesToIntegerThat().isEqualTo(1) } @Test @@ -47,6 +52,7 @@ class NumericExpressionParserTest { withValueThat().isIntegerThat().isEqualTo(2) } } + assertThat(expression).evaluatesToIntegerThat().isEqualTo(2) } @Test @@ -58,6 +64,7 @@ class NumericExpressionParserTest { withValueThat().isIntegerThat().isEqualTo(732) } } + assertThat(expression).evaluatesToIntegerThat().isEqualTo(732) } @Test @@ -69,6 +76,7 @@ class NumericExpressionParserTest { withValueThat().isIrrationalThat().isWithin(1e-5).of(2.5) } } + assertThat(expression).evaluatesToIrrationalThat().isWithin(1e-5).of(2.5) } @Test @@ -89,6 +97,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression).evaluatesToIntegerThat().isEqualTo(3) } @Test @@ -109,6 +118,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression).evaluatesToIntegerThat().isEqualTo(-1) } @Test @@ -129,6 +139,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression).evaluatesToIntegerThat().isEqualTo(-1) } @Test @@ -149,6 +160,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression).evaluatesToIntegerThat().isEqualTo(2) } @Test @@ -169,6 +181,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression).evaluatesToIntegerThat().isEqualTo(2) } @Test @@ -189,6 +202,12 @@ class NumericExpressionParserTest { } } } + assertThat(expression).evaluatesToRationalThat().apply { + hasNegativePropertyThat().isFalse() + hasWholeNumberThat().isEqualTo(0) + hasNumeratorThat().isEqualTo(1) + hasDenominatorThat().isEqualTo(2) + } } @Test @@ -209,6 +228,12 @@ class NumericExpressionParserTest { } } } + assertThat(expression).evaluatesToRationalThat().apply { + hasNegativePropertyThat().isFalse() + hasWholeNumberThat().isEqualTo(0) + hasNumeratorThat().isEqualTo(1) + hasDenominatorThat().isEqualTo(2) + } } @Test @@ -229,6 +254,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression).evaluatesToIntegerThat().isEqualTo(1) } @Test @@ -244,6 +270,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression).evaluatesToIntegerThat().isEqualTo(-2) } @Test @@ -259,6 +286,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression).evaluatesToIntegerThat().isEqualTo(2) } @Test @@ -272,6 +300,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression).evaluatesToIntegerThat().isEqualTo(2) } @Test @@ -287,6 +316,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression).evaluatesToIrrationalThat().isWithin(1e-5).of(1.414213562) } @Test @@ -302,6 +332,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression).evaluatesToIrrationalThat().isWithin(1e-5).of(1.414213562) } @Test @@ -332,6 +363,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression).evaluatesToIntegerThat().isEqualTo(7) } @Test @@ -362,6 +394,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression).evaluatesToIntegerThat().isEqualTo(162) } @Test @@ -394,6 +427,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression).evaluatesToIntegerThat().isEqualTo(1296) } @Test @@ -419,6 +453,9 @@ class NumericExpressionParserTest { } } } + // Note that this may differ from other calculators since the negation is applied last (others + // may interpret it as (-3)^4). + assertThat(expression).evaluatesToIntegerThat().isEqualTo(-81) } @Test @@ -444,6 +481,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression).evaluatesToIrrationalThat().isWithin(1e-5).of(9.0) } @Test @@ -494,6 +532,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression).evaluatesToIntegerThat().isEqualTo(-1) } @Test @@ -544,14 +583,20 @@ class NumericExpressionParserTest { } } } + assertThat(expression).evaluatesToRationalThat().apply { + hasNegativePropertyThat().isFalse() + hasWholeNumberThat().isEqualTo(1) + hasNumeratorThat().isEqualTo(1) + hasDenominatorThat().isEqualTo(4) + } } @Test fun testParse_nestedExponents_returnsExpWithExponentsAsRightAssociative() { - val expression = parseNumericExpressionWithoutOptionalErrors("2^3^4") + val expression = parseNumericExpressionWithoutOptionalErrors("2^3^1.5") // Exponentiation is resolved with right associativity, that is, from right to left. This is - // made clearer by grouping: 2^(3^4). Note that this is a specific choice made by the + // made clearer by grouping: 2^(3^1.5). Note that this is a specific choice made by the // implementation as there's no broad consensus around exponentiation associativity for infix // exponentiation. Right associativity is ideal since it more closely matches written-out // exponentiation (where the nested exponent is resolved first). @@ -571,18 +616,19 @@ class NumericExpressionParserTest { } rightOperand { constant { - withValueThat().isIntegerThat().isEqualTo(4) + withValueThat().isIrrationalThat().isWithin(1e-5).of(1.5) } } } } } } + assertThat(expression).evaluatesToIrrationalThat().isWithin(1e-5).of(36.660445757) } @Test fun testParse_nestedExponents_withGroups_returnsExpWithForcedLeftAssociativeExponent() { - val expression = parseNumericExpressionWithAllErrors("(2^3)^4") + val expression = parseNumericExpressionWithAllErrors("(2^3)^1.5") // Nested exponentiation can be "forced" to be left-associative by using a group to explicitly // change the order (since groups have higher precedence than exponents). @@ -606,11 +652,12 @@ class NumericExpressionParserTest { } rightOperand { constant { - withValueThat().isIntegerThat().isEqualTo(4) + withValueThat().isIrrationalThat().isWithin(1e-5).of(1.5) } } } } + assertThat(expression).evaluatesToIrrationalThat().isWithin(1e-5).of(22.627416998) } @Test @@ -651,6 +698,7 @@ class NumericExpressionParserTest { } } } + // Cannot evaluate this expression in real numbers. } @Test @@ -688,6 +736,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression).evaluatesToIntegerThat().isEqualTo(-3) } @Test @@ -708,6 +757,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression).evaluatesToIntegerThat().isEqualTo(6) } @Test @@ -732,6 +782,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression).evaluatesToIntegerThat().isEqualTo(6) } @Test @@ -764,6 +815,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression).evaluatesToIntegerThat().isEqualTo(6) } @Test @@ -807,6 +859,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression).evaluatesToIntegerThat().isEqualTo(21) } @Test @@ -832,6 +885,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression).evaluatesToIrrationalThat().isWithin(1e-5).of(3.464101615) } @Test @@ -861,6 +915,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression).evaluatesToIrrationalThat().isWithin(1e-5).of(2.449489743) } @Test @@ -886,6 +941,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression).evaluatesToIrrationalThat().isWithin(1e-5).of(2.828427125) } @Test @@ -915,6 +971,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression).evaluatesToIrrationalThat().isWithin(1e-5).of(2.0) } @Test @@ -943,6 +1000,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression).evaluatesToIrrationalThat().isWithin(1e-5).of(4.242640687) } @Test @@ -985,6 +1043,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression).evaluatesToIrrationalThat().isWithin(1e-5).of(4.898979486) } @Test @@ -1019,6 +1078,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression).evaluatesToIrrationalThat().isWithin(1e-5).of(5.242640687) } @Test @@ -1078,6 +1138,12 @@ class NumericExpressionParserTest { } } } + assertThat(expression).evaluatesToRationalThat().apply { + hasNegativePropertyThat().isFalse() + hasWholeNumberThat().isEqualTo(73) + hasNumeratorThat().isEqualTo(1) + hasDenominatorThat().isEqualTo(2) + } } @Test @@ -1114,6 +1180,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression).evaluatesToIntegerThat().isEqualTo(32) } @Test @@ -1155,6 +1222,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression).evaluatesToIntegerThat().isEqualTo(8192) } @Test @@ -1256,6 +1324,12 @@ class NumericExpressionParserTest { } } } + assertThat(expression).evaluatesToRationalThat().apply { + hasNegativePropertyThat().isFalse() + hasWholeNumberThat().isEqualTo(351) + hasNumeratorThat().isEqualTo(1) + hasDenominatorThat().isEqualTo(3) + } } @Test @@ -1345,6 +1419,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression).evaluatesToIrrationalThat().isWithin(1e-5).of(956.092045778) } @Test @@ -1376,6 +1451,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression).evaluatesToIntegerThat().isEqualTo(6) } @Test @@ -1437,6 +1513,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression).evaluatesToIntegerThat().isEqualTo(13) } @Test @@ -1556,6 +1633,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression).evaluatesToIrrationalThat().isWithin(1e-5).of(322194.700361352) } @Test diff --git a/utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt b/utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt index 2e13da959aa..2c0b7581c9e 100644 --- a/utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt @@ -1,29 +1,43 @@ package org.oppia.android.util.math -import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat import org.junit.Test import org.junit.runner.RunWith import org.oppia.android.app.model.Fraction import org.oppia.android.app.model.Real import org.oppia.android.testing.assertThrows +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Iteration +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Parameter +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.RunParameterized import org.oppia.android.testing.math.RealSubject.Companion.assertThat +import org.oppia.android.util.math.FractionParser.Companion.parseFraction import org.robolectric.annotation.LooperMode /** Tests for [Real] extensions. */ // FunctionName: test names are conventionally named with underscores. @Suppress("FunctionName") -@RunWith(AndroidJUnit4::class) +@RunWith(OppiaParameterizedTestRunner::class) @LooperMode(LooperMode.Mode.PAUSED) class RealExtensionsTest { private companion object { private const val PI = 3.1415 + private val ZERO_FRACTION = Fraction.newBuilder().apply { + numerator = 0 + denominator = 1 + }.build() + private val ONE_HALF_FRACTION = Fraction.newBuilder().apply { numerator = 1 denominator = 2 }.build() + private val ONE_FOURTH_FRACTION = Fraction.newBuilder().apply { + numerator = 1 + denominator = 4 + }.build() + private val ONE_AND_ONE_HALF_FRACTION = Fraction.newBuilder().apply { numerator = 1 denominator = 2 @@ -43,6 +57,16 @@ class RealExtensionsTest { private val NEGATIVE_PI_REAL = createIrrationalReal(-PI) } + @Parameter var lhsInt: Int = Int.MIN_VALUE + @Parameter lateinit var lhsFrac: String + @Parameter var lhsDouble: Double = Double.MIN_VALUE + @Parameter var rhsInt: Int = Int.MIN_VALUE + @Parameter lateinit var rhsFrac: String + @Parameter var rhsDouble: Double = Double.MIN_VALUE + @Parameter var expInt: Int = Int.MIN_VALUE + @Parameter lateinit var expFrac: String + @Parameter var expDouble: Double = Double.MIN_VALUE + @Test fun testIsRational_default_returnsFalse() { val defaultReal = Real.getDefaultInstance() @@ -73,6 +97,36 @@ class RealExtensionsTest { assertThat(result).isFalse() } + @Test + fun testIsInteger_default_returnsFalse() { + val defaultReal = Real.getDefaultInstance() + + val result = defaultReal.isInteger() + + assertThat(result).isFalse() + } + + @Test + fun testIsInteger_twoInteger_returnsTrue() { + val result = TWO_REAL.isInteger() + + assertThat(result).isTrue() + } + + @Test + fun testIsInteger_oneHalfFraction_returnsFalse() { + val result = ONE_HALF_REAL.isInteger() + + assertThat(result).isFalse() + } + + @Test + fun testIsInteger_piIrrational_returnsFalse() { + val result = PI_REAL.isInteger() + + assertThat(result).isFalse() + } + @Test fun testIsNegative_default_throwsException() { val defaultReal = Real.getDefaultInstance() @@ -399,12 +453,1334 @@ class RealExtensionsTest { assertThat(result).isIrrationalThat().isWithin(1e-5).of(PI) } + + /* + * Begin operator tests. + * + * Note that parameterized tests are used here to reduce the length of the overall test despite it + * being not best practice (since each parameterized test is actually verifying multiple + * behaviors). + * + * For a reference on the iteration names: + * - 'identity' refers to an operator identity (i.e. a value which doesn't result in a change to + * the other operand of the operation) + * - commutativity refers to verifying commutativity, e.g.: a+b=b+a or a*b=b*a + * - noncommutativity refers to verifying that commutativity doesn't hold, e.g.: 2^3 != 3^2 + * - anticommutativity refers to verifying that commutativity is operationally reversed, e.g.: + * a-b=-(b-a) and a/b=1/(b/a). + */ + + // Addition tests. + + @Test + @RunParameterized( + Iteration("identity+identity", "lhsInt=0", "rhsInt=0", "expInt=0"), + Iteration("int+identity", "lhsInt=1", "rhsInt=0", "expInt=1"), + Iteration("int+int", "lhsInt=1", "rhsInt=2", "expInt=3"), + Iteration("commutativity", "lhsInt=2", "rhsInt=1", "expInt=3"), + Iteration("int+-int", "lhsInt=1", "rhsInt=-2", "expInt=-1"), + Iteration("-int+int", "lhsInt=-1", "rhsInt=2", "expInt=1"), + Iteration("-int+-int", "lhsInt=-1", "rhsInt=-2", "expInt=-3") + ) + fun testPlus_intAndInt_returnsInt() { + val lhsReal = createIntegerReal(lhsInt) + val rhsReal = createIntegerReal(rhsInt) + + val result = lhsReal + rhsReal + + assertThat(result).isIntegerThat().isEqualTo(expInt) + } + + @Test + @RunParameterized( + Iteration("identity+identity", "lhsInt=0", "rhsFrac=0/1", "expFrac=0/1"), + Iteration("int+identity", "lhsInt=1", "rhsFrac=0/1", "expFrac=1"), + Iteration("int+fraction", "lhsInt=2", "rhsFrac=1/3", "expFrac=2 1/3"), + Iteration("int+wholeNumberFraction", "lhsInt=2", "rhsFrac=3/1", "expFrac=5"), + Iteration("commutativity", "lhsInt=3", "rhsFrac=2/1", "expFrac=5"), + Iteration("int+-fraction", "lhsInt=2", "rhsFrac=-1/3", "expFrac=1 2/3"), + Iteration("-int+fraction", "lhsInt=-2", "rhsFrac=1/3", "expFrac=-1 2/3"), + Iteration("-int+-fraction", "lhsInt=-2", "rhsFrac=-1/3", "expFrac=-2 1/3") + ) + fun testPlus_intAndFraction_returnsFraction() { + val lhsReal = createIntegerReal(lhsInt) + val rhsReal = createRationalReal(rhsFrac) + + val result = lhsReal + rhsReal + + val expectedFraction = parseFraction(expFrac) + assertThat(result).isRationalThat().isEqualTo(expectedFraction) + } + + @Test + @RunParameterized( + Iteration("identity+identity", "lhsInt=0", "rhsDouble=0.0", "expDouble=0.0"), + Iteration("int+identity", "lhsInt=1", "rhsDouble=0.0", "expDouble=1.0"), + Iteration("int+double", "lhsInt=1", "rhsDouble=3.14", "expDouble=4.14"), + Iteration("int+wholeNumberDouble", "lhsInt=1", "rhsDouble=3.0", "expDouble=4.0"), + Iteration("commutativity", "lhsInt=3", "rhsDouble=1.0", "expDouble=4.0"), + Iteration("int+-double", "lhsInt=1", "rhsDouble=-3.14", "expDouble=-2.14"), + Iteration("-int+double", "lhsInt=-1", "rhsDouble=3.14", "expDouble=2.14"), + Iteration("-int+-double", "lhsInt=-1", "rhsDouble=-3.14", "expDouble=-4.14") + ) + fun testPlus_intAndDouble_returnsDouble() { + val lhsReal = createIntegerReal(lhsInt) + val rhsReal = createIrrationalReal(rhsDouble) + + val result = lhsReal + rhsReal + + assertThat(result).isIrrationalThat().isWithin(1e-5).of(expDouble) + } + + @Test + @RunParameterized( + Iteration("identity+identity", "lhsFrac=0/1", "rhsInt=0", "expFrac=0/1"), + Iteration("fraction+identity", "lhsFrac=1/1", "rhsInt=0", "expFrac=1"), + Iteration("fraction+int", "lhsFrac=1/3", "rhsInt=2", "expFrac=2 1/3"), + Iteration("wholeNumberFraction+int", "lhsFrac=3/1", "rhsInt=2", "expFrac=5"), + Iteration("commutativity", "lhsFrac=2/1", "rhsInt=3", "expFrac=5"), + Iteration("fraction+-int", "lhsFrac=1/3", "rhsInt=-2", "expFrac=-1 2/3"), + Iteration("-fraction+int", "lhsFrac=-1/3", "rhsInt=2", "expFrac=1 2/3"), + Iteration("-fraction+-int", "lhsFrac=-1/3", "rhsInt=-2", "expFrac=-2 1/3") + ) + fun testPlus_fractionAndInt_returnsFraction() { + val lhsReal = createRationalReal(lhsFrac) + val rhsReal = createIntegerReal(rhsInt) + + val result = lhsReal + rhsReal + + val expectedFraction = parseFraction(expFrac) + assertThat(result).isRationalThat().isEqualTo(expectedFraction) + } + + @Test + @RunParameterized( + Iteration("identity+identity", "lhsFrac=0/1", "rhsFrac=0/1", "expFrac=0/1"), + Iteration("fraction+identity", "lhsFrac=3/2", "rhsFrac=0/1", "expFrac=1 1/2"), + Iteration("fraction+fraction", "lhsFrac=3/2", "rhsFrac=1/3", "expFrac=1 5/6"), + Iteration("commutativity", "lhsFrac=1/3", "rhsFrac=3/2", "expFrac=1 5/6"), + Iteration("fraction+-fraction", "lhsFrac=1/2", "rhsFrac=-1/3", "expFrac=1/6"), + Iteration("-fraction+fraction", "lhsFrac=-1/2", "rhsFrac=1/3", "expFrac=-1/6"), + Iteration("-fraction+-fraction", "lhsFrac=-1/2", "rhsFrac=-1/3", "expFrac=-5/6") + ) + fun testPlus_fractionAndFraction_returnsFraction() { + val lhsReal = createRationalReal(lhsFrac) + val rhsReal = createRationalReal(rhsFrac) + + val result = lhsReal + rhsReal + + val expectedFraction = parseFraction(expFrac) + assertThat(result).isRationalThat().isEqualTo(expectedFraction) + } + + @Test + @RunParameterized( + Iteration("identity+identity", "lhsFrac=0/1", "rhsDouble=0.0", "expDouble=0.0"), + Iteration("fraction+identity", "lhsFrac=3/2", "rhsDouble=0.0", "expDouble=1.5"), + Iteration("fraction+double", "lhsFrac=3/2", "rhsDouble=3.14", "expDouble=4.64"), + Iteration("wholeNumberFraction+double", "lhsFrac=3/1", "rhsDouble=2.0", "expDouble=5.0"), + Iteration("commutativity", "lhsFrac=2/1", "rhsDouble=3.0", "expDouble=5.0"), + Iteration("fraction+-double", "lhsFrac=3/2", "rhsDouble=-3.14", "expDouble=-1.64"), + Iteration("-fraction+double", "lhsFrac=-3/2", "rhsDouble=3.14", "expDouble=1.64"), + Iteration("-fraction+-double", "lhsFrac=-3/2", "rhsDouble=-3.14", "expDouble=-4.64") + ) + fun testPlus_fractionAndDouble_returnsDouble() { + val lhsReal = createRationalReal(lhsFrac) + val rhsReal = createIrrationalReal(rhsDouble) + + val result = lhsReal + rhsReal + + assertThat(result).isIrrationalThat().isWithin(1e-5).of(expDouble) + } + + @Test + @RunParameterized( + Iteration("identity+identity", "lhsDouble=0.0", "rhsInt=0", "expDouble=0.0"), + Iteration("double+identity", "lhsDouble=1.0", "rhsInt=0", "expDouble=1.0"), + Iteration("double+int", "lhsDouble=3.14", "rhsInt=1", "expDouble=4.14"), + Iteration("wholeNumberDouble+int", "lhsDouble=3.0", "rhsInt=1", "expDouble=4.0"), + Iteration("commutativity", "lhsDouble=1.0", "rhsInt=3", "expDouble=4.0"), + Iteration("double+-int", "lhsDouble=3.14", "rhsInt=-1", "expDouble=2.14"), + Iteration("-double+int", "lhsDouble=-3.14", "rhsInt=1", "expDouble=-2.14"), + Iteration("-double+-int", "lhsDouble=-3.14", "rhsInt=-1", "expDouble=-4.14") + ) + fun testPlus_doubleAndInt_returnsDouble() { + val lhsReal = createIrrationalReal(lhsDouble) + val rhsReal = createIntegerReal(rhsInt) + + val result = lhsReal + rhsReal + + assertThat(result).isIrrationalThat().isWithin(1e-5).of(expDouble) + } + + @Test + @RunParameterized( + Iteration("identity+identity", "lhsDouble=0.0", "rhsFrac=0/1", "expDouble=0.0"), + Iteration("double+identity", "lhsDouble=3.14", "rhsFrac=0/1", "expDouble=3.14"), + Iteration("double+fraction", "lhsDouble=3.14", "rhsFrac=3/2", "expDouble=4.64"), + Iteration("double+wholeNumberFraction", "lhsDouble=2.0", "rhsFrac=3/1", "expDouble=5.0"), + Iteration("commutativity", "lhsDouble=3.0", "rhsFrac=2/1", "expDouble=5.0"), + Iteration("double+-fraction", "lhsDouble=3.14", "rhsFrac=-3/2", "expDouble=1.64"), + Iteration("-double+fraction", "lhsDouble=-3.14", "rhsFrac=3/2", "expDouble=-1.64"), + Iteration("-double+-fraction", "lhsDouble=-3.14", "rhsFrac=-3/2", "expDouble=-4.64") + ) + fun testPlus_doubleAndFraction_returnsDouble() { + val lhsReal = createIrrationalReal(lhsDouble) + val rhsReal = createRationalReal(rhsFrac) + + val result = lhsReal + rhsReal + + assertThat(result).isIrrationalThat().isWithin(1e-5).of(expDouble) + } + + @Test + @RunParameterized( + Iteration("identity+identity", "lhsDouble=0.0", "rhsDouble=0.0", "expDouble=0.0"), + Iteration("double+identity", "lhsDouble=1.0", "rhsDouble=0.0", "expDouble=1.0"), + Iteration("double+double", "lhsDouble=3.14", "rhsDouble=2.7", "expDouble=5.84"), + Iteration("commutativity", "lhsDouble=2.7", "rhsDouble=3.14", "expDouble=5.84"), + Iteration("double+-double", "lhsDouble=3.14", "rhsDouble=-2.7", "expDouble=0.44"), + Iteration("-double+double", "lhsDouble=-3.14", "rhsDouble=2.7", "expDouble=-0.44"), + Iteration("-double+-double", "lhsDouble=-3.14", "rhsDouble=-2.7", "expDouble=-5.84") + ) + fun testPlus_doubleAndDouble_returnsDouble() { + val lhsReal = createIrrationalReal(lhsDouble) + val rhsReal = createIrrationalReal(rhsDouble) + + val result = lhsReal + rhsReal + + assertThat(result).isIrrationalThat().isWithin(1e-5).of(expDouble) + } + + // Subtraction tests. + + @Test + @RunParameterized( + Iteration("identity-identity", "lhsInt=0", "rhsInt=0", "expInt=0"), + Iteration("int-identity", "lhsInt=1", "rhsInt=0", "expInt=1"), + Iteration("int-int", "lhsInt=1", "rhsInt=2", "expInt=-1"), + Iteration("anticommutativity", "lhsInt=2", "rhsInt=1", "expInt=1"), + Iteration("int--int", "lhsInt=1", "rhsInt=-2", "expInt=3"), + Iteration("-int-int", "lhsInt=-1", "rhsInt=2", "expInt=-3"), + Iteration("-int--int", "lhsInt=-1", "rhsInt=-2", "expInt=1") + ) + fun testMinus_intAndInt_returnsInt() { + val lhsReal = createIntegerReal(lhsInt) + val rhsReal = createIntegerReal(rhsInt) + + val result = lhsReal - rhsReal + + assertThat(result).isIntegerThat().isEqualTo(expInt) + } + + @Test + @RunParameterized( + Iteration("identity-identity", "lhsInt=0", "rhsFrac=0/1", "expFrac=0/1"), + Iteration("int-identity", "lhsInt=1", "rhsFrac=0/1", "expFrac=1"), + Iteration("int-fraction", "lhsInt=2", "rhsFrac=1/3", "expFrac=1 2/3"), + Iteration("int-wholeNumberFraction", "lhsInt=2", "rhsFrac=3/1", "expFrac=-1"), + Iteration("anticommutativity", "lhsInt=3", "rhsFrac=2/1", "expFrac=1"), + Iteration("int--fraction", "lhsInt=2", "rhsFrac=-1/3", "expFrac=2 1/3"), + Iteration("-int-fraction", "lhsInt=-2", "rhsFrac=1/3", "expFrac=-2 1/3"), + Iteration("-int--fraction", "lhsInt=-2", "rhsFrac=-1/3", "expFrac=-1 2/3") + ) + fun testMinus_intAndFraction_returnsFraction() { + val lhsReal = createIntegerReal(lhsInt) + val rhsReal = createRationalReal(rhsFrac) + + val result = lhsReal - rhsReal + + val expectedFraction = parseFraction(expFrac) + assertThat(result).isRationalThat().isEqualTo(expectedFraction) + } + + @Test + @RunParameterized( + Iteration("identity-identity", "lhsInt=0", "rhsDouble=0.0", "expDouble=0.0"), + Iteration("int-identity", "lhsInt=1", "rhsDouble=0.0", "expDouble=1.0"), + Iteration("int-double", "lhsInt=1", "rhsDouble=3.14", "expDouble=-2.14"), + Iteration("int-wholeNumberDouble", "lhsInt=1", "rhsDouble=3.0", "expDouble=-2.0"), + Iteration("anticommutativity", "lhsInt=3", "rhsDouble=1.0", "expDouble=2.0"), + Iteration("int--double", "lhsInt=1", "rhsDouble=-3.14", "expDouble=4.14"), + Iteration("-int-double", "lhsInt=-1", "rhsDouble=3.14", "expDouble=-4.14"), + Iteration("-int--double", "lhsInt=-1", "rhsDouble=-3.14", "expDouble=2.14") + ) + fun testMinus_intAndDouble_returnsDouble() { + val lhsReal = createIntegerReal(lhsInt) + val rhsReal = createIrrationalReal(rhsDouble) + + val result = lhsReal - rhsReal + + assertThat(result).isIrrationalThat().isWithin(1e-5).of(expDouble) + } + + @Test + @RunParameterized( + Iteration("identity-identity", "lhsFrac=0/1", "rhsInt=0", "expFrac=0/1"), + Iteration("fraction-identity", "lhsFrac=1/1", "rhsInt=0", "expFrac=1"), + Iteration("fraction-int", "lhsFrac=1/3", "rhsInt=2", "expFrac=-1 2/3"), + Iteration("wholeNumberFraction-int", "lhsFrac=3/1", "rhsInt=2", "expFrac=1"), + Iteration("anticommutativity", "lhsFrac=2/1", "rhsInt=3", "expFrac=-1"), + Iteration("fraction--int", "lhsFrac=1/3", "rhsInt=-2", "expFrac=2 1/3"), + Iteration("-fraction-int", "lhsFrac=-1/3", "rhsInt=2", "expFrac=-2 1/3"), + Iteration("-fraction--int", "lhsFrac=-1/3", "rhsInt=-2", "expFrac=1 2/3") + ) + fun testMinus_fractionAndInt_returnsFraction() { + val lhsReal = createRationalReal(lhsFrac) + val rhsReal = createIntegerReal(rhsInt) + + val result = lhsReal - rhsReal + + val expectedFraction = parseFraction(expFrac) + assertThat(result).isRationalThat().isEqualTo(expectedFraction) + } + + @Test + @RunParameterized( + Iteration("identity-identity", "lhsFrac=0/1", "rhsFrac=0/1", "expFrac=0/1"), + Iteration("fraction-identity", "lhsFrac=3/2", "rhsFrac=0/1", "expFrac=1 1/2"), + Iteration("fraction-fraction", "lhsFrac=3/2", "rhsFrac=1/3", "expFrac=1 1/6"), + Iteration("anticommutativity", "lhsFrac=1/3", "rhsFrac=3/2", "expFrac=-1 1/6"), + Iteration("fraction--fraction", "lhsFrac=1/2", "rhsFrac=-1/3", "expFrac=5/6"), + Iteration("-fraction-fraction", "lhsFrac=-1/2", "rhsFrac=1/3", "expFrac=-5/6"), + Iteration("-fraction--fraction", "lhsFrac=-1/2", "rhsFrac=-1/3", "expFrac=-1/6") + ) + fun testMinus_fractionAndFraction_returnsFraction() { + val lhsReal = createRationalReal(lhsFrac) + val rhsReal = createRationalReal(rhsFrac) + + val result = lhsReal - rhsReal + + val expectedFraction = parseFraction(expFrac) + assertThat(result).isRationalThat().isEqualTo(expectedFraction) + } + + @Test + @RunParameterized( + Iteration("identity-identity", "lhsFrac=0/1", "rhsDouble=0.0", "expDouble=0.0"), + Iteration("fraction-identity", "lhsFrac=3/2", "rhsDouble=0.0", "expDouble=1.5"), + Iteration("fraction-double", "lhsFrac=3/2", "rhsDouble=3.14", "expDouble=-1.64"), + Iteration("wholeNumberFraction-double", "lhsFrac=3/1", "rhsDouble=2.0", "expDouble=1.0"), + Iteration("anticommutativity", "lhsFrac=2/1", "rhsDouble=3.0", "expDouble=-1.0"), + Iteration("fraction--double", "lhsFrac=3/2", "rhsDouble=-3.14", "expDouble=4.64"), + Iteration("-fraction-double", "lhsFrac=-3/2", "rhsDouble=3.14", "expDouble=-4.64"), + Iteration("-fraction--double", "lhsFrac=-3/2", "rhsDouble=-3.14", "expDouble=1.64") + ) + fun testMinus_fractionAndDouble_returnsDouble() { + val lhsReal = createRationalReal(lhsFrac) + val rhsReal = createIrrationalReal(rhsDouble) + + val result = lhsReal - rhsReal + + assertThat(result).isIrrationalThat().isWithin(1e-5).of(expDouble) + } + + @Test + @RunParameterized( + Iteration("identity-identity", "lhsDouble=0.0", "rhsInt=0", "expDouble=0.0"), + Iteration("double-identity", "lhsDouble=1.0", "rhsInt=0", "expDouble=1.0"), + Iteration("double-int", "lhsDouble=3.14", "rhsInt=1", "expDouble=2.14"), + Iteration("wholeNumberDouble-int", "lhsDouble=3.0", "rhsInt=1", "expDouble=2.0"), + Iteration("anticommutativity", "lhsDouble=1.0", "rhsInt=3", "expDouble=-2.0"), + Iteration("double--int", "lhsDouble=3.14", "rhsInt=-1", "expDouble=4.14"), + Iteration("-double-int", "lhsDouble=-3.14", "rhsInt=1", "expDouble=-4.14"), + Iteration("-double--int", "lhsDouble=-3.14", "rhsInt=-1", "expDouble=-2.14") + ) + fun testMinus_doubleAndInt_returnsDouble() { + val lhsReal = createIrrationalReal(lhsDouble) + val rhsReal = createIntegerReal(rhsInt) + + val result = lhsReal - rhsReal + + assertThat(result).isIrrationalThat().isWithin(1e-5).of(expDouble) + } + + @Test + @RunParameterized( + Iteration("identity-identity", "lhsDouble=0.0", "rhsFrac=0/1", "expDouble=0.0"), + Iteration("double-identity", "lhsDouble=3.14", "rhsFrac=0/1", "expDouble=3.14"), + Iteration("double-fraction", "lhsDouble=3.14", "rhsFrac=3/2", "expDouble=1.64"), + Iteration("double-wholeNumberFraction", "lhsDouble=2.0", "rhsFrac=3/1", "expDouble=-1.0"), + Iteration("anticommutativity", "lhsDouble=3.0", "rhsFrac=2/1", "expDouble=1.0"), + Iteration("double--fraction", "lhsDouble=3.14", "rhsFrac=-3/2", "expDouble=4.64"), + Iteration("-double-fraction", "lhsDouble=-3.14", "rhsFrac=3/2", "expDouble=-4.64"), + Iteration("-double--fraction", "lhsDouble=-3.14", "rhsFrac=-3/2", "expDouble=-1.64") + ) + fun testMinus_doubleAndFraction_returnsDouble() { + val lhsReal = createIrrationalReal(lhsDouble) + val rhsReal = createRationalReal(rhsFrac) + + val result = lhsReal - rhsReal + + assertThat(result).isIrrationalThat().isWithin(1e-5).of(expDouble) + } + + @Test + @RunParameterized( + Iteration("identity-identity", "lhsDouble=0.0", "rhsDouble=0.0", "expDouble=0.0"), + Iteration("double-identity", "lhsDouble=1.0", "rhsDouble=0.0", "expDouble=1.0"), + Iteration("double-double", "lhsDouble=3.14", "rhsDouble=2.7", "expDouble=0.44"), + Iteration("anticommutativity", "lhsDouble=2.7", "rhsDouble=3.14", "expDouble=-0.44"), + Iteration("double--double", "lhsDouble=3.14", "rhsDouble=-2.7", "expDouble=5.84"), + Iteration("-double-double", "lhsDouble=-3.14", "rhsDouble=2.7", "expDouble=-5.84"), + Iteration("-double--double", "lhsDouble=-3.14", "rhsDouble=-2.7", "expDouble=-0.44") + ) + fun testMinus_doubleAndDouble_returnsDouble() { + val lhsReal = createIrrationalReal(lhsDouble) + val rhsReal = createIrrationalReal(rhsDouble) + + val result = lhsReal - rhsReal + + assertThat(result).isIrrationalThat().isWithin(1e-5).of(expDouble) + } + + // Multiplication tests. + + @Test + @RunParameterized( + Iteration("identity*identity", "lhsInt=1", "rhsInt=1", "expInt=1"), + Iteration("int*identity", "lhsInt=2", "rhsInt=1", "expInt=2"), + Iteration("int*int", "lhsInt=3", "rhsInt=2", "expInt=6"), + Iteration("commutativity", "lhsInt=2", "rhsInt=3", "expInt=6"), + Iteration("int*-int", "lhsInt=3", "rhsInt=-2", "expInt=-6"), + Iteration("-int*int", "lhsInt=-3", "rhsInt=2", "expInt=-6"), + Iteration("-int*-int", "lhsInt=-3", "rhsInt=-2", "expInt=6") + ) + fun testTimes_intAndInt_returnsInt() { + val lhsReal = createIntegerReal(lhsInt) + val rhsReal = createIntegerReal(rhsInt) + + val result = lhsReal * rhsReal + + assertThat(result).isIntegerThat().isEqualTo(expInt) + } + + @Test + @RunParameterized( + Iteration("identity*identity", "lhsInt=1", "rhsFrac=1", "expFrac=1"), + Iteration("int*identity", "lhsInt=2", "rhsFrac=1", "expFrac=2"), + Iteration("int*fraction", "lhsInt=2", "rhsFrac=1/3", "expFrac=2/3"), + Iteration("int*wholeNumberFraction", "lhsInt=2", "rhsFrac=3/1", "expFrac=6"), + Iteration("commutativity", "lhsInt=3", "rhsFrac=2/1", "expFrac=6"), + Iteration("int*-fraction", "lhsInt=2", "rhsFrac=-1/3", "expFrac=-2/3"), + Iteration("-int*fraction", "lhsInt=-2", "rhsFrac=1/3", "expFrac=-2/3"), + Iteration("-int*-fraction", "lhsInt=-2", "rhsFrac=-1/3", "expFrac=2/3") + ) + fun testTimes_intAndFraction_returnsFraction() { + val lhsReal = createIntegerReal(lhsInt) + val rhsReal = createRationalReal(rhsFrac) + + val result = lhsReal * rhsReal + + val expectedFraction = parseFraction(expFrac) + assertThat(result).isRationalThat().isEqualTo(expectedFraction) + } + + @Test + @RunParameterized( + Iteration("identity*identity", "lhsInt=1", "rhsDouble=1.0", "expDouble=1.0"), + Iteration("int*identity", "lhsInt=2", "rhsDouble=1.0", "expDouble=2.0"), + Iteration("int*double", "lhsInt=2", "rhsDouble=3.14", "expDouble=6.28"), + Iteration("int*wholeNumberDouble", "lhsInt=2", "rhsDouble=3.0", "expDouble=6.0"), + Iteration("commutativity", "lhsInt=3", "rhsDouble=2.0", "expDouble=6.0"), + Iteration("int*-double", "lhsInt=2", "rhsDouble=-3.14", "expDouble=-6.28"), + Iteration("-int*double", "lhsInt=-2", "rhsDouble=3.14", "expDouble=-6.28"), + Iteration("-int*-double", "lhsInt=-2", "rhsDouble=-3.14", "expDouble=6.28") + ) + fun testTimes_intAndDouble_returnsDouble() { + val lhsReal = createIntegerReal(lhsInt) + val rhsReal = createIrrationalReal(rhsDouble) + + val result = lhsReal * rhsReal + + assertThat(result).isIrrationalThat().isWithin(1e-5).of(expDouble) + } + + @Test + @RunParameterized( + Iteration("identity*identity", "lhsFrac=1/1", "rhsInt=1", "expFrac=1"), + Iteration("fraction*identity", "lhsFrac=2/1", "rhsInt=1", "expFrac=2"), + Iteration("fraction*int", "lhsFrac=1/3", "rhsInt=2", "expFrac=2/3"), + Iteration("wholeNumberFraction*int", "lhsFrac=3/1", "rhsInt=2", "expFrac=6"), + Iteration("commutativity", "lhsFrac=2/1", "rhsInt=3", "expFrac=6"), + Iteration("fraction*-int", "lhsFrac=1/3", "rhsInt=-2", "expFrac=-2/3"), + Iteration("-fraction*int", "lhsFrac=-1/3", "rhsInt=2", "expFrac=-2/3"), + Iteration("-fraction*-int", "lhsFrac=-1/3", "rhsInt=-2", "expFrac=2/3") + ) + fun testTimes_fractionAndInt_returnsFraction() { + val lhsReal = createRationalReal(lhsFrac) + val rhsReal = createIntegerReal(rhsInt) + + val result = lhsReal * rhsReal + + val expectedFraction = parseFraction(expFrac) + assertThat(result).isRationalThat().isEqualTo(expectedFraction) + } + + @Test + @RunParameterized( + Iteration("identity*identity", "lhsFrac=1/1", "rhsFrac=1/1", "expFrac=1"), + Iteration("fraction*identity", "lhsFrac=3/2", "rhsFrac=1/1", "expFrac=1 1/2"), + Iteration("fraction*fraction", "lhsFrac=3/2", "rhsFrac=4/7", "expFrac=6/7"), + Iteration("commutativity", "lhsFrac=4/7", "rhsFrac=3/2", "expFrac=6/7"), + Iteration("fraction*-fraction", "lhsFrac=1 3/9", "rhsFrac=-8/11", "expFrac=-32/33"), + Iteration("-fraction*fraction", "lhsFrac=-1 3/9", "rhsFrac=8/11", "expFrac=-32/33"), + Iteration("-fraction*-fraction", "lhsFrac=-1 3/9", "rhsFrac=-8/11", "expFrac=32/33") + ) + fun testTimes_fractionAndFraction_returnsFraction() { + val lhsReal = createRationalReal(lhsFrac) + val rhsReal = createRationalReal(rhsFrac) + + val result = lhsReal * rhsReal + + val expectedFraction = parseFraction(expFrac) + assertThat(result).isRationalThat().isEqualTo(expectedFraction) + } + + @Test + @RunParameterized( + Iteration("identity*identity", "lhsFrac=1/1", "rhsDouble=1.0", "expDouble=1.0"), + Iteration("fraction*identity", "lhsFrac=3/2", "rhsDouble=1.0", "expDouble=1.5"), + Iteration("fraction*double", "lhsFrac=3/2", "rhsDouble=3.14", "expDouble=4.71"), + Iteration("wholeNumberFraction*double", "lhsFrac=3/1", "rhsDouble=2.0", "expDouble=6.0"), + Iteration("commutativity", "lhsFrac=2/1", "rhsDouble=3.0", "expDouble=6.0"), + Iteration("fraction*-double", "lhsFrac=1 3/2", "rhsDouble=-3.14", "expDouble=-7.85"), + Iteration("-fraction*double", "lhsFrac=-1 3/2", "rhsDouble=3.14", "expDouble=-7.85"), + Iteration("-fraction*-double", "lhsFrac=-1 3/2", "rhsDouble=-3.14", "expDouble=7.85") + ) + fun testTimes_fractionAndDouble_returnsDouble() { + val lhsReal = createRationalReal(lhsFrac) + val rhsReal = createIrrationalReal(rhsDouble) + + val result = lhsReal * rhsReal + + assertThat(result).isIrrationalThat().isWithin(1e-5).of(expDouble) + } + + @Test + @RunParameterized( + Iteration("identity*identity", "lhsDouble=1.0", "rhsInt=1", "expDouble=1.0"), + Iteration("double*identity", "lhsDouble=2.0", "rhsInt=1", "expDouble=2.0"), + Iteration("double*int", "lhsDouble=3.14", "rhsInt=2", "expDouble=6.28"), + Iteration("wholeNumberDouble*int", "lhsDouble=3.0", "rhsInt=2", "expDouble=6"), + Iteration("commutativity", "lhsDouble=2.0", "rhsInt=3", "expDouble=6.0"), + Iteration("double*-int", "lhsDouble=3.14", "rhsInt=-2", "expDouble=-6.28"), + Iteration("-double*int", "lhsDouble=-3.14", "rhsInt=2", "expDouble=-6.28"), + Iteration("-double*-int", "lhsDouble=-3.14", "rhsInt=-2", "expDouble=6.28") + ) + fun testTimes_doubleAndInt_returnsDouble() { + val lhsReal = createIrrationalReal(lhsDouble) + val rhsReal = createIntegerReal(rhsInt) + + val result = lhsReal * rhsReal + + assertThat(result).isIrrationalThat().isWithin(1e-5).of(expDouble) + } + + @Test + @RunParameterized( + Iteration("identity*identity", "lhsDouble=1.0", "rhsFrac=1/1", "expDouble=1.0"), + Iteration("double*identity", "lhsDouble=2.0", "rhsFrac=1/1", "expDouble=2.0"), + Iteration("double*fraction", "lhsDouble=3.14", "rhsFrac=3/2", "expDouble=4.71"), + Iteration("double*wholeNumberFraction", "lhsDouble=2.0", "rhsFrac=3/1", "expDouble=6.0"), + Iteration("commutativity", "lhsDouble=3.0", "rhsFrac=2/1", "expDouble=6.0"), + Iteration("double*-fraction", "lhsDouble=3.14", "rhsFrac=-1 3/2", "expDouble=-7.85"), + Iteration("-double*fraction", "lhsDouble=-3.14", "rhsFrac=1 3/2", "expDouble=-7.85"), + Iteration("-double*-fraction", "lhsDouble=-3.14", "rhsFrac=-1 3/2", "expDouble=7.85") + ) + fun testTimes_doubleAndFraction_returnsDouble() { + val lhsReal = createIrrationalReal(lhsDouble) + val rhsReal = createRationalReal(rhsFrac) + + val result = lhsReal * rhsReal + + assertThat(result).isIrrationalThat().isWithin(1e-5).of(expDouble) + } + + @Test + @RunParameterized( + Iteration("identity*identity", "lhsDouble=1.0", "rhsDouble=1.0", "expDouble=1.0"), + Iteration("double*identity", "lhsDouble=2.0", "rhsDouble=1.0", "expDouble=2.0"), + Iteration("double*double", "lhsDouble=3.14", "rhsDouble=2.7", "expDouble=8.478"), + Iteration("commutativity", "lhsDouble=2.7", "rhsDouble=3.14", "expDouble=8.478"), + Iteration("double*-double", "lhsDouble=3.14", "rhsDouble=-2.7", "expDouble=-8.478"), + Iteration("-double*double", "lhsDouble=-3.14", "rhsDouble=2.7", "expDouble=-8.478"), + Iteration("-double*-double", "lhsDouble=-3.14", "rhsDouble=-2.7", "expDouble=8.478") + ) + fun testTimes_doubleAndDouble_returnsDouble() { + val lhsReal = createIrrationalReal(lhsDouble) + val rhsReal = createIrrationalReal(rhsDouble) + + val result = lhsReal * rhsReal + + assertThat(result).isIrrationalThat().isWithin(1e-5).of(expDouble) + } + + // Division tests. + + @Test + @RunParameterized( + Iteration("identity/identity", "lhsInt=1", "rhsInt=1", "expInt=1"), + Iteration("int/identity", "lhsInt=2", "rhsInt=1", "expInt=2"), + Iteration("int/int", "lhsInt=8", "rhsInt=2", "expInt=4"), + Iteration("int/-int", "lhsInt=8", "rhsInt=-2", "expInt=-4"), + Iteration("-int/int", "lhsInt=-8", "rhsInt=2", "expInt=-4"), + Iteration("-int/-int", "lhsInt=-8", "rhsInt=-2", "expInt=4") + ) + fun testDiv_intAndInt_divides_returnsInt() { + val lhsReal = createIntegerReal(lhsInt) + val rhsReal = createIntegerReal(rhsInt) + + val result = lhsReal / rhsReal + + // If the divisor divides the dividend, the result is an integer. + assertThat(result).isIntegerThat().isEqualTo(expInt) + } + + @Test + @RunParameterized( + Iteration("int/int", "lhsInt=7", "rhsInt=2", "expFrac=3 1/2"), + Iteration("anticommutativity", "lhsInt=2", "rhsInt=7", "expFrac=2/7"), + Iteration("int/-int", "lhsInt=7", "rhsInt=-2", "expFrac=-3 1/2"), + Iteration("-int/int", "lhsInt=-7", "rhsInt=2", "expFrac=-3 1/2"), + Iteration("-int/-int", "lhsInt=-7", "rhsInt=-2", "expFrac=3 1/2") + ) + fun testDiv_intAndInt_doesNotDivide_returnsFraction() { + val lhsReal = createIntegerReal(lhsInt) + val rhsReal = createIntegerReal(rhsInt) + + val result = lhsReal / rhsReal + + // If the divisor doesn't divide the dividend, the result is a fraction. + val expectedFraction = parseFraction(expFrac) + assertThat(result).isRationalThat().isEqualTo(expectedFraction) + } + + @Test + @RunParameterized( + Iteration("identity/identity", "lhsInt=1", "rhsFrac=1", "expFrac=1"), + Iteration("int/identity", "lhsInt=2", "rhsFrac=1", "expFrac=2"), + Iteration("int/fraction", "lhsInt=4", "rhsFrac=1/3", "expFrac=12"), + Iteration("int/wholeNumberFraction", "lhsInt=2", "rhsFrac=3/1", "expFrac=2/3"), + Iteration("anticommutativity", "lhsInt=3", "rhsFrac=2/1", "expFrac=1 1/2"), + Iteration("int/-fraction", "lhsInt=5", "rhsFrac=-2/3", "expFrac=-7 1/2"), + Iteration("-int/fraction", "lhsInt=-5", "rhsFrac=2/3", "expFrac=-7 1/2"), + Iteration("-int/-fraction", "lhsInt=-5", "rhsFrac=-2/3", "expFrac=7 1/2") + ) + fun testDiv_intAndFraction_returnsFraction() { + val lhsReal = createIntegerReal(lhsInt) + val rhsReal = createRationalReal(rhsFrac) + + val result = lhsReal / rhsReal + + val expectedFraction = parseFraction(expFrac) + assertThat(result).isRationalThat().isEqualTo(expectedFraction) + } + + @Test + @RunParameterized( + Iteration("identity/identity", "lhsInt=1", "rhsDouble=1.0", "expDouble=1.0"), + Iteration("int/identity", "lhsInt=2", "rhsDouble=1.0", "expDouble=2.0"), + Iteration("int/double", "lhsInt=2", "rhsDouble=3.14", "expDouble=0.636942675"), + Iteration("int/wholeNumberDouble", "lhsInt=2", "rhsDouble=3.0", "expDouble=0.666666667"), + Iteration("anticommutativity", "lhsInt=3", "rhsDouble=2.0", "expDouble=1.5"), + Iteration("int/-double", "lhsInt=2", "rhsDouble=-3.14", "expDouble=-0.636942675"), + Iteration("-int/double", "lhsInt=-2", "rhsDouble=3.14", "expDouble=-0.636942675"), + Iteration("-int/-double", "lhsInt=-2", "rhsDouble=-3.14", "expDouble=0.636942675") + ) + fun testDiv_intAndDouble_returnsDouble() { + val lhsReal = createIntegerReal(lhsInt) + val rhsReal = createIrrationalReal(rhsDouble) + + val result = lhsReal / rhsReal + + assertThat(result).isIrrationalThat().isWithin(1e-5).of(expDouble) + } + + @Test + @RunParameterized( + Iteration("identity/identity", "lhsFrac=1/1", "rhsInt=1", "expFrac=1"), + Iteration("fraction/identity", "lhsFrac=2/1", "rhsInt=1", "expFrac=2"), + Iteration("fraction/int", "lhsFrac=1/3", "rhsInt=2", "expFrac=1/6"), + Iteration("wholeNumberFraction/int", "lhsFrac=3/1", "rhsInt=2", "expFrac=1 1/2"), + Iteration("anticommutativity", "lhsFrac=2/1", "rhsInt=3", "expFrac=2/3"), + Iteration("fraction/-int", "lhsFrac=-1 1/3", "rhsInt=2", "expFrac=-2/3"), + Iteration("-fraction/int", "lhsFrac=1 1/3", "rhsInt=-2", "expFrac=-2/3"), + Iteration("-fraction/-int", "lhsFrac=-1 1/3", "rhsInt=-2", "expFrac=2/3") + ) + fun testDiv_fractionAndInt_returnsFraction() { + val lhsReal = createRationalReal(lhsFrac) + val rhsReal = createIntegerReal(rhsInt) + + val result = lhsReal / rhsReal + + val expectedFraction = parseFraction(expFrac) + assertThat(result).isRationalThat().isEqualTo(expectedFraction) + } + + @Test + @RunParameterized( + Iteration("identity/identity", "lhsFrac=1/1", "rhsFrac=1/1", "expFrac=1"), + Iteration("fraction/identity", "lhsFrac=3/2", "rhsFrac=1/1", "expFrac=1 1/2"), + Iteration("fraction/fraction", "lhsFrac=3/2", "rhsFrac=4/7", "expFrac=2 5/8"), + Iteration("anticommutativity", "lhsFrac=4/7", "rhsFrac=3/2", "expFrac=8/21"), + Iteration("fraction/-fraction", "lhsFrac=1 3/9", "rhsFrac=-8/11", "expFrac=-1 5/6"), + Iteration("-fraction/fraction", "lhsFrac=-1 3/9", "rhsFrac=8/11", "expFrac=-1 5/6"), + Iteration("-fraction/-fraction", "lhsFrac=-1 3/9", "rhsFrac=-8/11", "expFrac=1 5/6") + ) + fun testDiv_fractionAndFraction_returnsFraction() { + val lhsReal = createRationalReal(lhsFrac) + val rhsReal = createRationalReal(rhsFrac) + + val result = lhsReal / rhsReal + + val expectedFraction = parseFraction(expFrac) + assertThat(result).isRationalThat().isEqualTo(expectedFraction) + } + + @Test + @RunParameterized( + Iteration("identity/identity", "lhsFrac=1/1", "rhsDouble=1.0", "expDouble=1.0"), + Iteration("fraction/identity", "lhsFrac=3/2", "rhsDouble=1.0", "expDouble=1.5"), + Iteration("fraction/double", "lhsFrac=3/2", "rhsDouble=3.14", "expDouble=0.477707006"), + Iteration("wholeNumberFraction/double", "lhsFrac=3/1", "rhsDouble=2.0", "expDouble=1.5"), + Iteration("anticommutativity", "lhsFrac=2/1", "rhsDouble=3.0", "expDouble=0.666666667"), + Iteration("fraction/-double", "lhsFrac=1 3/2", "rhsDouble=-3.14", "expDouble=-0.796178344"), + Iteration("-fraction/double", "lhsFrac=-1 3/2", "rhsDouble=3.14", "expDouble=-0.796178344"), + Iteration("-fraction/-double", "lhsFrac=-1 3/2", "rhsDouble=-3.14", "expDouble=0.796178344") + ) + fun testDiv_fractionAndDouble_returnsDouble() { + val lhsReal = createRationalReal(lhsFrac) + val rhsReal = createIrrationalReal(rhsDouble) + + val result = lhsReal / rhsReal + + assertThat(result).isIrrationalThat().isWithin(1e-5).of(expDouble) + } + + @Test + @RunParameterized( + Iteration("identity/identity", "lhsDouble=1.0", "rhsInt=1", "expDouble=1.0"), + Iteration("double/identity", "lhsDouble=2.0", "rhsInt=1", "expDouble=2.0"), + Iteration("double/int", "lhsDouble=3.14", "rhsInt=2", "expDouble=1.57"), + Iteration("wholeNumberDouble/int", "lhsDouble=3.0", "rhsInt=2", "expDouble=1.5"), + Iteration("anticommutativity", "lhsDouble=2.0", "rhsInt=3", "expDouble=0.666666667"), + Iteration("double/-int", "lhsDouble=3.14", "rhsInt=-2", "expDouble=-1.57"), + Iteration("-double/int", "lhsDouble=-3.14", "rhsInt=2", "expDouble=-1.57"), + Iteration("-double/-int", "lhsDouble=-3.14", "rhsInt=-2", "expDouble=1.57") + ) + fun testDiv_doubleAndInt_returnsDouble() { + val lhsReal = createIrrationalReal(lhsDouble) + val rhsReal = createIntegerReal(rhsInt) + + val result = lhsReal / rhsReal + + assertThat(result).isIrrationalThat().isWithin(1e-5).of(expDouble) + } + + @Test + @RunParameterized( + Iteration("identity/identity", "lhsDouble=1.0", "rhsFrac=1/1", "expDouble=1.0"), + Iteration("double/identity", "lhsDouble=2.0", "rhsFrac=1/1", "expDouble=2.0"), + Iteration("double/fraction", "lhsDouble=3.14", "rhsFrac=3/2", "expDouble=2.093333333"), + Iteration("double/wholeNumberFraction", "lhsDouble=2.0", "rhsFrac=3/1", "expDouble=0.66666667"), + Iteration("anticommutativity", "lhsDouble=3.0", "rhsFrac=2/1", "expDouble=1.5"), + Iteration("double/-fraction", "lhsDouble=3.14", "rhsFrac=-1 3/2", "expDouble=-1.256"), + Iteration("-double/fraction", "lhsDouble=-3.14", "rhsFrac=1 3/2", "expDouble=-1.256"), + Iteration("-double/-fraction", "lhsDouble=-3.14", "rhsFrac=-1 3/2", "expDouble=1.256") + ) + fun testDiv_doubleAndFraction_returnsDouble() { + val lhsReal = createIrrationalReal(lhsDouble) + val rhsReal = createRationalReal(rhsFrac) + + val result = lhsReal / rhsReal + + assertThat(result).isIrrationalThat().isWithin(1e-5).of(expDouble) + } + + @Test + @RunParameterized( + Iteration("identity/identity", "lhsDouble=1.0", "rhsDouble=1.0", "expDouble=1.0"), + Iteration("double/identity", "lhsDouble=2.0", "rhsDouble=1.0", "expDouble=2.0"), + Iteration("double/double", "lhsDouble=3.14", "rhsDouble=2.7", "expDouble=1.162962963"), + Iteration("anticommutativity", "lhsDouble=2.7", "rhsDouble=3.14", "expDouble=0.859872611"), + Iteration("double/-double", "lhsDouble=3.14", "rhsDouble=-2.7", "expDouble=-1.162962963"), + Iteration("-double/double", "lhsDouble=-3.14", "rhsDouble=2.7", "expDouble=-1.162962963"), + Iteration("-double/-double", "lhsDouble=-3.14", "rhsDouble=-2.7", "expDouble=1.162962963") + ) + fun testDiv_doubleAndDouble_returnsDouble() { + val lhsReal = createIrrationalReal(lhsDouble) + val rhsReal = createIrrationalReal(rhsDouble) + + val result = lhsReal / rhsReal + + assertThat(result).isIrrationalThat().isWithin(1e-5).of(expDouble) + } + + @Test + fun testDiv_intDividedByZeroInt_throwsException() { + val lhsReal = createIntegerReal(2) + val rhsReal = createIntegerReal(0) + + assertThrows(ArithmeticException::class) { lhsReal / rhsReal } + } + + @Test + fun testDiv_intDividedByZeroFraction_throwsException() { + val lhsReal = createIntegerReal(2) + val rhsReal = createRationalReal(ZERO_FRACTION) + + assertThrows(ArithmeticException::class) { lhsReal / rhsReal } + } + + @Test + fun testDiv_intDividedByZeroDouble_returnsInfinityDouble() { + val lhsReal = createIntegerReal(2) + val rhsReal = createIrrationalReal(0.0) + + val result = lhsReal / rhsReal + + assertThat(result).isEqualTo(createIrrationalReal(Double.POSITIVE_INFINITY)) + } + + @Test + fun testDiv_fractionDividedByZeroInt_throwsException() { + val lhsReal = ONE_AND_ONE_HALF_REAL + val rhsReal = createIntegerReal(0) + + assertThrows(ArithmeticException::class) { lhsReal / rhsReal } + } + + @Test + fun testDiv_fractionDividedByZeroFraction_throwsException() { + val lhsReal = ONE_AND_ONE_HALF_REAL + val rhsReal = createRationalReal(ZERO_FRACTION) + + assertThrows(ArithmeticException::class) { lhsReal / rhsReal } + } + + @Test + fun testDiv_fractionDividedByZeroDouble_returnsInfinityDouble() { + val lhsReal = ONE_AND_ONE_HALF_REAL + val rhsReal = createIrrationalReal(0.0) + + val result = lhsReal / rhsReal + + assertThat(result).isEqualTo(createIrrationalReal(Double.POSITIVE_INFINITY)) + } + + @Test + fun testDiv_doubleDividedByZeroInt_returnsInfinityDouble() { + val lhsReal = createIrrationalReal(3.14) + val rhsReal = createIntegerReal(0) + + val result = lhsReal / rhsReal + + assertThat(result).isEqualTo(createIrrationalReal(Double.POSITIVE_INFINITY)) + } + + @Test + fun testDiv_doubleDividedByZeroFraction_returnsInfinityDouble() { + val lhsReal = createIrrationalReal(3.14) + val rhsReal = createRationalReal(ZERO_FRACTION) + + val result = lhsReal / rhsReal + + assertThat(result).isEqualTo(createIrrationalReal(Double.POSITIVE_INFINITY)) + } + + @Test + fun testDiv_doubleDividedByZeroDouble_returnsInfinityDouble() { + val lhsReal = createIrrationalReal(3.14) + val rhsReal = createIrrationalReal(0.0) + + val result = lhsReal / rhsReal + + assertThat(result).isEqualTo(createIrrationalReal(Double.POSITIVE_INFINITY)) + } + + // Exponentiation tests. + + @Test + @RunParameterized( + Iteration("0^0", "lhsInt=0", "rhsInt=0", "expInt=1"), + Iteration("identity^0", "lhsInt=1", "rhsInt=0", "expInt=1"), + Iteration("identity^identity", "lhsInt=1", "rhsInt=1", "expInt=1"), + Iteration("int^0", "lhsInt=2", "rhsInt=0", "expInt=1"), + Iteration("int^identity", "lhsInt=2", "rhsInt=1", "expInt=2"), + Iteration("int^int", "lhsInt=2", "rhsInt=3", "expInt=8"), + Iteration("noncommutativity", "lhsInt=3", "rhsInt=2", "expInt=9"), + Iteration("-int^even int", "lhsInt=-2", "rhsInt=4", "expInt=16"), + Iteration("-int^odd int", "lhsInt=-2", "rhsInt=3", "expInt=-8") + ) + fun testPow_intAndInt_positivePower_returnsInt() { + val lhsReal = createIntegerReal(lhsInt) + val rhsReal = createIntegerReal(rhsInt) + + val result = lhsReal pow rhsReal + + // Integer raised to positive (or zero) integers always results in another integer. + assertThat(result).isIntegerThat().isEqualTo(expInt) + } + + @Test + @RunParameterized( + Iteration("int^-int", "lhsInt=2", "rhsInt=-3", "expFrac=1/8"), + Iteration("-int^-even int", "lhsInt=-2", "rhsInt=-4", "expFrac=1/16"), + Iteration("-int^-odd int", "lhsInt=-2", "rhsInt=-3", "expFrac=-1/8") + ) + fun testPow_intAndInt_negativePower_returnsFraction() { + val lhsReal = createIntegerReal(lhsInt) + val rhsReal = createIntegerReal(rhsInt) + + val result = lhsReal pow rhsReal + + // Integers raised to a negative integer yields a fraction since x^-y=1/(x^y). + val expectedFraction = parseFraction(expFrac) + assertThat(result).isRationalThat().isEqualTo(expectedFraction) + } + + @Test + @RunParameterized( + Iteration("0^0", "lhsInt=0", "rhsFrac=0/1", "expFrac=1"), + Iteration("identity^0", "lhsInt=1", "rhsFrac=0/1", "expFrac=1"), + Iteration("identity^identity", "lhsInt=1", "rhsFrac=1", "expFrac=1"), + Iteration("int^0", "lhsInt=2", "rhsFrac=0/1", "expFrac=1"), + Iteration("int^identity", "lhsInt=2", "rhsFrac=1", "expFrac=2"), + Iteration("int^fraction", "lhsInt=16", "rhsFrac=3/2", "expFrac=64"), + Iteration("int^wholeNumberFraction", "lhsInt=2", "rhsFrac=3/1", "expFrac=8"), + Iteration("noncommutativity", "lhsInt=3", "rhsFrac=2/1", "expFrac=9"), + Iteration("int^odd fraction", "lhsInt=8", "rhsFrac=5/3", "expFrac=32"), + Iteration("int^-fraction", "lhsInt=8", "rhsFrac=-4/2", "expFrac=1/64"), + Iteration("-int^odd fraction", "lhsInt=-8", "rhsFrac=5/3", "expFrac=-32"), + Iteration("-int^-fraction", "lhsInt=-4", "rhsFrac=-4/2", "expFrac=1/16"), + Iteration("-int^-odd fraction", "lhsInt=-8", "rhsFrac=-5/3", "expFrac=-1/32") + ) + fun testPow_intAndFraction_denominatorCanRootInt_returnsFraction() { + val lhsReal = createIntegerReal(lhsInt) + val rhsReal = createRationalReal(rhsFrac) + + val result = lhsReal pow rhsReal + + val expectedFraction = parseFraction(expFrac) + assertThat(result).isRationalThat().isEqualTo(expectedFraction) + } + + @Test + @RunParameterized( + Iteration("int^fraction", "lhsInt=3", "rhsFrac=2/3", "expDouble=2.080083823"), + Iteration("-int^fraction", "lhsInt=-4", "rhsFrac=2/3", "expDouble=2.5198421"), + Iteration("int^-fraction", "lhsInt=2", "rhsFrac=-2/3", "expDouble=0.629960525"), + Iteration("-int^-fraction", "lhsInt=-4", "rhsFrac=-2/3", "expDouble=0.396850263") + ) + fun testPow_intAndFraction_denominatorCannotRootInt_returnsDouble() { + val lhsReal = createIntegerReal(lhsInt) + val rhsReal = createRationalReal(rhsFrac) + + val result = lhsReal pow rhsReal + + assertThat(result).isIrrationalThat().isWithin(1e-5).of(expDouble) + } + + @Test + @RunParameterized( + Iteration("0^0", "lhsInt=0", "rhsDouble=0.0", "expDouble=1.0"), + Iteration("identity^0", "lhsInt=1", "rhsDouble=0.0", "expDouble=1.0"), + Iteration("identity^identity", "lhsInt=1", "rhsDouble=1.0", "expDouble=1.0"), + Iteration("int^0", "lhsInt=2", "rhsDouble=0.0", "expDouble=1.0"), + Iteration("int^identity", "lhsInt=2", "rhsDouble=1.0", "expDouble=2.0"), + Iteration("int^double", "lhsInt=2", "rhsDouble=3.14", "expDouble=8.815240927"), + Iteration("int^wholeNumberDouble", "lhsInt=2", "rhsDouble=3.0", "expDouble=8.0"), + Iteration("noncommutativity", "lhsInt=3", "rhsDouble=2.0", "expDouble=9.0"), + Iteration("int^-double", "lhsInt=2", "rhsDouble=-3.14", "expDouble=0.113439894") + ) + fun testPow_intAndDouble_returnsDouble() { + val lhsReal = createIntegerReal(lhsInt) + val rhsReal = createIrrationalReal(rhsDouble) + + val result = lhsReal pow rhsReal + + assertThat(result).isIrrationalThat().isWithin(1e-5).of(expDouble) + } + + @Test + @RunParameterized( + Iteration("0^0", "lhsFrac=0", "rhsInt=0", "expFrac=1"), + Iteration("identity^0", "lhsFrac=1", "rhsInt=0", "expFrac=1"), + Iteration("identity^identity", "lhsFrac=1", "rhsInt=1", "expFrac=1"), + Iteration("fraction^0", "lhsFrac=1/3", "rhsInt=0", "expFrac=1"), + Iteration("fraction^identity", "lhsFrac=1/3", "rhsInt=1", "expFrac=1/3"), + Iteration("fraction^int", "lhsFrac=2/3", "rhsInt=3", "expFrac=8/27"), + Iteration("wholeNumberFraction^int", "lhsFrac=3", "rhsInt=2", "expFrac=9"), + Iteration("noncommutativity", "lhsFrac=2", "rhsInt=3", "expFrac=8"), + Iteration("fraction^-int", "lhsFrac=4/3", "rhsInt=-2", "expFrac=9/16"), + Iteration("-fraction^int", "lhsFrac=-4/3", "rhsInt=2", "expFrac=1 7/9"), + Iteration("-fraction^-int", "lhsFrac=-4/3", "rhsInt=-2", "expFrac=9/16") + ) + fun testPow_fractionAndInt_returnsFraction() { + val lhsReal = createRationalReal(lhsFrac) + val rhsReal = createIntegerReal(rhsInt) + + val result = lhsReal pow rhsReal + + val expectedFraction = parseFraction(expFrac) + assertThat(result).isRationalThat().isEqualTo(expectedFraction) + } + + @Test + @RunParameterized( + Iteration("0^0", "lhsFrac=0", "rhsFrac=0", "expFrac=1"), + Iteration("identity^0", "lhsFrac=1", "rhsFrac=0", "expFrac=1"), + Iteration("identity^identity", "lhsFrac=1", "rhsFrac=1", "expFrac=1"), + Iteration("fraction^0", "lhsFrac=3/2", "rhsFrac=0", "expFrac=1"), + Iteration("fraction^identity", "lhsFrac=3/2", "rhsFrac=1", "expFrac=1 1/2"), + Iteration("fraction^fraction", "lhsFrac=32/243", "rhsFrac=3/5", "expFrac=8/27"), + Iteration("fraction^wholeNumberFraction", "lhsFrac=3", "rhsFrac=2", "expFrac=9"), + Iteration("noncommutativity", "lhsFrac=2", "rhsFrac=3", "expFrac=8"), + Iteration("fraction^-fraction", "lhsFrac=32/243", "rhsFrac=-3/5", "expFrac=3 3/8") + ) + fun testPow_fractionAndFraction_denominatorCanRootFraction_returnsFraction() { + val lhsReal = createRationalReal(lhsFrac) + val rhsReal = createRationalReal(rhsFrac) + + val result = lhsReal pow rhsReal + + val expectedFraction = parseFraction(expFrac) + assertThat(result).isRationalThat().isEqualTo(expectedFraction) + } + + @Test + @RunParameterized( + Iteration("fraction^fraction", "lhsFrac=3/2", "rhsFrac=2/3", "expDouble=1.310370697"), + Iteration("noncommutativity", "lhsFrac=2/3", "rhsFrac=3/2", "expDouble=0.544331054"), + Iteration("fraction^-fraction", "lhsFrac=3/2", "rhsFrac=-2/3", "expDouble=0.763142828") + ) + fun testPow_fractionAndFraction_denominatorCannotRootFraction_returnsDouble() { + val lhsReal = createRationalReal(lhsFrac) + val rhsReal = createRationalReal(rhsFrac) + + val result = lhsReal pow rhsReal + + assertThat(result).isIrrationalThat().isWithin(1e-5).of(expDouble) + } + + @Test + @RunParameterized( + Iteration("0^0", "lhsFrac=0", "rhsDouble=0.0", "expDouble=1.0"), + Iteration("identity^0", "lhsFrac=1", "rhsDouble=0.0", "expDouble=1.0"), + Iteration("identity^identity", "lhsFrac=1", "rhsDouble=1.0", "expDouble=1.0"), + Iteration("fraction^0", "lhsFrac=3/2", "rhsDouble=0.0", "expDouble=1.0"), + Iteration("fraction^identity", "lhsFrac=3/2", "rhsDouble=1.0", "expDouble=1.5"), + Iteration("fraction^double", "lhsFrac=3/2", "rhsDouble=3.14", "expDouble=3.572124224"), + Iteration("wholeNumberFraction^double", "lhsFrac=3", "rhsDouble=2.0", "expDouble=9.0"), + Iteration("noncommutativity", "lhsFrac=2", "rhsDouble=3.0", "expDouble=8.0"), + Iteration("fraction^-double", "lhsFrac=1 3/2", "rhsDouble=-3.14", "expDouble=0.056294812") + ) + fun testPow_fractionAndDouble_returnsDouble() { + val lhsReal = createRationalReal(lhsFrac) + val rhsReal = createIrrationalReal(rhsDouble) + + val result = lhsReal pow rhsReal + + assertThat(result).isIrrationalThat().isWithin(1e-5).of(expDouble) + } + + @Test + @RunParameterized( + Iteration("0^0", "lhsDouble=0.0", "rhsInt=0", "expDouble=1.0"), + Iteration("identity^0", "lhsDouble=1.0", "rhsInt=0", "expDouble=1.0"), + Iteration("identity^identity", "lhsDouble=1.0", "rhsInt=1", "expDouble=1.0"), + Iteration("double^0", "lhsDouble=3.14", "rhsInt=0", "expDouble=1.0"), + Iteration("double^identity", "lhsDouble=3.14", "rhsInt=1", "expDouble=3.14"), + Iteration("double^int", "lhsDouble=3.14", "rhsInt=2", "expDouble=9.8596"), + Iteration("wholeNumberDouble^int", "lhsDouble=3.0", "rhsInt=2", "expDouble=9.0"), + Iteration("noncommutativity", "lhsDouble=2.0", "rhsInt=3", "expDouble=8.0"), + Iteration("double^-int", "lhsDouble=3.14", "rhsInt=-3", "expDouble=0.032300635"), + Iteration("-double^int", "lhsDouble=-3.14", "rhsInt=3", "expDouble=-30.959144"), + Iteration("-double^-int", "lhsDouble=-3.14", "rhsInt=-3", "expDouble=-0.032300635") + ) + fun testPow_doubleAndInt_returnsDouble() { + val lhsReal = createIrrationalReal(lhsDouble) + val rhsReal = createIntegerReal(rhsInt) + + val result = lhsReal pow rhsReal + + assertThat(result).isIrrationalThat().isWithin(1e-5).of(expDouble) + } + + @Test + @RunParameterized( + Iteration("0^0", "lhsDouble=0.0", "rhsFrac=0/1", "expDouble=1.0"), + Iteration("identity^0", "lhsDouble=1.0", "rhsFrac=0/1", "expDouble=1.0"), + Iteration("identity^identity", "lhsDouble=1.0", "rhsFrac=1", "expDouble=1.0"), + Iteration("double^0", "lhsDouble=3.14", "rhsFrac=0/1", "expDouble=1.0"), + Iteration("double^identity", "lhsDouble=3.14", "rhsFrac=1", "expDouble=3.14"), + Iteration("double^fraction", "lhsDouble=3.14", "rhsFrac=3/2", "expDouble=5.564094176"), + Iteration("double^wholeNumberFraction", "lhsDouble=2.0", "rhsFrac=3/1", "expDouble=8.0"), + Iteration("noncommutativity", "lhsDouble=3.0", "rhsFrac=2/1", "expDouble=9.0"), + Iteration("double^-fraction", "lhsDouble=3.14", "rhsFrac=-3/2", "expDouble=0.179723773") + ) + fun testPow_doubleAndFraction_returnsDouble() { + val lhsReal = createIrrationalReal(lhsDouble) + val rhsReal = createRationalReal(rhsFrac) + + val result = lhsReal pow rhsReal + + assertThat(result).isIrrationalThat().isWithin(1e-5).of(expDouble) + } + + @Test + @RunParameterized( + Iteration("0^0", "lhsDouble=0.0", "rhsDouble=0.0", "expDouble=1.0"), + Iteration("identity^0", "lhsDouble=1.0", "rhsDouble=0.0", "expDouble=1.0"), + Iteration("identity^identity", "lhsDouble=1.0", "rhsDouble=1.0", "expDouble=1.0"), + Iteration("double^0", "lhsDouble=3.14", "rhsDouble=0.0", "expDouble=1.0"), + Iteration("double^identity", "lhsDouble=3.14", "rhsDouble=1.0", "expDouble=3.14"), + Iteration("double^double", "lhsDouble=3.14", "rhsDouble=2.7", "expDouble=21.963929943"), + Iteration("noncommutativity", "lhsDouble=2.7", "rhsDouble=3.14", "expDouble=22.619459311"), + Iteration("double^-double", "lhsDouble=3.14", "rhsDouble=-2.7", "expDouble=0.045529193") + ) + fun testPow_doubleAndDouble_returnsDouble() { + val lhsReal = createIrrationalReal(lhsDouble) + val rhsReal = createIrrationalReal(rhsDouble) + + val result = lhsReal pow rhsReal + + assertThat(result).isIrrationalThat().isWithin(1e-5).of(expDouble) + } + + @Test + fun testPow_negativeIntToOneHalfFraction_throwsException() { + val lhsReal = createIntegerReal(-3) + val rhsReal = createRationalReal(ONE_HALF_FRACTION) + + val exception = assertThrows(IllegalStateException::class) { lhsReal pow rhsReal } + + assertThat(exception).hasMessageThat().contains("Radicand results in imaginary number") + } + + @Test + fun testPow_negativeIntToNonzeroDouble_returnsNotANumber() { + val lhsReal = createIntegerReal(-3) + val rhsReal = createIrrationalReal(3.14) + + val result = lhsReal pow rhsReal + + assertThat(result).isIrrationalThat().isNaN() + } + + @Test + fun testPow_negativeFractionToOneHalfFraction_throwsException() { + val lhsReal = NEGATIVE_ONE_AND_ONE_HALF_REAL + val rhsReal = createRationalReal(ONE_HALF_FRACTION) + + val exception = assertThrows(IllegalStateException::class) { lhsReal pow rhsReal } + + assertThat(exception).hasMessageThat().contains("Radicand results in imaginary number") + } + + @Test + fun testPow_negativeFractionToNegativeFractionWithOddNumerator_throwsException() { + val lhsReal = createRationalReal((-4).toWholeNumberFraction()) + val rhsReal = createRationalReal(-ONE_AND_ONE_HALF_FRACTION) + + val exception = assertThrows(IllegalStateException::class) { lhsReal pow rhsReal } + + assertThat(exception).hasMessageThat().contains("Radicand results in imaginary number") + } + + @Test + fun testPow_negativeFractionToNonzeroDouble_returnsNotANumber() { + val lhsReal = NEGATIVE_ONE_AND_ONE_HALF_REAL + val rhsReal = createIrrationalReal(3.14) + + val result = lhsReal pow rhsReal + + assertThat(result).isIrrationalThat().isNaN() + } + + @Test + fun testPow_negativeDoubleToOneHalfFraction_returnsNotANumber() { + val lhsReal = createIrrationalReal(-2.7) + val rhsReal = createRationalReal(ONE_HALF_FRACTION) + + val result = lhsReal pow rhsReal + + assertThat(result).isIrrationalThat().isNaN() + } + + @Test + fun testPow_negativeDoubleToNonzeroDouble_returnsNotANumber() { + val lhsReal = createIrrationalReal(-2.7) + val rhsReal = createIrrationalReal(3.14) + + val result = lhsReal pow rhsReal + + assertThat(result).isIrrationalThat().isNaN() + } + + /* End operator tests. */ + + @Test + fun testSqrt_defaultReal_throwsException() { + val real = Real.getDefaultInstance() + + val exception = assertThrows(IllegalStateException::class) { sqrt(real) } + + assertThat(exception).hasMessageThat().contains("Invalid real") + } + + @Test + fun testSqrt_negativeInteger_throwsException() { + val real = createIntegerReal(-2) + + val exception = assertThrows(IllegalStateException::class) { sqrt(real) } + + assertThat(exception).hasMessageThat().contains("Radicand results in imaginary number") + } + + @Test + fun testSqrt_zeroInteger_returnsZeroInteger() { + val real = createIntegerReal(0) + + val result = sqrt(real) + + assertThat(result).isIntegerThat().isEqualTo(0) + } + + @Test + fun testSqrt_fourInteger_returnsTwoInteger() { + val real = createIntegerReal(4) + + val result = sqrt(real) + + assertThat(result).isIntegerThat().isEqualTo(2) + } + + @Test + fun testSqrt_fourTwo_returnsSqrtTwoDouble() { + val real = createIntegerReal(2) + + val result = sqrt(real) + + assertThat(result).isIrrationalThat().isWithin(1e-5).of(1.414213562) + } + + @Test + fun testSqrt_negativeFraction_throwsException() { + val real = createRationalReal((-2).toWholeNumberFraction()) + + val exception = assertThrows(IllegalStateException::class) { sqrt(real) } + + assertThat(exception).hasMessageThat().contains("Radicand results in imaginary number") + } + + @Test + fun testSqrt_zeroFraction_returnZeroFraction() { + val real = createRationalReal(ZERO_FRACTION) + + val result = sqrt(real) + + assertThat(result).isRationalThat().isEqualTo(ZERO_FRACTION) + } + + @Test + fun testSqrt_fourFraction_returnsTwoFraction() { + val real = createRationalReal(4.toWholeNumberFraction()) + + val result = sqrt(real) + + assertThat(result).isRationalThat().isEqualTo(2.toWholeNumberFraction()) + } + + @Test + fun testSqrt_oneFourthFraction_returnsOneHalfFraction() { + val real = createRationalReal(ONE_FOURTH_FRACTION) + + val result = sqrt(real) + + assertThat(result).isRationalThat().isEqualTo(ONE_HALF_FRACTION) + } + + @Test + fun testSqrt_sixteenthNinthsFraction_returnsOneAndOneThirdFraction() { + val real = createRationalReal(createFraction(numerator = 16, denominator = 9)) + + val result = sqrt(real) + + // Verify that both the numerator and denominator are properly rooted, and that a proper + // fraction is returned. + assertThat(result).isRationalThat().apply { + hasNegativePropertyThat().isFalse() + hasWholeNumberThat().isEqualTo(1) + hasNumeratorThat().isEqualTo(1) + hasDenominatorThat().isEqualTo(3) + } + } + + @Test + fun testSqrt_twoThirdsFraction_returnsComputedDouble() { + val real = createRationalReal(createFraction(numerator = 2, denominator = 3)) + + val result = sqrt(real) + + // sqrt(2/3) can't be computed perfectly, so a double must be computed, instead. + assertThat(result).isIrrationalThat().isWithin(1e-5).of(0.816496581) + } + + @Test + fun testSqrt_negativeDouble_returnsNotANumber() { + val real = createIrrationalReal(-2.7) + + val result = sqrt(real) + + assertThat(result).isIrrationalThat().isNaN() + } + + @Test + fun testSqrt_zeroDouble_returnsZeroDouble() { + val real = createIrrationalReal(0.0) + + val result = sqrt(real) + + assertThat(result).isIrrationalThat().isWithin(1e-5).of(0.0) + } + + @Test + fun testSqrt_fourDouble_returnsTwoDouble() { + val real = createIrrationalReal(4.0) + + val result = sqrt(real) + + assertThat(result).isIrrationalThat().isWithin(1e-5).of(2.0) + } + + @Test + fun testSqrt_twoDouble_returnsRootTwoDouble() { + val real = createIrrationalReal(2.0) + + val result = sqrt(real) + + assertThat(result).isIrrationalThat().isWithin(1e-5).of(1.414213562) + } + + @Test + fun testSqrt_nonWholeDouble_returnsCorrectSquareRootDouble() { + val real = createIrrationalReal(3.14) + + val result = sqrt(real) + + assertThat(result).isIrrationalThat().isWithin(1e-5).of(1.772004515) + } } private fun createIntegerReal(value: Int) = Real.newBuilder().apply { integer = value }.build() +private fun createRationalReal(rawFractionExpression: String) = + createRationalReal(parseFraction(rawFractionExpression)) + private fun createRationalReal(value: Fraction) = Real.newBuilder().apply { rational = value }.build() @@ -412,3 +1788,8 @@ private fun createRationalReal(value: Fraction) = Real.newBuilder().apply { private fun createIrrationalReal(value: Double) = Real.newBuilder().apply { irrational = value }.build() + +private fun createFraction(numerator: Int, denominator: Int) = Fraction.newBuilder().apply { + this.numerator = numerator + this.denominator = denominator +}.build() From 19a64251d92dcf1f268fd61d6635e2736228dda4 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 26 Jan 2022 15:47:28 -0800 Subject: [PATCH 064/162] Split StringToFractionParser. This is a temporary change that will be finished upstream (since there's an earlier PR that's a better fit for this change). --- .../app/parser/StringToFractionParser.kt | 65 ++----------------- domain/BUILD.bazel | 1 + .../org/oppia/android/domain/util/BUILD.bazel | 1 - .../oppia/android/util/extensions/BUILD.bazel | 8 +++ .../util/extensions}/StringExtensions.kt | 0 5 files changed, 16 insertions(+), 59 deletions(-) rename {domain/src/main/java/org/oppia/android/domain/util => utility/src/main/java/org/oppia/android/util/extensions}/StringExtensions.kt (100%) diff --git a/app/src/main/java/org/oppia/android/app/parser/StringToFractionParser.kt b/app/src/main/java/org/oppia/android/app/parser/StringToFractionParser.kt index 62fc2e9a282..0b806b263d7 100644 --- a/app/src/main/java/org/oppia/android/app/parser/StringToFractionParser.kt +++ b/app/src/main/java/org/oppia/android/app/parser/StringToFractionParser.kt @@ -5,30 +5,25 @@ import org.oppia.android.R import org.oppia.android.app.model.Fraction import org.oppia.android.app.translation.AppLanguageResourceHandler import org.oppia.android.domain.util.normalizeWhitespace +import org.oppia.android.util.math.FractionParser /** This class contains method that helps to parse string to fraction. */ class StringToFractionParser { - private val wholeNumberOnlyRegex = - """^-? ?(\d+)$""".toRegex() - private val fractionOnlyRegex = - """^-? ?(\d+) ?/ ?(\d+)$""".toRegex() - private val mixedNumberRegex = - """^-? ?(\d+) (\d+) ?/ ?(\d+)$""".toRegex() - private val invalidCharsRegex = - """^[\d\s/-]+$""".toRegex() + private val invalidCharsRegex = """^[\d\s/-]+$""".toRegex() private val invalidCharsLengthRegex = "\\d{8,}".toRegex() /** * Returns a [FractionParsingError] for the specified text input if it's an invalid fraction, or * [FractionParsingError.VALID] if no issues are found. Note that a valid fraction returned by - * this method is guaranteed to be parsed correctly by [parseRegularFraction]. + * this method is guaranteed to be parsed correctly by [parseFraction]. * * This method should only be used when a user tries submitting an answer. Real-time error * detection should be done using [getRealTimeAnswerError], instead. */ fun getSubmitTimeError(text: String): FractionParsingError { - if (invalidCharsLengthRegex.find(text) != null) + if (invalidCharsLengthRegex.find(text) != null) { return FractionParsingError.NUMBER_TOO_LONG + } val fraction = parseFraction(text) return when { fraction == null -> FractionParsingError.INVALID_FORMAT @@ -57,56 +52,10 @@ class StringToFractionParser { } /** Returns a [Fraction] parse from the specified raw text string. */ - fun parseFraction(text: String): Fraction? { - // Normalize whitespace to ensure that answer follows a simpler subset of possible patterns. - val inputText: String = text.normalizeWhitespace() - return parseMixedNumber(inputText) - ?: parseRegularFraction(inputText) - ?: parseWholeNumber(inputText) - } + fun parseFraction(text: String): Fraction? = FractionParser.tryParseFraction(text) /** Returns a [Fraction] parse from the specified raw text string. */ - fun parseFractionFromString(text: String): Fraction { - return parseFraction(text) - ?: throw IllegalArgumentException("Incorrectly formatted fraction: $text") - } - - private fun parseMixedNumber(inputText: String): Fraction? { - val mixedNumberMatch = mixedNumberRegex.matchEntire(inputText) ?: return null - val (_, wholeNumberText, numeratorText, denominatorText) = - mixedNumberMatch.groupValues - return Fraction.newBuilder() - .setIsNegative(isInputNegative(inputText)) - .setWholeNumber(wholeNumberText.toInt()) - .setNumerator(numeratorText.toInt()) - .setDenominator(denominatorText.toInt()) - .build() - } - - private fun parseRegularFraction(inputText: String): Fraction? { - val fractionOnlyMatch = fractionOnlyRegex.matchEntire(inputText) ?: return null - val (_, numeratorText, denominatorText) = fractionOnlyMatch.groupValues - // Fraction-only numbers imply no whole number. - return Fraction.newBuilder() - .setIsNegative(isInputNegative(inputText)) - .setNumerator(numeratorText.toInt()) - .setDenominator(denominatorText.toInt()) - .build() - } - - private fun parseWholeNumber(inputText: String): Fraction? { - val wholeNumberMatch = wholeNumberOnlyRegex.matchEntire(inputText) ?: return null - val (_, wholeNumberText) = wholeNumberMatch.groupValues - // Whole number fractions imply '0/1' fractional parts. - return Fraction.newBuilder() - .setIsNegative(isInputNegative(inputText)) - .setWholeNumber(wholeNumberText.toInt()) - .setNumerator(0) - .setDenominator(1) - .build() - } - - private fun isInputNegative(inputText: String): Boolean = inputText.startsWith("-") + fun parseFractionFromString(text: String): Fraction = FractionParser.parseFraction(text) /** Enum to store the errors of [FractionInputInteractionView]. */ enum class FractionParsingError(@StringRes private var error: Int?) { diff --git a/domain/BUILD.bazel b/domain/BUILD.bazel index 6d15ff50d73..49bdc4dbf0c 100755 --- a/domain/BUILD.bazel +++ b/domain/BUILD.bazel @@ -122,6 +122,7 @@ kt_android_library( "//utility/src/main/java/org/oppia/android/util/data:data_providers", "//utility/src/main/java/org/oppia/android/util/extensions:bundle_extensions", "//utility/src/main/java/org/oppia/android/util/extensions:context_extensions", + "//utility/src/main/java/org/oppia/android/util/extensions:string_extensions", "//utility/src/main/java/org/oppia/android/util/logging:event_logger", "//utility/src/main/java/org/oppia/android/util/logging:log_uploader", "//utility/src/main/java/org/oppia/android/util/math:extensions", diff --git a/domain/src/main/java/org/oppia/android/domain/util/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/util/BUILD.bazel index 7c1dc1e6ce2..de927b14eaf 100644 --- a/domain/src/main/java/org/oppia/android/domain/util/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/util/BUILD.bazel @@ -23,7 +23,6 @@ kt_android_library( srcs = [ "InteractionObjectExtensions.kt", "JsonExtensions.kt", - "StringExtensions.kt", "WorkDataExtensions.kt", ], visibility = ["//domain:__subpackages__"], diff --git a/utility/src/main/java/org/oppia/android/util/extensions/BUILD.bazel b/utility/src/main/java/org/oppia/android/util/extensions/BUILD.bazel index 525f5160210..cfc2be6bb6a 100644 --- a/utility/src/main/java/org/oppia/android/util/extensions/BUILD.bazel +++ b/utility/src/main/java/org/oppia/android/util/extensions/BUILD.bazel @@ -25,3 +25,11 @@ kt_android_library( "//third_party:com_google_protobuf_protobuf-javalite", ], ) + +kt_android_library( + name = "string_extensions", + srcs = [ + "StringExtensions.kt", + ], + visibility = ["//:oppia_api_visibility"], +) diff --git a/domain/src/main/java/org/oppia/android/domain/util/StringExtensions.kt b/utility/src/main/java/org/oppia/android/util/extensions/StringExtensions.kt similarity index 100% rename from domain/src/main/java/org/oppia/android/domain/util/StringExtensions.kt rename to utility/src/main/java/org/oppia/android/util/extensions/StringExtensions.kt From 7a364c39eb9c996766e0e201d89b6ae66de05fc3 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 26 Jan 2022 16:16:12 -0800 Subject: [PATCH 065/162] Address reviewer comments + other stuff. This also fixes a typo and incorrectly ordered exemptions list I noticed during development of downstream PRs. --- model/src/main/proto/math.proto | 6 +++--- scripts/assets/test_file_exemptions.textproto | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/model/src/main/proto/math.proto b/model/src/main/proto/math.proto index a8af5ba5bec..d648acad614 100644 --- a/model/src/main/proto/math.proto +++ b/model/src/main/proto/math.proto @@ -36,7 +36,7 @@ message Real { // decimal values need to be treated as irrational and non-factorable. double irrational = 2; - // Indicates that thi sreal value is an integer (as a special case of rational values since + // Indicates that this real value is an integer (as a special case of rational values since // integers are easier to work with than fraction objects). Note that this isn't the only case // where the real value can be an integer. It can also be an integer double value, or a fraction // with only a whole number component. @@ -60,12 +60,12 @@ message RatioExpression { // Values of this proto can be analyzed using MathExpressionSubject. message MathExpression { // The index within the input text stream at which point the expression starts (it's an inclusive - // index). If both this and the end index are zero then no parsing information is included for + // index). If both this and the end index are the same then no parsing information is included for // this specific expression. uint32 parse_start_index = 1; // The index within the input text stream at which point the expression ends, exclusively. If both - // this and the start index are zero then no parsing information is included for this specific + // this and the start index are the same then no parsing information is included for this specific // expression. uint32 parse_end_index = 2; diff --git a/scripts/assets/test_file_exemptions.textproto b/scripts/assets/test_file_exemptions.textproto index 6d52e0b504a..d852a048e85 100644 --- a/scripts/assets/test_file_exemptions.textproto +++ b/scripts/assets/test_file_exemptions.textproto @@ -647,9 +647,9 @@ exempted_file_path: "testing/src/main/java/org/oppia/android/testing/espresso/Im exempted_file_path: "testing/src/main/java/org/oppia/android/testing/espresso/KonfettiViewMatcher.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/junit/DefineAppLanguageLocaleContext.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/FractionSubject.kt" -exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/RealSubject.kt" -exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/MathEquationSubject.kt" +exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt" +exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/RealSubject.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/mockito/MockitoKotlinHelper.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/network/ApiMockLoader.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/network/MockClassroomService.kt" From cfe6cab959f04da6c8f23be9cfb95e34068d6dcb Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 26 Jan 2022 18:11:14 -0800 Subject: [PATCH 066/162] Move StringExtensions & fraction parsing. This splits fraction parsing between UI & utility components. --- app/BUILD.bazel | 3 +- .../FractionInputInteractionView.kt | 2 + .../app/parser/FractionParsingUiError.kt | 36 ++ .../app/parser/StringToNumberParser.kt | 2 +- .../android/app/parser/StringToRatioParser.kt | 4 +- .../FractionInteractionViewModel.kt | 23 +- .../InputInteractionViewTestActivityTest.kt | 2 + .../app/parser/FractionParsingUiErrorTest.kt | 271 ++++++++++ .../app/parser/StringToFractionParserTest.kt | 509 ------------------ domain/BUILD.bazel | 1 + ...TextInputContainsRuleClassifierProvider.kt | 2 +- .../TextInputEqualsRuleClassifierProvider.kt | 2 +- ...tInputFuzzyEqualsRuleClassifierProvider.kt | 2 +- ...xtInputStartsWithRuleClassifierProvider.kt | 2 +- .../org/oppia/android/domain/util/BUILD.bazel | 1 - .../domain/util/StringExtensionsTest.kt | 2 + .../oppia/android/util/extensions/BUILD.bazel | 8 + .../util/extensions}/StringExtensions.kt | 2 +- .../org/oppia/android/util/math/BUILD.bazel | 12 + .../oppia/android/util/math/FractionParser.kt | 43 +- .../org/oppia/android/util/math/BUILD.bazel | 18 + .../android/util/math/FractionParserTest.kt | 280 ++++++++++ 22 files changed, 681 insertions(+), 546 deletions(-) create mode 100644 app/src/main/java/org/oppia/android/app/parser/FractionParsingUiError.kt create mode 100644 app/src/test/java/org/oppia/android/app/parser/FractionParsingUiErrorTest.kt delete mode 100644 app/src/test/java/org/oppia/android/app/parser/StringToFractionParserTest.kt rename {domain/src/main/java/org/oppia/android/domain/util => utility/src/main/java/org/oppia/android/util/extensions}/StringExtensions.kt (92%) rename app/src/main/java/org/oppia/android/app/parser/StringToFractionParser.kt => utility/src/main/java/org/oppia/android/util/math/FractionParser.kt (80%) create mode 100644 utility/src/test/java/org/oppia/android/util/math/FractionParserTest.kt diff --git a/app/BUILD.bazel b/app/BUILD.bazel index db1015977b4..f170cc195ae 100644 --- a/app/BUILD.bazel +++ b/app/BUILD.bazel @@ -189,7 +189,7 @@ VIEW_MODELS_WITH_RESOURCE_IMPORTS = [ "src/main/java/org/oppia/android/app/onboarding/OnboardingViewModel.kt", "src/main/java/org/oppia/android/app/ongoingtopiclist/OngoingTopicItemViewModel.kt", "src/main/java/org/oppia/android/app/options/TextSizeItemViewModel.kt", - "src/main/java/org/oppia/android/app/parser/StringToFractionParser.kt", + "src/main/java/org/oppia/android/app/parser/FractionParsingUiError.kt", "src/main/java/org/oppia/android/app/parser/StringToNumberParser.kt", "src/main/java/org/oppia/android/app/parser/StringToRatioParser.kt", "src/main/java/org/oppia/android/app/player/state/itemviewmodel/DragDropInteractionContentViewModel.kt", @@ -658,6 +658,7 @@ kt_android_library( "//utility/src/main/java/org/oppia/android/util/extensions:context_extensions", "//utility/src/main/java/org/oppia/android/util/logging/firebase:debug_event_logger", "//utility/src/main/java/org/oppia/android/util/logging/firebase:debug_module", + "//utility/src/main/java/org/oppia/android/util/math:fraction_parser", # TODO(#59): Remove 'debug_util_module' once we completely migrate to Bazel from Gradle as # we can then directly exclude debug files from the build and thus won't be requiring this module. "//utility/src/main/java/org/oppia/android/util/networking:debug_util_module", diff --git a/app/src/main/java/org/oppia/android/app/customview/interaction/FractionInputInteractionView.kt b/app/src/main/java/org/oppia/android/app/customview/interaction/FractionInputInteractionView.kt index 6209a6c269e..49972bda253 100644 --- a/app/src/main/java/org/oppia/android/app/customview/interaction/FractionInputInteractionView.kt +++ b/app/src/main/java/org/oppia/android/app/customview/interaction/FractionInputInteractionView.kt @@ -20,6 +20,8 @@ import org.oppia.android.app.utility.KeyboardHelper.Companion.showSoftKeyboard // background="@drawable/edit_text_background" // maxLength="200". +// TODO(#4135): Add a dedicated test suite for this class. + /** The custom EditText class for fraction input interaction view. */ class FractionInputInteractionView @JvmOverloads constructor( context: Context, diff --git a/app/src/main/java/org/oppia/android/app/parser/FractionParsingUiError.kt b/app/src/main/java/org/oppia/android/app/parser/FractionParsingUiError.kt new file mode 100644 index 00000000000..fb1991cca59 --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/parser/FractionParsingUiError.kt @@ -0,0 +1,36 @@ +package org.oppia.android.app.parser + +import androidx.annotation.StringRes +import org.oppia.android.R +import org.oppia.android.app.translation.AppLanguageResourceHandler +import org.oppia.android.util.math.FractionParser.FractionParsingError + +/** Enum to store the errors of [FractionInputInteractionView]. */ +enum class FractionParsingUiError(@StringRes private var error: Int?) { + VALID(error = null), + INVALID_CHARS(error = R.string.fraction_error_invalid_chars), + INVALID_FORMAT(error = R.string.fraction_error_invalid_format), + DIVISION_BY_ZERO(error = R.string.fraction_error_divide_by_zero), + NUMBER_TOO_LONG(error = R.string.fraction_error_larger_than_seven_digits); + + /** + * Returns the string corresponding to this error's string resources, or null if there is none. + */ + fun getErrorMessageFromStringRes(resourceHandler: AppLanguageResourceHandler): String? = + error?.let(resourceHandler::getStringInLocale) + + companion object { + /** + * Returns the [FractionParsingUiError] corresponding to the specified [FractionParsingError]. + */ + fun createFromParsingError(parsingError: FractionParsingError): FractionParsingUiError { + return when (parsingError) { + FractionParsingError.VALID -> VALID + FractionParsingError.INVALID_CHARS -> INVALID_CHARS + FractionParsingError.INVALID_FORMAT -> INVALID_FORMAT + FractionParsingError.DIVISION_BY_ZERO -> DIVISION_BY_ZERO + FractionParsingError.NUMBER_TOO_LONG -> NUMBER_TOO_LONG + } + } + } +} diff --git a/app/src/main/java/org/oppia/android/app/parser/StringToNumberParser.kt b/app/src/main/java/org/oppia/android/app/parser/StringToNumberParser.kt index 3670e2fd16c..5b625623ad2 100644 --- a/app/src/main/java/org/oppia/android/app/parser/StringToNumberParser.kt +++ b/app/src/main/java/org/oppia/android/app/parser/StringToNumberParser.kt @@ -3,7 +3,7 @@ package org.oppia.android.app.parser import androidx.annotation.StringRes import org.oppia.android.R import org.oppia.android.app.translation.AppLanguageResourceHandler -import org.oppia.android.domain.util.normalizeWhitespace +import org.oppia.android.util.extensions.normalizeWhitespace /** * This class contains methods that help to parse string to number, check realtime and submit time diff --git a/app/src/main/java/org/oppia/android/app/parser/StringToRatioParser.kt b/app/src/main/java/org/oppia/android/app/parser/StringToRatioParser.kt index 1d2562fa73a..31895402263 100644 --- a/app/src/main/java/org/oppia/android/app/parser/StringToRatioParser.kt +++ b/app/src/main/java/org/oppia/android/app/parser/StringToRatioParser.kt @@ -4,8 +4,8 @@ import androidx.annotation.StringRes import org.oppia.android.R import org.oppia.android.app.model.RatioExpression import org.oppia.android.app.translation.AppLanguageResourceHandler -import org.oppia.android.domain.util.normalizeWhitespace -import org.oppia.android.domain.util.removeWhitespace +import org.oppia.android.util.extensions.normalizeWhitespace +import org.oppia.android.util.extensions.removeWhitespace /** * Utility for parsing [RatioExpression]s from strings and validating strings can be parsed diff --git a/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/FractionInteractionViewModel.kt b/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/FractionInteractionViewModel.kt index b2e2329cefd..8ce11c53e71 100644 --- a/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/FractionInteractionViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/FractionInteractionViewModel.kt @@ -9,12 +9,13 @@ import org.oppia.android.app.model.Interaction import org.oppia.android.app.model.InteractionObject import org.oppia.android.app.model.UserAnswer import org.oppia.android.app.model.WrittenTranslationContext -import org.oppia.android.app.parser.StringToFractionParser +import org.oppia.android.app.parser.FractionParsingUiError import org.oppia.android.app.player.state.answerhandling.AnswerErrorCategory import org.oppia.android.app.player.state.answerhandling.InteractionAnswerErrorOrAvailabilityCheckReceiver import org.oppia.android.app.player.state.answerhandling.InteractionAnswerHandler import org.oppia.android.app.translation.AppLanguageResourceHandler import org.oppia.android.domain.translation.TranslationController +import org.oppia.android.util.math.FractionParser /** [StateItemViewModel] for the fraction input interaction. */ class FractionInteractionViewModel( @@ -32,7 +33,7 @@ class FractionInteractionViewModel( var errorMessage = ObservableField("") val hintText: CharSequence = deriveHintText(interaction) - private val stringToFractionParser: StringToFractionParser = StringToFractionParser() + private val fractionParser = FractionParser() init { val callback: Observable.OnPropertyChangedCallback = @@ -52,7 +53,7 @@ class FractionInteractionViewModel( if (answerText.isNotEmpty()) { val answerTextString = answerText.toString() answer = InteractionObject.newBuilder().apply { - fraction = stringToFractionParser.parseFractionFromString(answerTextString) + fraction = fractionParser.parseFractionFromString(answerTextString) }.build() plainAnswer = answerTextString this.writtenTranslationContext = this@FractionInteractionViewModel.writtenTranslationContext @@ -63,14 +64,18 @@ class FractionInteractionViewModel( override fun checkPendingAnswerError(category: AnswerErrorCategory): String? { if (answerText.isNotEmpty()) { when (category) { - AnswerErrorCategory.REAL_TIME -> + AnswerErrorCategory.REAL_TIME -> { pendingAnswerError = - stringToFractionParser.getRealTimeAnswerError(answerText.toString()) - .getErrorMessageFromStringRes(resourceHandler) - AnswerErrorCategory.SUBMIT_TIME -> + FractionParsingUiError.createFromParsingError( + fractionParser.getRealTimeAnswerError(answerText.toString()) + ).getErrorMessageFromStringRes(resourceHandler) + } + AnswerErrorCategory.SUBMIT_TIME -> { pendingAnswerError = - stringToFractionParser.getSubmitTimeError(answerText.toString()) - .getErrorMessageFromStringRes(resourceHandler) + FractionParsingUiError.createFromParsingError( + fractionParser.getSubmitTimeError(answerText.toString()) + ).getErrorMessageFromStringRes(resourceHandler) + } } errorMessage.set(pendingAnswerError) } diff --git a/app/src/sharedTest/java/org/oppia/android/app/testing/InputInteractionViewTestActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/testing/InputInteractionViewTestActivityTest.kt index d9100849877..a154034f6dc 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/testing/InputInteractionViewTestActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/testing/InputInteractionViewTestActivityTest.kt @@ -122,6 +122,8 @@ class InputInteractionViewTestActivityTest { ApplicationProvider.getApplicationContext().inject(this) } + // TODO(#4135): Move fraction input tests to a dedicated test suite. + @Test fun testFractionInput_withNoInput_hasCorrectPendingAnswerType() { val activityScenario = ActivityScenario.launch( diff --git a/app/src/test/java/org/oppia/android/app/parser/FractionParsingUiErrorTest.kt b/app/src/test/java/org/oppia/android/app/parser/FractionParsingUiErrorTest.kt new file mode 100644 index 00000000000..0342d7e3539 --- /dev/null +++ b/app/src/test/java/org/oppia/android/app/parser/FractionParsingUiErrorTest.kt @@ -0,0 +1,271 @@ +package org.oppia.android.app.parser + +import android.app.Application +import androidx.appcompat.app.AppCompatActivity +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.rules.ActivityScenarioRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import dagger.Component +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.oppia.android.app.activity.ActivityComponent +import org.oppia.android.app.activity.ActivityComponentFactory +import org.oppia.android.app.application.ApplicationComponent +import org.oppia.android.app.application.ApplicationInjector +import org.oppia.android.app.application.ApplicationInjectorProvider +import org.oppia.android.app.application.ApplicationModule +import org.oppia.android.app.application.ApplicationStartupListenerModule +import org.oppia.android.app.devoptions.DeveloperOptionsModule +import org.oppia.android.app.devoptions.DeveloperOptionsStarterModule +import org.oppia.android.app.shim.ViewBindingShimModule +import org.oppia.android.app.testing.activity.TestActivity +import org.oppia.android.app.topic.PracticeTabModule +import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule +import org.oppia.android.data.backends.gae.NetworkConfigProdModule +import org.oppia.android.data.backends.gae.NetworkModule +import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule +import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule +import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule +import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule +import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule +import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule +import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule +import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule +import org.oppia.android.domain.exploration.lightweightcheckpointing.ExplorationStorageModule +import org.oppia.android.domain.hintsandsolution.HintsAndSolutionConfigModule +import org.oppia.android.domain.hintsandsolution.HintsAndSolutionProdModule +import org.oppia.android.domain.onboarding.ExpirationMetaDataRetrieverModule +import org.oppia.android.domain.oppialogger.LogStorageModule +import org.oppia.android.domain.oppialogger.loguploader.LogUploadWorkerModule +import org.oppia.android.domain.platformparameter.PlatformParameterModule +import org.oppia.android.domain.platformparameter.PlatformParameterSingletonModule +import org.oppia.android.domain.question.QuestionModule +import org.oppia.android.domain.topic.PrimeTopicAssetsControllerModule +import org.oppia.android.domain.workmanager.WorkManagerConfigurationModule +import org.oppia.android.testing.TestLogReportingModule +import org.oppia.android.testing.junit.InitializeDefaultLocaleRule +import org.oppia.android.testing.robolectric.RobolectricModule +import org.oppia.android.testing.threading.TestDispatcherModule +import org.oppia.android.testing.time.FakeOppiaClockModule +import org.oppia.android.util.accessibility.AccessibilityTestModule +import org.oppia.android.util.caching.AssetModule +import org.oppia.android.util.caching.testing.CachingTestModule +import org.oppia.android.util.gcsresource.GcsResourceModule +import org.oppia.android.util.locale.LocaleProdModule +import org.oppia.android.util.logging.LoggerModule +import org.oppia.android.util.logging.firebase.FirebaseLogUploaderModule +import org.oppia.android.util.math.FractionParser +import org.oppia.android.util.math.FractionParser.FractionParsingError +import org.oppia.android.util.networking.NetworkConnectionDebugUtilModule +import org.oppia.android.util.networking.NetworkConnectionUtilDebugModule +import org.oppia.android.util.parser.html.HtmlParserEntityTypeModule +import org.oppia.android.util.parser.image.GlideImageLoaderModule +import org.oppia.android.util.parser.image.ImageParsingModule +import org.robolectric.annotation.Config +import org.robolectric.annotation.LooperMode +import javax.inject.Singleton + +/** Tests for [FractionParsingUiError]. */ +@RunWith(AndroidJUnit4::class) +@LooperMode(LooperMode.Mode.PAUSED) +@Config(application = FractionParsingUiErrorTest.TestApplication::class, qualifiers = "port-xxhdpi") +class FractionParsingUiErrorTest { + @get:Rule + val initializeDefaultLocaleRule = InitializeDefaultLocaleRule() + + @get:Rule + var activityRule = + ActivityScenarioRule( + TestActivity.createIntent(ApplicationProvider.getApplicationContext()) + ) + + private lateinit var fractionParser: FractionParser + + @Before + fun setUp() { + setUpTestApplicationComponent() + fractionParser = FractionParser() + } + + @Test + fun testSubmitTimeError_validMixedNumber_noErrorMessage() { + activityRule.scenario.onActivity { activity -> + val errorMessage = fractionParser.getSubmitTimeError("11 22/33") + .toUiError() + .getErrorMessageFromStringRes(activity.appLanguageResourceHandler) + assertThat(errorMessage).isNull() + } + } + + @Test + fun testSubmitTimeError_tenDigitNumber_numberTooLong_hasRelevantErrorMessage() { + activityRule.scenario.onActivity { activity -> + val errorMessage = fractionParser.getSubmitTimeError("0123456789") + .toUiError() + .getErrorMessageFromStringRes(activity.appLanguageResourceHandler) + assertThat(errorMessage) + .isEqualTo("None of the numbers in the fraction should have more than 7 digits.") + } + } + + @Test + fun testSubmitTimeError_nonDigits_invalidFormat_hasRelevantErrorMessage() { + activityRule.scenario.onActivity { activity -> + val errorMessage = fractionParser.getSubmitTimeError("jdhfc") + .toUiError() + .getErrorMessageFromStringRes(activity.appLanguageResourceHandler) + assertThat(errorMessage) + .isEqualTo("Please enter a valid fraction (e.g., 5/3 or 1 2/3)") + } + } + + @Test + fun testSubmitTimeError_divisionByZero_hasRelevantErrorMessage() { + activityRule.scenario.onActivity { activity -> + val errorMessage = fractionParser.getSubmitTimeError("123/0") + .toUiError() + .getErrorMessageFromStringRes(activity.appLanguageResourceHandler) + assertThat(errorMessage).isEqualTo("Please do not put 0 in the denominator") + } + } + + @Test + fun testSubmitTimeError_ambiguousSpacing_invalidFormat_hasRelevantErrorMessage() { + activityRule.scenario.onActivity { activity -> + val errorMessage = fractionParser.getSubmitTimeError("1 2 3/4") + .toUiError() + .getErrorMessageFromStringRes(activity.appLanguageResourceHandler) + assertThat(errorMessage) + .isEqualTo("Please enter a valid fraction (e.g., 5/3 or 1 2/3)") + } + } + + @Test + fun testSubmitTimeError_emptyString_invalidFormat_hasRelevantErrorMessage() { + activityRule.scenario.onActivity { activity -> + val errorMessage = fractionParser.getSubmitTimeError("") + .toUiError() + .getErrorMessageFromStringRes(activity.appLanguageResourceHandler) + assertThat(errorMessage) + .isEqualTo("Please enter a valid fraction (e.g., 5/3 or 1 2/3)") + } + } + + @Test + fun testRealTimeError_validRegularFraction_noErrorMessage() { + activityRule.scenario.onActivity { activity -> + val errorMessage = fractionParser.getRealTimeAnswerError("2/3") + .toUiError() + .getErrorMessageFromStringRes(activity.appLanguageResourceHandler) + assertThat(errorMessage).isNull() + } + } + + @Test + fun testRealTimeError_nonDigits_invalidChars_hasRelevantErrorMessage() { + activityRule.scenario.onActivity { activity -> + val errorMessage = fractionParser.getRealTimeAnswerError("abc") + .toUiError() + .getErrorMessageFromStringRes(activity.appLanguageResourceHandler) + assertThat(errorMessage) + .isEqualTo("Please only use numerical digits, spaces or forward slashes (/)") + } + } + + @Test + fun testRealTimeError_noNumerator_invalidFormat_hasRelevantErrorMessage() { + activityRule.scenario.onActivity { activity -> + val errorMessage = fractionParser.getRealTimeAnswerError("/3") + .toUiError() + .getErrorMessageFromStringRes(activity.appLanguageResourceHandler) + assertThat(errorMessage) + .isEqualTo("Please enter a valid fraction (e.g., 5/3 or 1 2/3)") + } + } + + @Test + fun testRealTimeError_severalSlashes_invalidFormat_hasRelevantErrorMessage() { + activityRule.scenario.onActivity { activity -> + val errorMessage = fractionParser.getRealTimeAnswerError("1/3/8") + .toUiError() + .getErrorMessageFromStringRes(activity.appLanguageResourceHandler) + assertThat(errorMessage) + .isEqualTo("Please enter a valid fraction (e.g., 5/3 or 1 2/3)") + } + } + + @Test + fun testRealTimeError_severalDashes_invalidFormat_hasRelevantErrorMessage() { + activityRule.scenario.onActivity { activity -> + val errorMessage = fractionParser.getRealTimeAnswerError("-1/-3") + .toUiError() + .getErrorMessageFromStringRes(activity.appLanguageResourceHandler) + assertThat(errorMessage) + .isEqualTo("Please enter a valid fraction (e.g., 5/3 or 1 2/3)") + } + } + + private fun setUpTestApplicationComponent() { + ApplicationProvider.getApplicationContext().inject(this) + } + + private companion object { + private fun FractionParsingError.toUiError(): FractionParsingUiError = + FractionParsingUiError.createFromParsingError(this) + } + + // TODO(#59): Figure out a way to reuse modules instead of needing to re-declare them. + @Singleton + @Component( + modules = [ + TestDispatcherModule::class, ApplicationModule::class, RobolectricModule::class, + PlatformParameterModule::class, PlatformParameterSingletonModule::class, + LoggerModule::class, ContinueModule::class, FractionInputModule::class, + ItemSelectionInputModule::class, MultipleChoiceInputModule::class, + NumberWithUnitsRuleModule::class, NumericInputRuleModule::class, TextInputRuleModule::class, + DragDropSortInputModule::class, ImageClickInputModule::class, InteractionsModule::class, + GcsResourceModule::class, GlideImageLoaderModule::class, ImageParsingModule::class, + HtmlParserEntityTypeModule::class, QuestionModule::class, TestLogReportingModule::class, + AccessibilityTestModule::class, LogStorageModule::class, CachingTestModule::class, + PrimeTopicAssetsControllerModule::class, ExpirationMetaDataRetrieverModule::class, + ViewBindingShimModule::class, RatioInputModule::class, WorkManagerConfigurationModule::class, + ApplicationStartupListenerModule::class, LogUploadWorkerModule::class, + HintsAndSolutionConfigModule::class, HintsAndSolutionProdModule::class, + FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class, + DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, + ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, + NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + ] + ) + interface TestApplicationComponent : ApplicationComponent { + @Component.Builder + interface Builder : ApplicationComponent.Builder + + fun inject(fractionParsingUiErrorTest: FractionParsingUiErrorTest) + } + + class TestApplication : Application(), ActivityComponentFactory, ApplicationInjectorProvider { + private val component: TestApplicationComponent by lazy { + DaggerFractionParsingUiErrorTest_TestApplicationComponent.builder() + .setApplication(this) + .build() as TestApplicationComponent + } + + fun inject(fractionParsingUiErrorTest: FractionParsingUiErrorTest) { + component.inject(fractionParsingUiErrorTest) + } + + override fun createActivityComponent(activity: AppCompatActivity): ActivityComponent { + return component.getActivityComponentBuilderProvider().get().setActivity(activity).build() + } + + override fun getApplicationInjector(): ApplicationInjector = component + } +} diff --git a/app/src/test/java/org/oppia/android/app/parser/StringToFractionParserTest.kt b/app/src/test/java/org/oppia/android/app/parser/StringToFractionParserTest.kt deleted file mode 100644 index 1d71c8c8afd..00000000000 --- a/app/src/test/java/org/oppia/android/app/parser/StringToFractionParserTest.kt +++ /dev/null @@ -1,509 +0,0 @@ -package org.oppia.android.app.parser - -import android.app.Application -import androidx.appcompat.app.AppCompatActivity -import androidx.test.core.app.ApplicationProvider -import androidx.test.ext.junit.rules.ActivityScenarioRule -import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.google.common.truth.Truth.assertThat -import dagger.Component -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.oppia.android.app.activity.ActivityComponent -import org.oppia.android.app.activity.ActivityComponentFactory -import org.oppia.android.app.application.ApplicationComponent -import org.oppia.android.app.application.ApplicationInjector -import org.oppia.android.app.application.ApplicationInjectorProvider -import org.oppia.android.app.application.ApplicationModule -import org.oppia.android.app.application.ApplicationStartupListenerModule -import org.oppia.android.app.devoptions.DeveloperOptionsModule -import org.oppia.android.app.devoptions.DeveloperOptionsStarterModule -import org.oppia.android.app.model.Fraction -import org.oppia.android.app.shim.ViewBindingShimModule -import org.oppia.android.app.testing.activity.TestActivity -import org.oppia.android.app.topic.PracticeTabModule -import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule -import org.oppia.android.data.backends.gae.NetworkConfigProdModule -import org.oppia.android.data.backends.gae.NetworkModule -import org.oppia.android.domain.classify.InteractionsModule -import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule -import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule -import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule -import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule -import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule -import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule -import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule -import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule -import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule -import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule -import org.oppia.android.domain.exploration.lightweightcheckpointing.ExplorationStorageModule -import org.oppia.android.domain.hintsandsolution.HintsAndSolutionConfigModule -import org.oppia.android.domain.hintsandsolution.HintsAndSolutionProdModule -import org.oppia.android.domain.onboarding.ExpirationMetaDataRetrieverModule -import org.oppia.android.domain.oppialogger.LogStorageModule -import org.oppia.android.domain.oppialogger.loguploader.LogUploadWorkerModule -import org.oppia.android.domain.platformparameter.PlatformParameterModule -import org.oppia.android.domain.platformparameter.PlatformParameterSingletonModule -import org.oppia.android.domain.question.QuestionModule -import org.oppia.android.domain.topic.PrimeTopicAssetsControllerModule -import org.oppia.android.domain.workmanager.WorkManagerConfigurationModule -import org.oppia.android.testing.TestLogReportingModule -import org.oppia.android.testing.assertThrows -import org.oppia.android.testing.junit.InitializeDefaultLocaleRule -import org.oppia.android.testing.robolectric.RobolectricModule -import org.oppia.android.testing.threading.TestDispatcherModule -import org.oppia.android.testing.time.FakeOppiaClockModule -import org.oppia.android.util.accessibility.AccessibilityTestModule -import org.oppia.android.util.caching.AssetModule -import org.oppia.android.util.caching.testing.CachingTestModule -import org.oppia.android.util.gcsresource.GcsResourceModule -import org.oppia.android.util.locale.LocaleProdModule -import org.oppia.android.util.logging.LoggerModule -import org.oppia.android.util.logging.firebase.FirebaseLogUploaderModule -import org.oppia.android.util.networking.NetworkConnectionDebugUtilModule -import org.oppia.android.util.networking.NetworkConnectionUtilDebugModule -import org.oppia.android.util.parser.html.HtmlParserEntityTypeModule -import org.oppia.android.util.parser.image.GlideImageLoaderModule -import org.oppia.android.util.parser.image.ImageParsingModule -import org.robolectric.annotation.Config -import org.robolectric.annotation.LooperMode -import javax.inject.Singleton - -/** Tests for [StringToFractionParser]. */ -@RunWith(AndroidJUnit4::class) -@LooperMode(LooperMode.Mode.PAUSED) -@Config(application = StringToFractionParserTest.TestApplication::class, qualifiers = "port-xxhdpi") -class StringToFractionParserTest { - @get:Rule - val initializeDefaultLocaleRule = InitializeDefaultLocaleRule() - - @get:Rule - var activityRule = - ActivityScenarioRule( - TestActivity.createIntent(ApplicationProvider.getApplicationContext()) - ) - - private lateinit var stringToFractionParser: StringToFractionParser - - @Before - fun setUp() { - setUpTestApplicationComponent() - stringToFractionParser = StringToFractionParser() - } - - @Test - fun testSubmitTimeError_regularFraction_returnsValid() { - val error = stringToFractionParser.getSubmitTimeError("1/2") - assertThat(error).isEqualTo(StringToFractionParser.FractionParsingError.VALID) - } - - @Test - fun testSubmitTimeError_regularNegativeFractionWithExtraSpaces_returnsValid() { - val error = stringToFractionParser.getSubmitTimeError(" -1 / 2 ") - assertThat(error).isEqualTo(StringToFractionParser.FractionParsingError.VALID) - } - - @Test - fun testSubmitTimeError_atLengthLimit_returnsValid() { - val error = stringToFractionParser.getSubmitTimeError("1234567/1234567") - assertThat(error).isEqualTo(StringToFractionParser.FractionParsingError.VALID) - } - - @Test - fun testSubmitTimeError_wholeNumber_returnsValid() { - val error = stringToFractionParser.getSubmitTimeError("888") - assertThat(error).isEqualTo(StringToFractionParser.FractionParsingError.VALID) - } - - @Test - fun testSubmitTimeError_wholeNegativeNumber_returnsValid() { - val error = stringToFractionParser.getSubmitTimeError("-777") - assertThat(error).isEqualTo(StringToFractionParser.FractionParsingError.VALID) - } - - @Test - fun testSubmitTimeError_mixedNumber_returnsValid() { - val error = stringToFractionParser.getSubmitTimeError("11 22/33") - assertThat(error).isEqualTo(StringToFractionParser.FractionParsingError.VALID) - } - - @Test - fun testSubmitTimeError_validMixedNumber_noErrorMessage() { - activityRule.scenario.onActivity { activity -> - val errorMessage = stringToFractionParser.getSubmitTimeError("11 22/33") - .getErrorMessageFromStringRes(activity.appLanguageResourceHandler) - assertThat(errorMessage).isNull() - } - } - - @Test - fun testSubmitTimeError_tenDigitNumber_returnsNumberTooLong() { - val error = stringToFractionParser.getSubmitTimeError("0123456789") - assertThat(error).isEqualTo(StringToFractionParser.FractionParsingError.NUMBER_TOO_LONG) - } - - @Test - fun testSubmitTimeError_tenDigitNumber_numberTooLong_hasRelevantErrorMessage() { - activityRule.scenario.onActivity { activity -> - val errorMessage = stringToFractionParser.getSubmitTimeError("0123456789") - .getErrorMessageFromStringRes(activity.appLanguageResourceHandler) - assertThat(errorMessage) - .isEqualTo("None of the numbers in the fraction should have more than 7 digits.") - } - } - - @Test - fun testSubmitTimeError_nonDigits_returnsInvalidFormat() { - val error = stringToFractionParser.getSubmitTimeError("jdhfc") - assertThat(error).isEqualTo(StringToFractionParser.FractionParsingError.INVALID_FORMAT) - } - - @Test - fun testSubmitTimeError_nonDigits_invalidFormat_hasRelevantErrorMessage() { - activityRule.scenario.onActivity { activity -> - val errorMessage = stringToFractionParser.getSubmitTimeError("jdhfc") - .getErrorMessageFromStringRes(activity.appLanguageResourceHandler) - assertThat(errorMessage) - .isEqualTo("Please enter a valid fraction (e.g., 5/3 or 1 2/3)") - } - } - - @Test - fun testSubmitTimeError_divisionByZero_returnsDivisionByZero() { - val error = stringToFractionParser.getSubmitTimeError("123/0") - assertThat(error).isEqualTo(StringToFractionParser.FractionParsingError.DIVISION_BY_ZERO) - } - - @Test - fun testSubmitTimeError_divisionByZero_hasRelevantErrorMessage() { - activityRule.scenario.onActivity { activity -> - val errorMessage = stringToFractionParser.getSubmitTimeError("123/0") - .getErrorMessageFromStringRes(activity.appLanguageResourceHandler) - assertThat(errorMessage).isEqualTo("Please do not put 0 in the denominator") - } - } - - @Test - fun testSubmitTimeError_ambiguousSpacing_returnsInvalidFormat() { - val error = stringToFractionParser.getSubmitTimeError("1 2 3/4") - assertThat(error).isEqualTo(StringToFractionParser.FractionParsingError.INVALID_FORMAT) - } - - @Test - fun testSubmitTimeError_ambiguousSpacing_invalidFormat_hasRelevantErrorMessage() { - activityRule.scenario.onActivity { activity -> - val errorMessage = stringToFractionParser.getSubmitTimeError("1 2 3/4") - .getErrorMessageFromStringRes(activity.appLanguageResourceHandler) - assertThat(errorMessage) - .isEqualTo("Please enter a valid fraction (e.g., 5/3 or 1 2/3)") - } - } - - @Test - fun testSubmitTimeError_emptyString_returnsInvalidFormat() { - val error = stringToFractionParser.getSubmitTimeError("") - assertThat(error).isEqualTo(StringToFractionParser.FractionParsingError.INVALID_FORMAT) - } - - @Test - fun testSubmitTimeError_emptyString_invalidFormat_hasRelevantErrorMessage() { - activityRule.scenario.onActivity { activity -> - val errorMessage = stringToFractionParser.getSubmitTimeError("") - .getErrorMessageFromStringRes(activity.appLanguageResourceHandler) - assertThat(errorMessage) - .isEqualTo("Please enter a valid fraction (e.g., 5/3 or 1 2/3)") - } - } - - @Test - fun testRealTimeError_regularFraction_returnsValid() { - val error = stringToFractionParser.getRealTimeAnswerError("2/3") - assertThat(error).isEqualTo(StringToFractionParser.FractionParsingError.VALID) - } - - @Test - fun testRealTimeError_regularNegativeFraction_returnsValid() { - val error = stringToFractionParser.getRealTimeAnswerError("-2/3") - assertThat(error).isEqualTo(StringToFractionParser.FractionParsingError.VALID) - } - - @Test - fun testRealTimeError_wholeNumber_returnsValid() { - val error = stringToFractionParser.getRealTimeAnswerError("4") - assertThat(error).isEqualTo(StringToFractionParser.FractionParsingError.VALID) - } - - @Test - fun testRealTimeError_wholeNegativeNumber_returnsValid() { - val error = stringToFractionParser.getRealTimeAnswerError("-4") - assertThat(error).isEqualTo(StringToFractionParser.FractionParsingError.VALID) - } - - @Test - fun testRealTimeError_mixedNumber_returnsValid() { - val error = stringToFractionParser.getRealTimeAnswerError("5 2/3") - assertThat(error).isEqualTo(StringToFractionParser.FractionParsingError.VALID) - } - - @Test - fun testRealTimeError_mixedNegativeNumber_returnsValid() { - val error = stringToFractionParser.getRealTimeAnswerError("-5 2/3") - assertThat(error).isEqualTo(StringToFractionParser.FractionParsingError.VALID) - } - - @Test - fun testRealTimeError_validRegularFraction_noErrorMessage() { - activityRule.scenario.onActivity { activity -> - val errorMessage = stringToFractionParser.getRealTimeAnswerError("2/3") - .getErrorMessageFromStringRes(activity.appLanguageResourceHandler) - assertThat(errorMessage).isNull() - } - } - - @Test - fun testRealTimeError_nonDigits_returnsInvalidChars() { - val error = stringToFractionParser.getRealTimeAnswerError("abc") - assertThat(error).isEqualTo(StringToFractionParser.FractionParsingError.INVALID_CHARS) - } - - @Test - fun testRealTimeError_nonDigits_invalidChars_hasRelevantErrorMessage() { - activityRule.scenario.onActivity { activity -> - val errorMessage = stringToFractionParser.getRealTimeAnswerError("abc") - .getErrorMessageFromStringRes(activity.appLanguageResourceHandler) - assertThat(errorMessage) - .isEqualTo("Please only use numerical digits, spaces or forward slashes (/)") - } - } - - @Test - fun testRealTimeError_noNumerator_returnsInvalidFormat() { - val error = stringToFractionParser.getRealTimeAnswerError("/3") - assertThat(error).isEqualTo(StringToFractionParser.FractionParsingError.INVALID_FORMAT) - } - - @Test - fun testRealTimeError_noNumerator_invalidFormat_hasRelevantErrorMessage() { - activityRule.scenario.onActivity { activity -> - val errorMessage = stringToFractionParser.getRealTimeAnswerError("/3") - .getErrorMessageFromStringRes(activity.appLanguageResourceHandler) - assertThat(errorMessage) - .isEqualTo("Please enter a valid fraction (e.g., 5/3 or 1 2/3)") - } - } - - @Test - fun testRealTimeError_severalSlashes_invalidFormat_returnsInvalidFormat() { - val error = stringToFractionParser.getRealTimeAnswerError("1/3/8") - assertThat(error).isEqualTo(StringToFractionParser.FractionParsingError.INVALID_FORMAT) - } - - @Test - fun testRealTimeError_severalSlashes_invalidFormat_hasRelevantErrorMessage() { - activityRule.scenario.onActivity { activity -> - val errorMessage = stringToFractionParser.getRealTimeAnswerError("1/3/8") - .getErrorMessageFromStringRes(activity.appLanguageResourceHandler) - assertThat(errorMessage) - .isEqualTo("Please enter a valid fraction (e.g., 5/3 or 1 2/3)") - } - } - - @Test - fun testRealTimeError_severalDashes_returnsInvalidFormat() { - val error = stringToFractionParser.getRealTimeAnswerError("-1/-3") - assertThat(error).isEqualTo(StringToFractionParser.FractionParsingError.INVALID_FORMAT) - } - - @Test - fun testRealTimeError_severalDashes_invalidFormat_hasRelevantErrorMessage() { - activityRule.scenario.onActivity { activity -> - val errorMessage = stringToFractionParser.getRealTimeAnswerError("-1/-3") - .getErrorMessageFromStringRes(activity.appLanguageResourceHandler) - assertThat(errorMessage) - .isEqualTo("Please enter a valid fraction (e.g., 5/3 or 1 2/3)") - } - } - - @Test - fun testParseFraction_divisionByZero_returnsFraction() { - val parseFraction = stringToFractionParser.parseFraction("8/0") - val parseFractionFromString = stringToFractionParser.parseFractionFromString("8/0") - val expectedFraction = Fraction.newBuilder().apply { - numerator = 8 - denominator = 0 - }.build() - assertThat(parseFractionFromString).isEqualTo(expectedFraction) - assertThat(parseFraction).isEqualTo(expectedFraction) - } - - @Test - fun testParseFraction_multipleFractions_failsWithError() { - val parseFraction = stringToFractionParser.parseFraction("7 1/2 4/5") - assertThat(parseFraction).isEqualTo(null) - - val exception = assertThrows(IllegalArgumentException::class) { - stringToFractionParser.parseFractionFromString("7 1/2 4/5") - } - assertThat(exception).hasMessageThat().contains("Incorrectly formatted fraction: 7 1/2 4/5") - } - - @Test - fun testParseFraction_nonDigits_failsWithError() { - val parseFraction = stringToFractionParser.parseFraction("abc") - assertThat(parseFraction).isEqualTo(null) - - val exception = assertThrows(IllegalArgumentException::class) { - stringToFractionParser.parseFractionFromString("abc") - } - assertThat(exception).hasMessageThat().contains("Incorrectly formatted fraction: abc") - } - - @Test - fun testParseFraction_regularFraction_returnsFraction() { - val parseFractionFromString = stringToFractionParser.parseFractionFromString("1/2") - val parseFraction = stringToFractionParser.parseFraction("1/2") - val expectedFraction = Fraction.newBuilder().apply { - numerator = 1 - denominator = 2 - }.build() - assertThat(parseFractionFromString).isEqualTo(expectedFraction) - assertThat(parseFraction).isEqualTo(expectedFraction) - } - - @Test - fun testParseFraction_regularNegativeFraction_returnsFraction() { - val parseFractionFromString = stringToFractionParser.parseFractionFromString("-8/4") - val parseFraction = stringToFractionParser.parseFraction("-8/4") - val expectedFraction = Fraction.newBuilder().apply { - isNegative = true - numerator = 8 - denominator = 4 - }.build() - assertThat(parseFractionFromString).isEqualTo(expectedFraction) - assertThat(parseFraction).isEqualTo(expectedFraction) - } - - @Test - fun testParseFraction_wholeNumber_returnsFraction() { - val parseFractionFromString = stringToFractionParser.parseFractionFromString("7") - val parseFraction = stringToFractionParser.parseFraction("7") - val expectedFraction = Fraction.newBuilder().apply { - wholeNumber = 7 - numerator = 0 - denominator = 1 - }.build() - assertThat(parseFractionFromString).isEqualTo(expectedFraction) - assertThat(parseFraction).isEqualTo(expectedFraction) - } - - @Test - fun testParseFraction_wholeNegativeNumber_returnsFraction() { - val parseFractionFromString = stringToFractionParser.parseFractionFromString("-7") - val parseFraction = stringToFractionParser.parseFraction("-7") - val expectedFraction = Fraction.newBuilder().apply { - isNegative = true - wholeNumber = 7 - numerator = 0 - denominator = 1 - }.build() - assertThat(parseFractionFromString).isEqualTo(expectedFraction) - assertThat(parseFraction).isEqualTo(expectedFraction) - } - - @Test - fun testParseFraction_mixedNumber_returnsFraction() { - val parseFractionFromString = stringToFractionParser.parseFractionFromString("1 3/4") - val parseFraction = stringToFractionParser.parseFraction("1 3/4") - val expectedFraction = Fraction.newBuilder().apply { - wholeNumber = 1 - numerator = 3 - denominator = 4 - }.build() - assertThat(parseFractionFromString).isEqualTo(expectedFraction) - assertThat(parseFraction).isEqualTo(expectedFraction) - } - - @Test - fun testParseFraction_negativeMixedNumber_returnsFraction() { - val parseFractionFromString = stringToFractionParser.parseFractionFromString("-123 456/7") - val parseFraction = stringToFractionParser.parseFraction("-123 456/7") - val expectedFraction = Fraction.newBuilder().apply { - isNegative = true - wholeNumber = 123 - numerator = 456 - denominator = 7 - }.build() - assertThat(parseFractionFromString).isEqualTo(expectedFraction) - assertThat(parseFraction).isEqualTo(expectedFraction) - } - - @Test - fun testParseFraction_longMixedNumber_returnsFraction() { - val parseFractionFromString = stringToFractionParser - .parseFractionFromString("1234567 1234567/1234567") - val parseFraction = stringToFractionParser - .parseFraction("1234567 1234567/1234567") - val expectedFraction = Fraction.newBuilder().apply { - wholeNumber = 1234567 - numerator = 1234567 - denominator = 1234567 - }.build() - assertThat(parseFractionFromString).isEqualTo(expectedFraction) - assertThat(parseFraction).isEqualTo(expectedFraction) - } - - private fun setUpTestApplicationComponent() { - ApplicationProvider.getApplicationContext().inject(this) - } - - // TODO(#59): Figure out a way to reuse modules instead of needing to re-declare them. - @Singleton - @Component( - modules = [ - TestDispatcherModule::class, ApplicationModule::class, RobolectricModule::class, - PlatformParameterModule::class, PlatformParameterSingletonModule::class, - LoggerModule::class, ContinueModule::class, FractionInputModule::class, - ItemSelectionInputModule::class, MultipleChoiceInputModule::class, - NumberWithUnitsRuleModule::class, NumericInputRuleModule::class, TextInputRuleModule::class, - DragDropSortInputModule::class, ImageClickInputModule::class, InteractionsModule::class, - GcsResourceModule::class, GlideImageLoaderModule::class, ImageParsingModule::class, - HtmlParserEntityTypeModule::class, QuestionModule::class, TestLogReportingModule::class, - AccessibilityTestModule::class, LogStorageModule::class, CachingTestModule::class, - PrimeTopicAssetsControllerModule::class, ExpirationMetaDataRetrieverModule::class, - ViewBindingShimModule::class, RatioInputModule::class, WorkManagerConfigurationModule::class, - ApplicationStartupListenerModule::class, LogUploadWorkerModule::class, - HintsAndSolutionConfigModule::class, HintsAndSolutionProdModule::class, - FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class, - DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, - ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, - NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class - ] - ) - interface TestApplicationComponent : ApplicationComponent { - @Component.Builder - interface Builder : ApplicationComponent.Builder - - fun inject(stringToFractionParserTest: StringToFractionParserTest) - } - - class TestApplication : Application(), ActivityComponentFactory, ApplicationInjectorProvider { - private val component: TestApplicationComponent by lazy { - DaggerStringToFractionParserTest_TestApplicationComponent.builder() - .setApplication(this) - .build() as TestApplicationComponent - } - - fun inject(stringToFractionParserTest: StringToFractionParserTest) { - component.inject(stringToFractionParserTest) - } - - override fun createActivityComponent(activity: AppCompatActivity): ActivityComponent { - return component.getActivityComponentBuilderProvider().get().setActivity(activity).build() - } - - override fun getApplicationInjector(): ApplicationInjector = component - } -} diff --git a/domain/BUILD.bazel b/domain/BUILD.bazel index 6d15ff50d73..49bdc4dbf0c 100755 --- a/domain/BUILD.bazel +++ b/domain/BUILD.bazel @@ -122,6 +122,7 @@ kt_android_library( "//utility/src/main/java/org/oppia/android/util/data:data_providers", "//utility/src/main/java/org/oppia/android/util/extensions:bundle_extensions", "//utility/src/main/java/org/oppia/android/util/extensions:context_extensions", + "//utility/src/main/java/org/oppia/android/util/extensions:string_extensions", "//utility/src/main/java/org/oppia/android/util/logging:event_logger", "//utility/src/main/java/org/oppia/android/util/logging:log_uploader", "//utility/src/main/java/org/oppia/android/util/math:extensions", diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/TextInputContainsRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/TextInputContainsRuleClassifierProvider.kt index 76b65756b5b..7c663c136c0 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/TextInputContainsRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/TextInputContainsRuleClassifierProvider.kt @@ -7,7 +7,7 @@ import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider import org.oppia.android.domain.translation.TranslationController -import org.oppia.android.domain.util.normalizeWhitespace +import org.oppia.android.util.extensions.normalizeWhitespace import org.oppia.android.util.locale.OppiaLocale import javax.inject.Inject diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/TextInputEqualsRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/TextInputEqualsRuleClassifierProvider.kt index a4b705f05b8..a9086ed0e87 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/TextInputEqualsRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/TextInputEqualsRuleClassifierProvider.kt @@ -7,7 +7,7 @@ import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider import org.oppia.android.domain.translation.TranslationController -import org.oppia.android.domain.util.normalizeWhitespace +import org.oppia.android.util.extensions.normalizeWhitespace import org.oppia.android.util.locale.OppiaLocale import javax.inject.Inject diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/TextInputFuzzyEqualsRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/TextInputFuzzyEqualsRuleClassifierProvider.kt index 3c69d469dec..ac17f971a71 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/TextInputFuzzyEqualsRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/TextInputFuzzyEqualsRuleClassifierProvider.kt @@ -7,7 +7,7 @@ import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider import org.oppia.android.domain.translation.TranslationController -import org.oppia.android.domain.util.normalizeWhitespace +import org.oppia.android.util.extensions.normalizeWhitespace import org.oppia.android.util.locale.OppiaLocale import javax.inject.Inject diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/TextInputStartsWithRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/TextInputStartsWithRuleClassifierProvider.kt index a216eabce4c..5862ce68045 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/TextInputStartsWithRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/TextInputStartsWithRuleClassifierProvider.kt @@ -7,7 +7,7 @@ import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider import org.oppia.android.domain.translation.TranslationController -import org.oppia.android.domain.util.normalizeWhitespace +import org.oppia.android.util.extensions.normalizeWhitespace import org.oppia.android.util.locale.OppiaLocale import javax.inject.Inject diff --git a/domain/src/main/java/org/oppia/android/domain/util/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/util/BUILD.bazel index 7c1dc1e6ce2..de927b14eaf 100644 --- a/domain/src/main/java/org/oppia/android/domain/util/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/util/BUILD.bazel @@ -23,7 +23,6 @@ kt_android_library( srcs = [ "InteractionObjectExtensions.kt", "JsonExtensions.kt", - "StringExtensions.kt", "WorkDataExtensions.kt", ], visibility = ["//domain:__subpackages__"], diff --git a/domain/src/test/java/org/oppia/android/domain/util/StringExtensionsTest.kt b/domain/src/test/java/org/oppia/android/domain/util/StringExtensionsTest.kt index f345fd1d134..4e42340ba6b 100644 --- a/domain/src/test/java/org/oppia/android/domain/util/StringExtensionsTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/util/StringExtensionsTest.kt @@ -4,6 +4,8 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat import org.junit.Test import org.junit.runner.RunWith +import org.oppia.android.util.extensions.normalizeWhitespace +import org.oppia.android.util.extensions.removeWhitespace import org.robolectric.annotation.LooperMode /** Tests for [StringExtensions]. */ diff --git a/utility/src/main/java/org/oppia/android/util/extensions/BUILD.bazel b/utility/src/main/java/org/oppia/android/util/extensions/BUILD.bazel index 525f5160210..cfc2be6bb6a 100644 --- a/utility/src/main/java/org/oppia/android/util/extensions/BUILD.bazel +++ b/utility/src/main/java/org/oppia/android/util/extensions/BUILD.bazel @@ -25,3 +25,11 @@ kt_android_library( "//third_party:com_google_protobuf_protobuf-javalite", ], ) + +kt_android_library( + name = "string_extensions", + srcs = [ + "StringExtensions.kt", + ], + visibility = ["//:oppia_api_visibility"], +) diff --git a/domain/src/main/java/org/oppia/android/domain/util/StringExtensions.kt b/utility/src/main/java/org/oppia/android/util/extensions/StringExtensions.kt similarity index 92% rename from domain/src/main/java/org/oppia/android/domain/util/StringExtensions.kt rename to utility/src/main/java/org/oppia/android/util/extensions/StringExtensions.kt index 0dbdc30fc86..5e48805739d 100644 --- a/domain/src/main/java/org/oppia/android/domain/util/StringExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/extensions/StringExtensions.kt @@ -1,4 +1,4 @@ -package org.oppia.android.domain.util +package org.oppia.android.util.extensions /** * Normalizes whitespace in the specified string in a way consistent with Oppia web: diff --git a/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel b/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel index 4ecd3e58e47..cae9779bc08 100644 --- a/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel @@ -18,3 +18,15 @@ kt_android_library( "//model/src/main/proto:math_java_proto_lite", ], ) + +kt_android_library( + name = "fraction_parser", + srcs = ["FractionParser.kt"], + visibility = [ + "//:oppia_api_visibility", + ], + deps = [ + "//model/src/main/proto:math_java_proto_lite", + "//utility/src/main/java/org/oppia/android/util/extensions:string_extensions", + ], +) diff --git a/app/src/main/java/org/oppia/android/app/parser/StringToFractionParser.kt b/utility/src/main/java/org/oppia/android/util/math/FractionParser.kt similarity index 80% rename from app/src/main/java/org/oppia/android/app/parser/StringToFractionParser.kt rename to utility/src/main/java/org/oppia/android/util/math/FractionParser.kt index 62fc2e9a282..659153b98f9 100644 --- a/app/src/main/java/org/oppia/android/app/parser/StringToFractionParser.kt +++ b/utility/src/main/java/org/oppia/android/util/math/FractionParser.kt @@ -1,13 +1,10 @@ -package org.oppia.android.app.parser +package org.oppia.android.util.math -import androidx.annotation.StringRes -import org.oppia.android.R import org.oppia.android.app.model.Fraction -import org.oppia.android.app.translation.AppLanguageResourceHandler -import org.oppia.android.domain.util.normalizeWhitespace +import org.oppia.android.util.extensions.normalizeWhitespace -/** This class contains method that helps to parse string to fraction. */ -class StringToFractionParser { +/** String parser for [Fraction]s. */ +class FractionParser { private val wholeNumberOnlyRegex = """^-? ?(\d+)$""".toRegex() private val fractionOnlyRegex = @@ -27,8 +24,9 @@ class StringToFractionParser { * detection should be done using [getRealTimeAnswerError], instead. */ fun getSubmitTimeError(text: String): FractionParsingError { - if (invalidCharsLengthRegex.find(text) != null) + if (invalidCharsLengthRegex.find(text) != null) { return FractionParsingError.NUMBER_TOO_LONG + } val fraction = parseFraction(text) return when { fraction == null -> FractionParsingError.INVALID_FORMAT @@ -108,18 +106,27 @@ class StringToFractionParser { private fun isInputNegative(inputText: String): Boolean = inputText.startsWith("-") - /** Enum to store the errors of [FractionInputInteractionView]. */ - enum class FractionParsingError(@StringRes private var error: Int?) { - VALID(error = null), - INVALID_CHARS(error = R.string.fraction_error_invalid_chars), - INVALID_FORMAT(error = R.string.fraction_error_invalid_format), - DIVISION_BY_ZERO(error = R.string.fraction_error_divide_by_zero), - NUMBER_TOO_LONG(error = R.string.fraction_error_larger_than_seven_digits); + /** Represents errors that can occur when parsing a fraction from a string. */ + enum class FractionParsingError { + /** Indicates that the considered string is a valid fraction. */ + VALID, + + /** Indicates that the string contains characters not found in fractions. */ + INVALID_CHARS, + + /** Indicates that the string does not resemble a fraction. */ + INVALID_FORMAT, + + /** + * Indicates that the string includes a zero denominator which would result in a division by + * zero. + */ + DIVISION_BY_ZERO, /** - * Returns the string corresponding to this error's string resources, or null if there is none. + * Indicates that at least one of the numbers present in the string is too long to be + * precisely represented in a fraction. */ - fun getErrorMessageFromStringRes(resourceHandler: AppLanguageResourceHandler): String? = - error?.let(resourceHandler::getStringInLocale) + NUMBER_TOO_LONG } } diff --git a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel index 313a5a1f751..6fb6fb73482 100644 --- a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel @@ -4,6 +4,24 @@ Tests for general-purpose mathematics utilities. load("//:oppia_android_test.bzl", "oppia_android_test") +oppia_android_test( + name = "FractionParserTest", + srcs = ["FractionParserTest.kt"], + custom_package = "org.oppia.android.util.math", + test_class = "org.oppia.android.util.math.FractionParserTest", + test_manifest = "//utility:test_manifest", + deps = [ + "//model/src/main/proto:math_java_proto_lite", + "//testing", + "//third_party:androidx_test_ext_junit", + "//third_party:com_google_truth_truth", + "//third_party:junit_junit", + "//third_party:org_robolectric_robolectric", + "//third_party:robolectric_android-all", + "//utility/src/main/java/org/oppia/android/util/math:fraction_parser", + ], +) + oppia_android_test( name = "RatioExtensionsTest", srcs = ["RatioExtensionsTest.kt"], diff --git a/utility/src/test/java/org/oppia/android/util/math/FractionParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/FractionParserTest.kt new file mode 100644 index 00000000000..8c3ebca13cc --- /dev/null +++ b/utility/src/test/java/org/oppia/android/util/math/FractionParserTest.kt @@ -0,0 +1,280 @@ +package org.oppia.android.util.math + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.oppia.android.app.model.Fraction +import org.oppia.android.testing.assertThrows +import org.robolectric.annotation.Config +import org.robolectric.annotation.LooperMode + +/** Tests for [FractionParser]. */ +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") +@RunWith(AndroidJUnit4::class) +@LooperMode(LooperMode.Mode.PAUSED) +@Config +class FractionParserTest { + private lateinit var fractionParser: FractionParser + + @Before + fun setUp() { + fractionParser = FractionParser() + } + + @Test + fun testSubmitTimeError_regularFraction_returnsValid() { + val error = fractionParser.getSubmitTimeError("1/2") + assertThat(error).isEqualTo(FractionParser.FractionParsingError.VALID) + } + + @Test + fun testSubmitTimeError_regularNegativeFractionWithExtraSpaces_returnsValid() { + val error = fractionParser.getSubmitTimeError(" -1 / 2 ") + assertThat(error).isEqualTo(FractionParser.FractionParsingError.VALID) + } + + @Test + fun testSubmitTimeError_atLengthLimit_returnsValid() { + val error = fractionParser.getSubmitTimeError("1234567/1234567") + assertThat(error).isEqualTo(FractionParser.FractionParsingError.VALID) + } + + @Test + fun testSubmitTimeError_wholeNumber_returnsValid() { + val error = fractionParser.getSubmitTimeError("888") + assertThat(error).isEqualTo(FractionParser.FractionParsingError.VALID) + } + + @Test + fun testSubmitTimeError_wholeNegativeNumber_returnsValid() { + val error = fractionParser.getSubmitTimeError("-777") + assertThat(error).isEqualTo(FractionParser.FractionParsingError.VALID) + } + + @Test + fun testSubmitTimeError_mixedNumber_returnsValid() { + val error = fractionParser.getSubmitTimeError("11 22/33") + assertThat(error).isEqualTo(FractionParser.FractionParsingError.VALID) + } + + @Test + fun testSubmitTimeError_tenDigitNumber_returnsNumberTooLong() { + val error = fractionParser.getSubmitTimeError("0123456789") + assertThat(error).isEqualTo(FractionParser.FractionParsingError.NUMBER_TOO_LONG) + } + + @Test + fun testSubmitTimeError_nonDigits_returnsInvalidFormat() { + val error = fractionParser.getSubmitTimeError("jdhfc") + assertThat(error).isEqualTo(FractionParser.FractionParsingError.INVALID_FORMAT) + } + + @Test + fun testSubmitTimeError_divisionByZero_returnsDivisionByZero() { + val error = fractionParser.getSubmitTimeError("123/0") + assertThat(error).isEqualTo(FractionParser.FractionParsingError.DIVISION_BY_ZERO) + } + + @Test + fun testSubmitTimeError_ambiguousSpacing_returnsInvalidFormat() { + val error = fractionParser.getSubmitTimeError("1 2 3/4") + assertThat(error).isEqualTo(FractionParser.FractionParsingError.INVALID_FORMAT) + } + + @Test + fun testSubmitTimeError_emptyString_returnsInvalidFormat() { + val error = fractionParser.getSubmitTimeError("") + assertThat(error).isEqualTo(FractionParser.FractionParsingError.INVALID_FORMAT) + } + + @Test + fun testRealTimeError_regularFraction_returnsValid() { + val error = fractionParser.getRealTimeAnswerError("2/3") + assertThat(error).isEqualTo(FractionParser.FractionParsingError.VALID) + } + + @Test + fun testRealTimeError_regularNegativeFraction_returnsValid() { + val error = fractionParser.getRealTimeAnswerError("-2/3") + assertThat(error).isEqualTo(FractionParser.FractionParsingError.VALID) + } + + @Test + fun testRealTimeError_wholeNumber_returnsValid() { + val error = fractionParser.getRealTimeAnswerError("4") + assertThat(error).isEqualTo(FractionParser.FractionParsingError.VALID) + } + + @Test + fun testRealTimeError_wholeNegativeNumber_returnsValid() { + val error = fractionParser.getRealTimeAnswerError("-4") + assertThat(error).isEqualTo(FractionParser.FractionParsingError.VALID) + } + + @Test + fun testRealTimeError_mixedNumber_returnsValid() { + val error = fractionParser.getRealTimeAnswerError("5 2/3") + assertThat(error).isEqualTo(FractionParser.FractionParsingError.VALID) + } + + @Test + fun testRealTimeError_mixedNegativeNumber_returnsValid() { + val error = fractionParser.getRealTimeAnswerError("-5 2/3") + assertThat(error).isEqualTo(FractionParser.FractionParsingError.VALID) + } + + @Test + fun testRealTimeError_nonDigits_returnsInvalidChars() { + val error = fractionParser.getRealTimeAnswerError("abc") + assertThat(error).isEqualTo(FractionParser.FractionParsingError.INVALID_CHARS) + } + + @Test + fun testRealTimeError_noNumerator_returnsInvalidFormat() { + val error = fractionParser.getRealTimeAnswerError("/3") + assertThat(error).isEqualTo(FractionParser.FractionParsingError.INVALID_FORMAT) + } + + @Test + fun testRealTimeError_severalSlashes_invalidFormat_returnsInvalidFormat() { + val error = fractionParser.getRealTimeAnswerError("1/3/8") + assertThat(error).isEqualTo(FractionParser.FractionParsingError.INVALID_FORMAT) + } + + @Test + fun testRealTimeError_severalDashes_returnsInvalidFormat() { + val error = fractionParser.getRealTimeAnswerError("-1/-3") + assertThat(error).isEqualTo(FractionParser.FractionParsingError.INVALID_FORMAT) + } + + @Test + fun testParseFraction_divisionByZero_returnsFraction() { + val parseFraction = fractionParser.parseFraction("8/0") + val parseFractionFromString = fractionParser.parseFractionFromString("8/0") + val expectedFraction = Fraction.newBuilder().apply { + numerator = 8 + denominator = 0 + }.build() + assertThat(parseFractionFromString).isEqualTo(expectedFraction) + assertThat(parseFraction).isEqualTo(expectedFraction) + } + + @Test + fun testParseFraction_multipleFractions_failsWithError() { + val parseFraction = fractionParser.parseFraction("7 1/2 4/5") + assertThat(parseFraction).isEqualTo(null) + + val exception = assertThrows(IllegalArgumentException::class) { + fractionParser.parseFractionFromString("7 1/2 4/5") + } + assertThat(exception).hasMessageThat().contains("Incorrectly formatted fraction: 7 1/2 4/5") + } + + @Test + fun testParseFraction_nonDigits_failsWithError() { + val parseFraction = fractionParser.parseFraction("abc") + assertThat(parseFraction).isEqualTo(null) + + val exception = assertThrows(IllegalArgumentException::class) { + fractionParser.parseFractionFromString("abc") + } + assertThat(exception).hasMessageThat().contains("Incorrectly formatted fraction: abc") + } + + @Test + fun testParseFraction_regularFraction_returnsFraction() { + val parseFractionFromString = fractionParser.parseFractionFromString("1/2") + val parseFraction = fractionParser.parseFraction("1/2") + val expectedFraction = Fraction.newBuilder().apply { + numerator = 1 + denominator = 2 + }.build() + assertThat(parseFractionFromString).isEqualTo(expectedFraction) + assertThat(parseFraction).isEqualTo(expectedFraction) + } + + @Test + fun testParseFraction_regularNegativeFraction_returnsFraction() { + val parseFractionFromString = fractionParser.parseFractionFromString("-8/4") + val parseFraction = fractionParser.parseFraction("-8/4") + val expectedFraction = Fraction.newBuilder().apply { + isNegative = true + numerator = 8 + denominator = 4 + }.build() + assertThat(parseFractionFromString).isEqualTo(expectedFraction) + assertThat(parseFraction).isEqualTo(expectedFraction) + } + + @Test + fun testParseFraction_wholeNumber_returnsFraction() { + val parseFractionFromString = fractionParser.parseFractionFromString("7") + val parseFraction = fractionParser.parseFraction("7") + val expectedFraction = Fraction.newBuilder().apply { + wholeNumber = 7 + numerator = 0 + denominator = 1 + }.build() + assertThat(parseFractionFromString).isEqualTo(expectedFraction) + assertThat(parseFraction).isEqualTo(expectedFraction) + } + + @Test + fun testParseFraction_wholeNegativeNumber_returnsFraction() { + val parseFractionFromString = fractionParser.parseFractionFromString("-7") + val parseFraction = fractionParser.parseFraction("-7") + val expectedFraction = Fraction.newBuilder().apply { + isNegative = true + wholeNumber = 7 + numerator = 0 + denominator = 1 + }.build() + assertThat(parseFractionFromString).isEqualTo(expectedFraction) + assertThat(parseFraction).isEqualTo(expectedFraction) + } + + @Test + fun testParseFraction_mixedNumber_returnsFraction() { + val parseFractionFromString = fractionParser.parseFractionFromString("1 3/4") + val parseFraction = fractionParser.parseFraction("1 3/4") + val expectedFraction = Fraction.newBuilder().apply { + wholeNumber = 1 + numerator = 3 + denominator = 4 + }.build() + assertThat(parseFractionFromString).isEqualTo(expectedFraction) + assertThat(parseFraction).isEqualTo(expectedFraction) + } + + @Test + fun testParseFraction_negativeMixedNumber_returnsFraction() { + val parseFractionFromString = fractionParser.parseFractionFromString("-123 456/7") + val parseFraction = fractionParser.parseFraction("-123 456/7") + val expectedFraction = Fraction.newBuilder().apply { + isNegative = true + wholeNumber = 123 + numerator = 456 + denominator = 7 + }.build() + assertThat(parseFractionFromString).isEqualTo(expectedFraction) + assertThat(parseFraction).isEqualTo(expectedFraction) + } + + @Test + fun testParseFraction_longMixedNumber_returnsFraction() { + val parseFractionFromString = fractionParser + .parseFractionFromString("1234567 1234567/1234567") + val parseFraction = fractionParser + .parseFraction("1234567 1234567/1234567") + val expectedFraction = Fraction.newBuilder().apply { + wholeNumber = 1234567 + numerator = 1234567 + denominator = 1234567 + }.build() + assertThat(parseFractionFromString).isEqualTo(expectedFraction) + assertThat(parseFraction).isEqualTo(expectedFraction) + } +} From 73dccd8f36ce44493c04c3912e76d1bc3c4c2e28 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 26 Jan 2022 19:05:23 -0800 Subject: [PATCH 067/162] Address reviewer comments. --- .../oppia/android/util/math/PolynomialExtensions.kt | 8 +++++--- .../java/org/oppia/android/util/math/RealExtensions.kt | 4 ++++ .../org/oppia/android/util/math/FloatExtensionsTest.kt | 10 ++++++++++ 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt index 25bb5f2955d..5983554ddb1 100644 --- a/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt @@ -24,10 +24,12 @@ fun Polynomial.getConstant(): Real = getTerm(0).coefficient * the polynomial, e.g. "1+x-7x^2"). */ fun Polynomial.toPlainText(): String { - return termList.map { it.toPlainText() }.reduce { acc, termAnswerStr -> + return termList.map { + it.toPlainText() + }.reduce { ongoingPolynomialStr, termAnswerStr -> if (termAnswerStr.startsWith("-")) { - "$acc - ${termAnswerStr.drop(1)}" - } else "$acc + $termAnswerStr" + "$ongoingPolynomialStr - ${termAnswerStr.drop(1)}" + } else "$ongoingPolynomialStr + $termAnswerStr" } } diff --git a/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt index b4fb0a39dad..7f8eabb7901 100644 --- a/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt @@ -41,6 +41,9 @@ fun Real.toDouble(): Double { * the real (which means proper fractions are converted to improper answer strings since fractions * like '1 1/2' can't be written as a numeric expression without converting them to an improper * form: '3/2'). + * + * Note that this will return an empty string if this [Real] doesn't represent an actual real value + * (e.g. a default instance). */ fun Real.toPlainText(): String = when (realTypeCase) { // Note that the rational part is first converted to an improper fraction since mixed fractions @@ -48,6 +51,7 @@ fun Real.toPlainText(): String = when (realTypeCase) { RATIONAL -> rational.toImproperForm().toAnswerString() IRRATIONAL -> irrational.toPlainString() INTEGER -> integer.toString() + // The Real type isn't valid, so rather than failing just return an empty string. REALTYPE_NOT_SET, null -> "" } diff --git a/utility/src/test/java/org/oppia/android/util/math/FloatExtensionsTest.kt b/utility/src/test/java/org/oppia/android/util/math/FloatExtensionsTest.kt index 9010828169e..6e1896902e6 100644 --- a/utility/src/test/java/org/oppia/android/util/math/FloatExtensionsTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/FloatExtensionsTest.kt @@ -45,6 +45,16 @@ class FloatExtensionsTest { assertThat(leftFloat).isNotEqualTo(rightFloat) } + @Test + fun testFloat_approximatelyEquals_zeroAndNonZeroValue_veryDifferent_returnsFalse() { + val leftFloat = 0f + val rightFloat = 7.3f + + val result = leftFloat.approximatelyEquals(rightFloat) + + assertThat(result).isFalse() + } + @Test fun testFloat_approximatelyEquals_nonZeroValues_outsideInterval_returnsFalse() { val leftFloat = 1.2f From b7535fa9a7172c0f46b075fb99ee0c4987889d34 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 26 Jan 2022 19:06:00 -0800 Subject: [PATCH 068/162] Alphabetize test exemptions. --- scripts/assets/test_file_exemptions.textproto | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/assets/test_file_exemptions.textproto b/scripts/assets/test_file_exemptions.textproto index ae6a4f367b2..bd843cca817 100644 --- a/scripts/assets/test_file_exemptions.textproto +++ b/scripts/assets/test_file_exemptions.textproto @@ -648,10 +648,10 @@ exempted_file_path: "testing/src/main/java/org/oppia/android/testing/espresso/Ko exempted_file_path: "testing/src/main/java/org/oppia/android/testing/junit/DefineAppLanguageLocaleContext.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/ComparableOperationListSubject.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/FractionSubject.kt" -exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/RealSubject.kt" -exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/PolynomialSubject.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/MathEquationSubject.kt" +exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/PolynomialSubject.kt" +exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/RealSubject.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/mockito/MockitoKotlinHelper.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/network/ApiMockLoader.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/network/MockClassroomService.kt" From a00164c9f4b2fcbc9045cf8b37dd85d39c80bbe9 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 26 Jan 2022 19:56:51 -0800 Subject: [PATCH 069/162] Fix typo & add regex check. The new regex check makes it so that all parameterized testing can be more easily tracked by the Android TL. --- .../file_content_validation_checks.textproto | 8 +++++ .../regex/RegexPatternValidationCheckTest.kt | 32 +++++++++++++++++++ .../android/testing/math/TokenSubject.kt | 2 +- 3 files changed, 41 insertions(+), 1 deletion(-) diff --git a/scripts/assets/file_content_validation_checks.textproto b/scripts/assets/file_content_validation_checks.textproto index fec94690079..2ac733e617b 100644 --- a/scripts/assets/file_content_validation_checks.textproto +++ b/scripts/assets/file_content_validation_checks.textproto @@ -282,3 +282,11 @@ file_content_checks { prohibited_content_regex: "^proto_library\\(" failure_message: "Don't use proto_library. Use oppia_proto_library instead." } +file_content_checks { + file_path_regex: ".+?\\.kt" + prohibited_content_regex: "OppiaParameterizedTestRunner" + failure_message: "To use OppiaParameterizedTestRunner, please add an exemption to file_content_validation_checks.textproto and add an explanation for your use case in your PR description. Note that parameterized tests should only be used in special circumstances where a single behavior can be tested across multiple inputs, or for especially large test suites that can be trivially reduced." + exempted_file_name: "scripts/src/javatests/org/oppia/android/scripts/regex/RegexPatternValidationCheckTest.kt" + exempted_file_name: "utility/src/test/java/org/oppia/android/util/math/MathTokenizerTest.kt" + exempted_file_patterns: "testing/src/main/java/org/oppia/android/testing/junit/.+?\\.kt" +} diff --git a/scripts/src/javatests/org/oppia/android/scripts/regex/RegexPatternValidationCheckTest.kt b/scripts/src/javatests/org/oppia/android/scripts/regex/RegexPatternValidationCheckTest.kt index 04155481013..0ecea9fbee5 100644 --- a/scripts/src/javatests/org/oppia/android/scripts/regex/RegexPatternValidationCheckTest.kt +++ b/scripts/src/javatests/org/oppia/android/scripts/regex/RegexPatternValidationCheckTest.kt @@ -125,6 +125,12 @@ class RegexPatternValidationCheckTest { " null, instead. Delegates uses reflection internally, have a non-trivial initialization" + " cost, and can cause breakages on KitKat devices. See #3939 for more context." private val doNotUseProtoLibrary = "Don't use proto_library. Use oppia_proto_library instead." + private val parameterizedTestRunnerRequiresException = + "To use OppiaParameterizedTestRunner, please add an exemption to" + + " file_content_validation_checks.textproto and add an explanation for your use case in your" + + " PR description. Note that parameterized tests should only be used in special" + + " circumstances where a single behavior can be tested across multiple inputs, or for" + + " especially large test suites that can be trivially reduced." private val wikiReferenceNote = "Refer to https://github.com/oppia/oppia-android/wiki/Static-Analysis-Checks" + "#regexpatternvalidation-check for more details on how to fix this." @@ -1569,6 +1575,32 @@ class RegexPatternValidationCheckTest { ) } + @Test + fun testFileContent_kotlinTestUsesParameterizedTestRunner_fileContentIsNotCorrect() { + val prohibitedContent = + """ + import org.oppia.android.testing.junit.OppiaParameterizedTestRunner + @RunWith(OppiaParameterizedTestRunner::class) + """.trimIndent() + tempFolder.newFolder("testfiles", "domain", "src", "test") + val stringFilePath = "domain/src/test/SomeTest.kt" + tempFolder.newFile("testfiles/$stringFilePath").writeText(prohibitedContent) + + val exception = assertThrows(Exception::class) { + runScript() + } + + assertThat(exception).hasMessageThat().contains(REGEX_CHECK_FAILED_OUTPUT_INDICATOR) + assertThat(outContent.toString().trim()) + .isEqualTo( + """ + $stringFilePath:1: $parameterizedTestRunnerRequiresException + $stringFilePath:2: $parameterizedTestRunnerRequiresException + $wikiReferenceNote + """.trimIndent() + ) + } + @Test fun testFileContent_java8OptionalImport_fileContentIsNotCorrect() { val prohibitedContent = "import java.util.Optional" diff --git a/testing/src/main/java/org/oppia/android/testing/math/TokenSubject.kt b/testing/src/main/java/org/oppia/android/testing/math/TokenSubject.kt index a623c59c7b6..737de61a7b9 100644 --- a/testing/src/main/java/org/oppia/android/testing/math/TokenSubject.kt +++ b/testing/src/main/java/org/oppia/android/testing/math/TokenSubject.kt @@ -131,7 +131,7 @@ class TokenSubject( } /** - * Truth subject for verifying properties of [Token]FunctionName. + * Truth subject for verifying properties of [FunctionName]. * * Call [assertThat] to create the subject. */ From 0287f19afa6067a1fb870f4182b18d339402d4c5 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 27 Jan 2022 13:29:42 -0800 Subject: [PATCH 070/162] Add missing KDocs. --- .../oppia/android/app/parser/FractionParsingUiError.kt | 9 +++++++++ scripts/assets/kdoc_validity_exemptions.textproto | 1 - 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/oppia/android/app/parser/FractionParsingUiError.kt b/app/src/main/java/org/oppia/android/app/parser/FractionParsingUiError.kt index fb1991cca59..731d26e0590 100644 --- a/app/src/main/java/org/oppia/android/app/parser/FractionParsingUiError.kt +++ b/app/src/main/java/org/oppia/android/app/parser/FractionParsingUiError.kt @@ -7,10 +7,19 @@ import org.oppia.android.util.math.FractionParser.FractionParsingError /** Enum to store the errors of [FractionInputInteractionView]. */ enum class FractionParsingUiError(@StringRes private var error: Int?) { + /** Corresponds to [FractionParsingError.VALID]. */ VALID(error = null), + + /** Corresponds to [FractionParsingError.INVALID_CHARS]. */ INVALID_CHARS(error = R.string.fraction_error_invalid_chars), + + /** Corresponds to [FractionParsingError.INVALID_FORMAT]. */ INVALID_FORMAT(error = R.string.fraction_error_invalid_format), + + /** Corresponds to [FractionParsingError.DIVISION_BY_ZERO]. */ DIVISION_BY_ZERO(error = R.string.fraction_error_divide_by_zero), + + /** Corresponds to [FractionParsingError.NUMBER_TOO_LONG]. */ NUMBER_TOO_LONG(error = R.string.fraction_error_larger_than_seven_digits); /** diff --git a/scripts/assets/kdoc_validity_exemptions.textproto b/scripts/assets/kdoc_validity_exemptions.textproto index e3c33bbfc1c..adb016b4ac0 100644 --- a/scripts/assets/kdoc_validity_exemptions.textproto +++ b/scripts/assets/kdoc_validity_exemptions.textproto @@ -161,7 +161,6 @@ exempted_file_path: "app/src/main/java/org/oppia/android/app/options/RouteToAudi exempted_file_path: "app/src/main/java/org/oppia/android/app/options/RouteToReadingTextSizeListener.kt" exempted_file_path: "app/src/main/java/org/oppia/android/app/options/TextSizeItemViewModel.kt" exempted_file_path: "app/src/main/java/org/oppia/android/app/options/TextSizeRadioButtonListener.kt" -exempted_file_path: "app/src/main/java/org/oppia/android/app/parser/StringToFractionParser.kt" exempted_file_path: "app/src/main/java/org/oppia/android/app/parser/StringToNumberParser.kt" exempted_file_path: "app/src/main/java/org/oppia/android/app/parser/StringToRatioParser.kt" exempted_file_path: "app/src/main/java/org/oppia/android/app/player/audio/AudioFragmentPresenter.kt" From 1e5279dfe72e7a831e68560ac970ac0308c5c5f0 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 27 Jan 2022 13:45:29 -0800 Subject: [PATCH 071/162] Post-merge cleanups. Also, fix text file exemption ordering. --- .../file_content_validation_checks.textproto | 1 + scripts/assets/test_file_exemptions.textproto | 2 +- .../oppia/android/testing/math/BUILD.bazel | 2 +- .../org/oppia/android/util/math/BUILD.bazel | 20 +++++++++---------- .../org/oppia/android/util/math/BUILD.bazel | 8 ++++---- 5 files changed, 17 insertions(+), 16 deletions(-) diff --git a/scripts/assets/file_content_validation_checks.textproto b/scripts/assets/file_content_validation_checks.textproto index 2ac733e617b..a41099287d7 100644 --- a/scripts/assets/file_content_validation_checks.textproto +++ b/scripts/assets/file_content_validation_checks.textproto @@ -287,6 +287,7 @@ file_content_checks { prohibited_content_regex: "OppiaParameterizedTestRunner" failure_message: "To use OppiaParameterizedTestRunner, please add an exemption to file_content_validation_checks.textproto and add an explanation for your use case in your PR description. Note that parameterized tests should only be used in special circumstances where a single behavior can be tested across multiple inputs, or for especially large test suites that can be trivially reduced." exempted_file_name: "scripts/src/javatests/org/oppia/android/scripts/regex/RegexPatternValidationCheckTest.kt" + exempted_file_name: "utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt" exempted_file_name: "utility/src/test/java/org/oppia/android/util/math/MathTokenizerTest.kt" exempted_file_patterns: "testing/src/main/java/org/oppia/android/testing/junit/.+?\\.kt" } diff --git a/scripts/assets/test_file_exemptions.textproto b/scripts/assets/test_file_exemptions.textproto index 8fc1bd8d33a..b17ae7d5abb 100644 --- a/scripts/assets/test_file_exemptions.textproto +++ b/scripts/assets/test_file_exemptions.textproto @@ -656,8 +656,8 @@ exempted_file_path: "testing/src/main/java/org/oppia/android/testing/junit/Param exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/ComparableOperationListSubject.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/FractionSubject.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt" -exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/MathParsingErrorSubject.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/MathEquationSubject.kt" +exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/MathParsingErrorSubject.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/PolynomialSubject.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/RealSubject.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/TokenSubject.kt" diff --git a/testing/src/main/java/org/oppia/android/testing/math/BUILD.bazel b/testing/src/main/java/org/oppia/android/testing/math/BUILD.bazel index d60f0f8aafa..f92150592c3 100644 --- a/testing/src/main/java/org/oppia/android/testing/math/BUILD.bazel +++ b/testing/src/main/java/org/oppia/android/testing/math/BUILD.bazel @@ -87,7 +87,7 @@ kt_android_library( ":math_expression_subject", ":real_subject", "//third_party:com_google_truth_truth", - "//utility/src/main/java/org/oppia/android/util/math:parsing_error", + "//utility/src/main/java/org/oppia/android/util/math:math_parsing_error", ], ) diff --git a/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel b/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel index bb33e61046d..91b1f83ddf6 100644 --- a/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel @@ -34,31 +34,31 @@ kt_android_library( ) kt_android_library( - name = "parsing_error", + name = "math_expression_parser", srcs = [ - "MathParsingError.kt", + "MathExpressionParser.kt", ], visibility = [ - "//:oppia_api_visibility", + "//:oppia_testing_visibility", ], deps = [ + ":extensions", + ":math_parsing_error", + ":peekable_iterator", + ":tokenizer", "//model/src/main/proto:math_java_proto_lite", ], ) kt_android_library( - name = "parser", + name = "math_parsing_error", srcs = [ - "MathExpressionParser.kt", + "MathParsingError.kt", ], visibility = [ - "//:oppia_testing_visibility", + "//:oppia_api_visibility", ], deps = [ - ":extensions", - ":parsing_error", - ":peekable_iterator", - ":tokenizer", "//model/src/main/proto:math_java_proto_lite", ], ) diff --git a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel index 49a3c4c73fe..b6bebcc07d3 100644 --- a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel @@ -19,7 +19,7 @@ oppia_android_test( "//third_party:junit_junit", "//third_party:org_robolectric_robolectric", "//third_party:robolectric_android-all", - "//utility/src/main/java/org/oppia/android/util/math:parser", + "//utility/src/main/java/org/oppia/android/util/math:math_expression_parser", ], ) @@ -38,7 +38,7 @@ oppia_android_test( "//third_party:junit_junit", "//third_party:org_robolectric_robolectric", "//third_party:robolectric_android-all", - "//utility/src/main/java/org/oppia/android/util/math:parser", + "//utility/src/main/java/org/oppia/android/util/math:math_expression_parser", ], ) @@ -112,7 +112,7 @@ oppia_android_test( "//third_party:junit_junit", "//third_party:org_robolectric_robolectric", "//third_party:robolectric_android-all", - "//utility/src/main/java/org/oppia/android/util/math:parser", + "//utility/src/main/java/org/oppia/android/util/math:math_expression_parser", ], ) @@ -151,7 +151,7 @@ oppia_android_test( "//third_party:junit_junit", "//third_party:org_robolectric_robolectric", "//third_party:robolectric_android-all", - "//utility/src/main/java/org/oppia/android/util/math:parser", + "//utility/src/main/java/org/oppia/android/util/math:math_expression_parser", ], ) From bbf7e2dfd88760c77f28ac09f3f1c76df4cfbf7e Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 27 Jan 2022 13:48:38 -0800 Subject: [PATCH 072/162] Add new test for negation with math symbol. --- .../util/math/NumericExpressionParserTest.kt | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt index b34df9875f6..93628887ae7 100644 --- a/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt @@ -244,6 +244,21 @@ class NumericExpressionParserTest { } } + @Test + fun testParse_negation_withMathSymbol_returnsExpressionWithUnaryOperation() { + val expression = parseNumericExpressionWithAllErrors("−2") + + assertThat(expression).hasStructureThatMatches { + negation { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + @Test fun testParse_positiveUnary_withoutOptionalErrors_returnsExpressionWithUnaryOperation() { val expression = parseNumericExpressionWithoutOptionalErrors("+2") From ba4128c54905b3ea59cb9bc7ed04c54c92044d32 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 27 Jan 2022 14:03:46 -0800 Subject: [PATCH 073/162] Post-merge fixes. --- .../android/util/math/RealExtensionsTest.kt | 43 ++++++++++--------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt b/utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt index 2c0b7581c9e..9760b175ff8 100644 --- a/utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt @@ -11,7 +11,6 @@ import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Iteration import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Parameter import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.RunParameterized import org.oppia.android.testing.math.RealSubject.Companion.assertThat -import org.oppia.android.util.math.FractionParser.Companion.parseFraction import org.robolectric.annotation.LooperMode /** Tests for [Real] extensions. */ @@ -57,6 +56,8 @@ class RealExtensionsTest { private val NEGATIVE_PI_REAL = createIrrationalReal(-PI) } + private val fractionParser by lazy { FractionParser() } + @Parameter var lhsInt: Int = Int.MIN_VALUE @Parameter lateinit var lhsFrac: String @Parameter var lhsDouble: Double = Double.MIN_VALUE @@ -508,7 +509,7 @@ class RealExtensionsTest { val result = lhsReal + rhsReal - val expectedFraction = parseFraction(expFrac) + val expectedFraction = fractionParser.parseFraction(expFrac) assertThat(result).isRationalThat().isEqualTo(expectedFraction) } @@ -549,7 +550,7 @@ class RealExtensionsTest { val result = lhsReal + rhsReal - val expectedFraction = parseFraction(expFrac) + val expectedFraction = fractionParser.parseFraction(expFrac) assertThat(result).isRationalThat().isEqualTo(expectedFraction) } @@ -569,7 +570,7 @@ class RealExtensionsTest { val result = lhsReal + rhsReal - val expectedFraction = parseFraction(expFrac) + val expectedFraction = fractionParser.parseFraction(expFrac) assertThat(result).isRationalThat().isEqualTo(expectedFraction) } @@ -690,7 +691,7 @@ class RealExtensionsTest { val result = lhsReal - rhsReal - val expectedFraction = parseFraction(expFrac) + val expectedFraction = fractionParser.parseFraction(expFrac) assertThat(result).isRationalThat().isEqualTo(expectedFraction) } @@ -731,7 +732,7 @@ class RealExtensionsTest { val result = lhsReal - rhsReal - val expectedFraction = parseFraction(expFrac) + val expectedFraction = fractionParser.parseFraction(expFrac) assertThat(result).isRationalThat().isEqualTo(expectedFraction) } @@ -751,7 +752,7 @@ class RealExtensionsTest { val result = lhsReal - rhsReal - val expectedFraction = parseFraction(expFrac) + val expectedFraction = fractionParser.parseFraction(expFrac) assertThat(result).isRationalThat().isEqualTo(expectedFraction) } @@ -872,7 +873,7 @@ class RealExtensionsTest { val result = lhsReal * rhsReal - val expectedFraction = parseFraction(expFrac) + val expectedFraction = fractionParser.parseFraction(expFrac) assertThat(result).isRationalThat().isEqualTo(expectedFraction) } @@ -913,7 +914,7 @@ class RealExtensionsTest { val result = lhsReal * rhsReal - val expectedFraction = parseFraction(expFrac) + val expectedFraction = fractionParser.parseFraction(expFrac) assertThat(result).isRationalThat().isEqualTo(expectedFraction) } @@ -933,7 +934,7 @@ class RealExtensionsTest { val result = lhsReal * rhsReal - val expectedFraction = parseFraction(expFrac) + val expectedFraction = fractionParser.parseFraction(expFrac) assertThat(result).isRationalThat().isEqualTo(expectedFraction) } @@ -1052,7 +1053,7 @@ class RealExtensionsTest { val result = lhsReal / rhsReal // If the divisor doesn't divide the dividend, the result is a fraction. - val expectedFraction = parseFraction(expFrac) + val expectedFraction = fractionParser.parseFraction(expFrac) assertThat(result).isRationalThat().isEqualTo(expectedFraction) } @@ -1073,7 +1074,7 @@ class RealExtensionsTest { val result = lhsReal / rhsReal - val expectedFraction = parseFraction(expFrac) + val expectedFraction = fractionParser.parseFraction(expFrac) assertThat(result).isRationalThat().isEqualTo(expectedFraction) } @@ -1114,7 +1115,7 @@ class RealExtensionsTest { val result = lhsReal / rhsReal - val expectedFraction = parseFraction(expFrac) + val expectedFraction = fractionParser.parseFraction(expFrac) assertThat(result).isRationalThat().isEqualTo(expectedFraction) } @@ -1134,7 +1135,7 @@ class RealExtensionsTest { val result = lhsReal / rhsReal - val expectedFraction = parseFraction(expFrac) + val expectedFraction = fractionParser.parseFraction(expFrac) assertThat(result).isRationalThat().isEqualTo(expectedFraction) } @@ -1336,7 +1337,7 @@ class RealExtensionsTest { val result = lhsReal pow rhsReal // Integers raised to a negative integer yields a fraction since x^-y=1/(x^y). - val expectedFraction = parseFraction(expFrac) + val expectedFraction = fractionParser.parseFraction(expFrac) assertThat(result).isRationalThat().isEqualTo(expectedFraction) } @@ -1362,7 +1363,7 @@ class RealExtensionsTest { val result = lhsReal pow rhsReal - val expectedFraction = parseFraction(expFrac) + val expectedFraction = fractionParser.parseFraction(expFrac) assertThat(result).isRationalThat().isEqualTo(expectedFraction) } @@ -1423,7 +1424,7 @@ class RealExtensionsTest { val result = lhsReal pow rhsReal - val expectedFraction = parseFraction(expFrac) + val expectedFraction = fractionParser.parseFraction(expFrac) assertThat(result).isRationalThat().isEqualTo(expectedFraction) } @@ -1445,7 +1446,7 @@ class RealExtensionsTest { val result = lhsReal pow rhsReal - val expectedFraction = parseFraction(expFrac) + val expectedFraction = fractionParser.parseFraction(expFrac) assertThat(result).isRationalThat().isEqualTo(expectedFraction) } @@ -1772,15 +1773,15 @@ class RealExtensionsTest { assertThat(result).isIrrationalThat().isWithin(1e-5).of(1.772004515) } + + private fun createRationalReal(rawFractionExpression: String) = + createRationalReal(fractionParser.parseFractionFromString(rawFractionExpression)) } private fun createIntegerReal(value: Int) = Real.newBuilder().apply { integer = value }.build() -private fun createRationalReal(rawFractionExpression: String) = - createRationalReal(parseFraction(rawFractionExpression)) - private fun createRationalReal(value: Fraction) = Real.newBuilder().apply { rational = value }.build() From fd097585daf150d7d141ee560591df5433cb9d9f Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 27 Jan 2022 15:17:00 -0800 Subject: [PATCH 074/162] Add KDocs. Also, add new regex exemption for new parameterized tests in this branch. --- .../file_content_validation_checks.textproto | 1 + .../testing/math/MathEquationSubject.kt | 12 ++ .../testing/math/MathExpressionSubject.kt | 33 +++++ .../util/math/ExpressionToLatexConverter.kt | 48 ++++++- .../android/util/math/FractionExtensions.kt | 19 ++- .../util/math/MathExpressionExtensions.kt | 21 ++- .../util/math/NumericExpressionEvaluator.kt | 46 ++++++ .../oppia/android/util/math/RealExtensions.kt | 132 ++++++++++++++++-- .../util/math/NumericExpressionParserTest.kt | 1 + 9 files changed, 295 insertions(+), 18 deletions(-) diff --git a/scripts/assets/file_content_validation_checks.textproto b/scripts/assets/file_content_validation_checks.textproto index a41099287d7..8b8fe90e4cd 100644 --- a/scripts/assets/file_content_validation_checks.textproto +++ b/scripts/assets/file_content_validation_checks.textproto @@ -289,5 +289,6 @@ file_content_checks { exempted_file_name: "scripts/src/javatests/org/oppia/android/scripts/regex/RegexPatternValidationCheckTest.kt" exempted_file_name: "utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt" exempted_file_name: "utility/src/test/java/org/oppia/android/util/math/MathTokenizerTest.kt" + exempted_file_name: "utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt" exempted_file_patterns: "testing/src/main/java/org/oppia/android/testing/junit/.+?\\.kt" } diff --git a/testing/src/main/java/org/oppia/android/testing/math/MathEquationSubject.kt b/testing/src/main/java/org/oppia/android/testing/math/MathEquationSubject.kt index 1eaa1c821df..859ff36db29 100644 --- a/testing/src/main/java/org/oppia/android/testing/math/MathEquationSubject.kt +++ b/testing/src/main/java/org/oppia/android/testing/math/MathEquationSubject.kt @@ -36,9 +36,21 @@ class MathEquationSubject private constructor( */ fun hasRightHandSideThat(): MathExpressionSubject = assertThat(actual.rightSide) + /** + * Returns a [StringSubject] to verify the LaTeX conversion of the tested [MathEquation]. + * + * For more details on LaTeX conversion, see [toRawLatex]. Note that this method, in contrast to + * [convertsWithFractionsToLatexStringThat], retains division operations as-is. + */ fun convertsToLatexStringThat(): StringSubject = assertThat(convertToLatex(divAsFraction = false)) + /** + * Returns a [StringSubject] to verify the LaTeX conversion of the tested [MathEquation]. + * + * For more details on LaTeX conversion, see [toRawLatex]. Note that this method, in contrast to + * [convertsToLatexStringThat], treats divisions as fractions. + */ fun convertsWithFractionsToLatexStringThat(): StringSubject = assertThat(convertToLatex(divAsFraction = true)) diff --git a/testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt b/testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt index 18e5699675c..1d4298ff3ac 100644 --- a/testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt +++ b/testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt @@ -95,18 +95,51 @@ class MathExpressionSubject private constructor( ExpressionComparator.createFromExpression(actual).also(init) } + /** + * Assumes that this expression evaluates to a fraction (i.e. [Real.getRational]) and returns a + * [FractionSubject] to verify the computed value. + * + * Note that this should only be used for numeric expressions as variable expressions cannot be + * evaluated. For more context on expression evaluation, see [evaluateAsNumericExpression]. + */ fun evaluatesToRationalThat(): FractionSubject = FractionSubject.assertThat(evaluateAsReal(expectedType = Real.RealTypeCase.RATIONAL).rational) + /** + * Assumes that this expression evaluates to an irrational (i.e. [Real.getIrrational]) and returns + * a [DoubleSubject] to verify the computed value. + * + * Note that this should only be used for numeric expressions as variable expressions cannot be + * evaluated. For more context on expression evaluation, see [evaluateAsNumericExpression]. + */ fun evaluatesToIrrationalThat(): DoubleSubject = assertThat(evaluateAsReal(expectedType = Real.RealTypeCase.IRRATIONAL).irrational) + /** + * Assumes that this expression evaluates to an integer (i.e. [Real.getInteger]) and returns an + * [IntegerSubject] to verify the computed value. + * + * Note that this should only be used for numeric expressions as variable expressions cannot be + * evaluated. For more context on expression evaluation, see [evaluateAsNumericExpression]. + */ fun evaluatesToIntegerThat(): IntegerSubject = assertThat(evaluateAsReal(expectedType = Real.RealTypeCase.INTEGER).integer) + /** + * Returns a [StringSubject] to verify the LaTeX conversion of the tested [MathExpression]. + * + * For more details on LaTeX conversion, see [toRawLatex]. Note that this method, in contrast to + * [convertsWithFractionsToLatexStringThat], retains division operations as-is. + */ fun convertsToLatexStringThat(): StringSubject = assertThat(convertToLatex(divAsFraction = false)) + /** + * Returns a [StringSubject] to verify the LaTeX conversion of the tested [MathExpression]. + * + * For more details on LaTeX conversion, see [toRawLatex]. Note that this method, in contrast to + * [convertsToLatexStringThat], treats divisions as fractions. + */ fun convertsWithFractionsToLatexStringThat(): StringSubject = assertThat(convertToLatex(divAsFraction = true)) diff --git a/utility/src/main/java/org/oppia/android/util/math/ExpressionToLatexConverter.kt b/utility/src/main/java/org/oppia/android/util/math/ExpressionToLatexConverter.kt index 2c108f02fa7..05cf8a25e90 100644 --- a/utility/src/main/java/org/oppia/android/util/math/ExpressionToLatexConverter.kt +++ b/utility/src/main/java/org/oppia/android/util/math/ExpressionToLatexConverter.kt @@ -22,14 +22,33 @@ import org.oppia.android.app.model.MathUnaryOperation.Operator.POSITIVE import org.oppia.android.app.model.MathBinaryOperation.Operator as BinaryOperator import org.oppia.android.app.model.MathUnaryOperation.Operator as UnaryOperator +/** + * Converter between math equations/expressions and renderable LaTeX strings. + * + * In order to use this converter, directly import [convertToLatex] and call it for any + * [MathExpression]s or [MathEquation]s that should be converted to a renderable LaTeX + * representation. + */ class ExpressionToLatexConverter private constructor() { companion object { - fun MathEquation.convertToLatex(divAsFraction: Boolean): String { - val lhs = leftSide - val rhs = rightSide - return "${lhs.convertToLatex(divAsFraction)} = ${rhs.convertToLatex(divAsFraction)}" - } - + /** + * Returns the LaTeX conversion of this [MathExpression]. + * + * Note that this routine attempts to retain the exact structure of the original expression, but + * not the actual original style. For example, parenthetical groups will be retained but spacing + * between operators will be normalized regardless of the original raw expression. + * + * Note that the returned LaTeX is primarily intended to be render-ready, and may not be as + * nicely human-readable. While some effort is taken to add spacing for better human + * readability, there may be extra curly braces or LaTeX structures to generally ensure + * correct rendering. + * + * Finally, the returned LaTeX should generally be portable/compatible with most LaTeX rendering + * systems as it only relies on basic LaTeX language structures. + * + * @param divAsFraction determines whether divisions within the math structure should be + * rendered instead as fractions rather than division operations + */ fun MathExpression.convertToLatex(divAsFraction: Boolean): String { return when (expressionTypeCase) { CONSTANT -> constant.toPlainText() @@ -47,6 +66,7 @@ class ExpressionToLatexConverter private constructor() { "\\frac{$lhsLatex}{$rhsLatex}" } else "$lhsLatex \\div $rhsLatex" EXPONENTIATE -> "$lhsLatex ^ {$rhsLatex}" + // There's no operator, so try and "recover" by outputting the raw operands. BinaryOperator.OPERATOR_UNSPECIFIED, BinaryOperator.UNRECOGNIZED, null -> "$lhsLatex $rhsLatex" } @@ -56,6 +76,7 @@ class ExpressionToLatexConverter private constructor() { when (unaryOperation.operator) { NEGATE -> "-$operandLatex" POSITIVE -> "+$operandLatex" + // There's no known operator, so just output the original operand. UnaryOperator.OPERATOR_UNSPECIFIED, UnaryOperator.UNRECOGNIZED, null -> operandLatex } } @@ -63,12 +84,25 @@ class ExpressionToLatexConverter private constructor() { val argumentLatex = functionCall.argument.convertToLatex(divAsFraction) when (functionCall.functionType) { SQUARE_ROOT -> "\\sqrt{$argumentLatex}" + // There's no recognized function, so try to "recover" by outputting the raw argument. FUNCTION_UNSPECIFIED, FunctionType.UNRECOGNIZED, null -> argumentLatex } } GROUP -> "(${group.convertToLatex(divAsFraction)})" - EXPRESSIONTYPE_NOT_SET, null -> "" + EXPRESSIONTYPE_NOT_SET, null -> "" // No corresponding LaTeX, so just go with empty string. } } + + /** + * Returns the LaTeX conversion of this [MathEquation]. + * + * See [convertToLatex] (for [MathExpression]s) for the specific behaviors and expectations of + * this function. + */ + fun MathEquation.convertToLatex(divAsFraction: Boolean): String { + val lhs = leftSide + val rhs = rightSide + return "${lhs.convertToLatex(divAsFraction)} = ${rhs.convertToLatex(divAsFraction)}" + } } } diff --git a/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt index 77e1e7ef420..8a91dff56a2 100644 --- a/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt @@ -188,7 +188,24 @@ operator fun Fraction.div(rhs: Fraction): Fraction { return this * rhs.toInvertedImproperForm() } -// TODO: document 0^0 case. +/** + * Raises this [Fraction] to the specified [exp] power and returns the result. + * + * Note that since this is an infix operation it should be used as follows (as an example): + * ```kotlin + * val result = fraction pow integerPower + * ``` + * + * This function can only fail when (exceptions are thrown in all cases): + * - This [Fraction] is malformed or incomplete (e.g. a default instance). + * - The resulting [Fraction] would result in a zero denominator. + * + * Some specific details about the returned value: + * - A proper-form fraction is always returned (per [toProperForm]). + * - Negative powers are supported (they will invert the resulting fraction). + * - 0^0 is special-cased to return a 1-valued fraction for consistency with the power function for + * reals (see that KDoc and/or https://stackoverflow.com/a/19955996 for context). + */ infix fun Fraction.pow(exp: Int): Fraction { return when { exp == 0 -> { diff --git a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt index 59b203a0d2d..9da1082f21e 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt @@ -6,8 +6,25 @@ import org.oppia.android.app.model.Real import org.oppia.android.util.math.ExpressionToLatexConverter.Companion.convertToLatex import org.oppia.android.util.math.NumericExpressionEvaluator.Companion.evaluate -fun MathEquation.toRawLatex(divAsFraction: Boolean): String = convertToLatex(divAsFraction) - +/** + * Returns the LaTeX conversion of this [MathExpression], with the style configuration determined by + * [divAsFraction]. + * + * See [convertToLatex] for specifics. + */ fun MathExpression.toRawLatex(divAsFraction: Boolean): String = convertToLatex(divAsFraction) +/** + * Returns the LaTeX conversion of this [MathEquation], with the style configuration determined by + * [divAsFraction]. + * + * See [convertToLatex] for specifics. + */ +fun MathEquation.toRawLatex(divAsFraction: Boolean): String = convertToLatex(divAsFraction) + +/** + * Returns the [Real] evaluation of this [MathExpression]. + * + * See [evaluate] for specifics. + */ fun MathExpression.evaluateAsNumericExpression(): Real? = evaluate() diff --git a/utility/src/main/java/org/oppia/android/util/math/NumericExpressionEvaluator.kt b/utility/src/main/java/org/oppia/android/util/math/NumericExpressionEvaluator.kt index 766da590400..1b314675ea7 100644 --- a/utility/src/main/java/org/oppia/android/util/math/NumericExpressionEvaluator.kt +++ b/utility/src/main/java/org/oppia/android/util/math/NumericExpressionEvaluator.kt @@ -25,8 +25,54 @@ import org.oppia.android.app.model.Real import org.oppia.android.app.model.MathBinaryOperation.Operator as BinaryOperator import org.oppia.android.app.model.MathUnaryOperation.Operator as UnaryOperator +/** + * Numeric evaluator for numeric [MathExpression]s. + * + * In order to use this evaluator, directly import [evaluate] and call it for any numeric + * [MathExpression]s that should be evaluated. + */ class NumericExpressionEvaluator private constructor() { companion object { + /** + * Evaluates a math expression. + * + * This function only works with numeric expressions since variable expressions have no means + * for evaluation (so they'll always result in a ``null`` return value). + * + * The function generally attempts to retain the most precise representation of a value in the + * following order (from highest priority to lowest): + * 1. Integers + * 2. Fractions (rational values) + * 3. Doubles (irrational values) + * + * Doubles will only be used if there's no other choice as they do not have perfect precision + * unlike the other two structures. Further, it's possible for doubles to be used in cases where + * an integer could work, or fractions to represent whole integers (due to quirks in underlying + * routines). That being said, within a certain precision threshold values returned by this + * function should be deterministic across multiple calls (for the same [MathExpression]). + * + * There are a number of cases where this function will fail: + * - When trying to evaluate a variable expression. + * - When trying to evaluate an invalid [MathExpression] (i.e. one of the substructures within + * the expression is not actually initialized per the proto structures). + * - When trying to perform an impossible math operation (such as divide by zero). Note that + * this will sometimes result in a [Real] being returned with a value like NaN or infinity, + * and other times may result in an exception being thrown. + * + * Note that there's no guard against overflowing values during computation, so care should be + * taken by the caller that this is possible for certain expressions. + * + * For more specifics on the constituent operations that "power" this function, see: + * - [Real.plus] + * - [Real.minus] + * - [Real.times] + * - [Real.div] + * - [Real.pow] + * - [Real.unaryMinus] + * - [sqrt] + * + * @return the [Real] representing the evaluated expression, or ``null`` if something went wrong + */ fun MathExpression.evaluate(): Real? { return when (expressionTypeCase) { CONSTANT -> constant diff --git a/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt index 6e8c01f33f4..adb977396f5 100644 --- a/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt @@ -16,6 +16,12 @@ import kotlin.math.pow */ fun Real.isRational(): Boolean = realTypeCase == RATIONAL +/** + * Returns whether this [Real] is explicitly an integer. + * + * This returns false if the real is a rational despite that being mathematically an integer (e.g. a + * whole number fraction). + */ fun Real.isInteger(): Boolean = realTypeCase == INTEGER /** Returns whether this [Real] is negative. */ @@ -81,6 +87,23 @@ operator fun Real.unaryMinus(): Real { } } +/** + * Adds this [Real] with another and returns the result. + * + * Neither [Real] being added are changed during the operation. + * + * Note that this function will always succeed (unless one of the [Real]s is malformed or + * incomplete), but the type of [Real] that's returned depends on the constituent [Real]s being + * added. For reference, here's how the conversion behaves: + * + * |---------------------------------------------------| + * | + | integer | rational | irrational | + * |------------|------------|------------|------------| + * | integer | integer | rational | irrational | + * | rational | rational | rational | irrational | + * | irrational | irrational | irrational | irrational | + * |---------------------------------------------------| + */ operator fun Real.plus(rhs: Real): Real { return combine( this, rhs, Fraction::plus, Fraction::plus, Fraction::plus, Double::plus, Double::plus, @@ -88,6 +111,15 @@ operator fun Real.plus(rhs: Real): Real { ) } +/** + * Subtracts this [Real] from another and returns the result. + * + * Neither [Real] being subtracted are changed during the operation. + * + * Note that this function will always succeed (unless one of the [Real]s is malformed or + * incomplete), but the type of [Real] that's returned depends on the constituent [Real]s being + * subtracted. For reference, see [Real.plus] (the same type conversion is used). + */ operator fun Real.minus(rhs: Real): Real { return combine( this, rhs, Fraction::minus, Fraction::minus, Fraction::minus, Double::minus, Double::minus, @@ -95,6 +127,18 @@ operator fun Real.minus(rhs: Real): Real { ) } +/** + * Multiplies this [Real] with another and returns the result. + * + * Neither [Real] being multiplied are changed during the operation. + * + * Note that this function will always succeed (unless one of the [Real]s is malformed or + * incomplete), but the type of [Real] that's returned depends on the constituent [Real]s being + * multiplied. For reference, see [Real.plus] (the same type conversion is used). + * + * Note that effective divisions by zero (i.e. fractions with zero denominators) may result in + * either an infinity being returned or an exception being thrown. + */ operator fun Real.times(rhs: Real): Real { return combine( this, rhs, Fraction::times, Fraction::times, Fraction::times, Double::times, Double::times, @@ -102,6 +146,18 @@ operator fun Real.times(rhs: Real): Real { ) } +/** + * Divides this [Real] by another and returns the result. + * + * Neither [Real] being divided are changed during the operation. + * + * Note that this function will always succeed (unless one of the [Real]s is malformed or + * incomplete), but the type of [Real] that's returned depends on the constituent [Real]s being + * divided. For reference, see [Real.plus] (the same type conversion is used). + * + * Note also that divisions by zero may result in either an exception being thrown, or an infinity + * being returned. + */ operator fun Real.div(rhs: Real): Real { return combine( this, rhs, Fraction::div, Fraction::div, Fraction::div, Double::div, Double::div, Double::div, @@ -109,14 +165,49 @@ operator fun Real.div(rhs: Real): Real { ) } -// TODO: document that roots represents the real value representation vs. principal root. Also, -// document 0^0 case per https://stackoverflow.com/a/19955996. -// Rules: -// - Anything involving a double always becomes a double. -// - Int^Int stays int unless it's negative (then it becomes a fraction) -// - Int^Fraction is treated as a fraction power & root (it becomes fraction or double) -// - Fraction^Int always yields a fraction -// - Fraction^Fraction yields a fraction or double (depending on the denominator root) +/** + * Computes the power of this [Real] raised to [rhs] and returns the result. + * + * Neither [Real] being combined are changed during the operation. + * + * As this is an infix function, it should be called as so (example): + * ```kotlin + * val result = baseReal pow powerReal + * ``` + * + * This function can fail in a few circumstances: + * - One of the [Real]s is malformed or incomplete (such as a default instance). + * - In cases where a root is being taken (i.e. when |[rhs]| < 1), if the root cannot be taken + * either an exception will be thrown or NaN will be returned (such as trying to take the even + * root of a negative value). + * + * Further, note that this function represents the real value root rather than the principal root, + * so negative bases are allowed so long as the root being used is odd. For non-integerlike powers, + * the base should never be negative except for fractions that could result in a positive base after + * exponentiation. + * + * This function special cases 0^0 to return 1 in all cases for consistency with the system ``pow`` + * function and other languages, per: https://stackoverflow.com/a/19955996. + * + * Finally, this function also attempts to retain maximum precision in much the same way as [sqrt] + * and [Real.plus] except there are more cases when a value may change types. See the following + * table for reference: + * + * |----------------------------------------------------------------------------------------------| + * | pow | positive int | negative int | rootable rational* | other rationals | irrational | + * |------------|--------------|--------------|--------------------|-----------------|------------| + * | integer | integer | rational | rational | irrational | irrational | + * | rational | rational | rational | rational | irrational | irrational | + * | irrational | irrational | irrational | irrational | irrational | irrational | + * |----------------------------------------------------------------------------------------------| + * + * *This corresponds to fraction powers whose denominator (which are treated as roots) can perform a + * perfect square root of either the integer base (for integer [Real]s) or both the numerator and + * denominator integers (for rational [Real]s). + * + * (Note that the left column represents the left-hand side and the top row represents the + * right-hand side of the operation). + */ infix fun Real.pow(rhs: Real): Real { // Powers can really only be effectively done via floats or whole-number only fractions. return when (realTypeCase) { @@ -160,6 +251,31 @@ infix fun Real.pow(rhs: Real): Real { } } +/** + * Returns the square root of the specified [Real]. + * + * [real] is not changed as a result of this operation (a new [Real] value is returned). + * + * Failure cases: + * - An invalid [Real] is passed in (such as a default instance), resulting in an exception being + * thrown. + * - A negative value is passed in (this will either result in an exception or a NaN being + * returned). + * + * Similar to [Real.plus] & other operations, this function attempts to retain as much precision as + * possible by first performing perfect roots before needing to perform a numerical approximation. + * This is achieved by attempting to take perfect integer roots for integer and rational types and, + * if that's not possible, then converting to a double. See the following conversion table for + * reference: + * + * |------------------------------------------------| + * | sqrt | perfect square | all other values | + * |------------|----------------|------------------| + * | integer | integer | irrational | + * | rational | rational | irrational | + * | irrational | irrational | irrational | + * |------------------------------------------------| + */ fun sqrt(real: Real): Real { return when (real.realTypeCase) { RATIONAL -> sqrt(real.rational) diff --git a/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt index a1557bc1745..cf43a7b3955 100644 --- a/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt @@ -286,6 +286,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression).evaluatesToIntegerThat().isEqualTo(-2) } @Test From 01c2326add92c975fa6f79d6df72578e9d84c3ad Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 27 Jan 2022 15:43:51 -0800 Subject: [PATCH 075/162] Refactor & simplify real ext impl. Also, fix/clarify some KDocs. --- .../oppia/android/util/math/RealExtensions.kt | 240 +++++++++--------- 1 file changed, 114 insertions(+), 126 deletions(-) diff --git a/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt index adb977396f5..99434f5e47a 100644 --- a/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt @@ -80,9 +80,9 @@ fun Real.isApproximatelyEqualTo(value: Double): Boolean { */ operator fun Real.unaryMinus(): Real { return when (realTypeCase) { - RATIONAL -> recompute { it.setRational(-rational) } - IRRATIONAL -> recompute { it.setIrrational(-irrational) } - INTEGER -> recompute { it.setInteger(-integer) } + RATIONAL -> createRationalReal(-rational) + IRRATIONAL -> createIrrationalReal(-irrational) + INTEGER -> createIntegerReal(-integer) REALTYPE_NOT_SET, null -> throw IllegalStateException("Invalid real: $this.") } } @@ -103,12 +103,34 @@ operator fun Real.unaryMinus(): Real { * | rational | rational | rational | irrational | * | irrational | irrational | irrational | irrational | * |---------------------------------------------------| + * + * As indicated by the above table, this function attempts to maintain as much precision as possible + * during operations (but will fall back to [Double]s if the calculation would otherwise result in a + * high level of error). While [Double]s don't perfectly capture precision, their error levels are + * generally better than the rounding errors encountered from integer arithmetic. */ operator fun Real.plus(rhs: Real): Real { - return combine( - this, rhs, Fraction::plus, Fraction::plus, Fraction::plus, Double::plus, Double::plus, - Double::plus, Int::plus, Int::plus, Int::add - ) + return when (realTypeCase) { + RATIONAL -> when (rhs.realTypeCase) { + RATIONAL -> createRationalReal(rational + rhs.rational) + IRRATIONAL -> createIrrationalReal(rational.toDouble() + rhs.irrational) + INTEGER -> createRationalReal(rational + rhs.integer.toWholeNumberFraction()) + REALTYPE_NOT_SET, null -> throw IllegalStateException("Invalid real: $rhs.") + } + IRRATIONAL -> when (rhs.realTypeCase) { + RATIONAL -> createIrrationalReal(irrational + rhs.rational.toDouble()) + IRRATIONAL -> createIrrationalReal(irrational + rhs.irrational) + INTEGER -> createIrrationalReal(irrational + rhs.integer) + REALTYPE_NOT_SET, null -> throw IllegalStateException("Invalid real: $rhs.") + } + INTEGER -> when (rhs.realTypeCase) { + RATIONAL -> createRationalReal(integer.toWholeNumberFraction() + rhs.rational) + IRRATIONAL -> createIrrationalReal(integer + rhs.irrational) + INTEGER -> createIntegerReal(integer + rhs.integer) + REALTYPE_NOT_SET, null -> throw IllegalStateException("Invalid real: $rhs.") + } + REALTYPE_NOT_SET, null -> throw IllegalStateException("Invalid real: $this.") + } } /** @@ -121,10 +143,27 @@ operator fun Real.plus(rhs: Real): Real { * subtracted. For reference, see [Real.plus] (the same type conversion is used). */ operator fun Real.minus(rhs: Real): Real { - return combine( - this, rhs, Fraction::minus, Fraction::minus, Fraction::minus, Double::minus, Double::minus, - Double::minus, Int::minus, Int::minus, Int::subtract - ) + return when (realTypeCase) { + RATIONAL -> when (rhs.realTypeCase) { + RATIONAL -> createRationalReal(rational - rhs.rational) + IRRATIONAL -> createIrrationalReal(rational.toDouble() - rhs.irrational) + INTEGER -> createRationalReal(rational - rhs.integer.toWholeNumberFraction()) + REALTYPE_NOT_SET, null -> throw IllegalStateException("Invalid real: $rhs.") + } + IRRATIONAL -> when (rhs.realTypeCase) { + RATIONAL -> createIrrationalReal(irrational - rhs.rational.toDouble()) + IRRATIONAL -> createIrrationalReal(irrational - rhs.irrational) + INTEGER -> createIrrationalReal(irrational - rhs.integer) + REALTYPE_NOT_SET, null -> throw IllegalStateException("Invalid real: $rhs.") + } + INTEGER -> when (rhs.realTypeCase) { + RATIONAL -> createRationalReal(integer.toWholeNumberFraction() - rhs.rational) + IRRATIONAL -> createIrrationalReal(integer - rhs.irrational) + INTEGER -> createIntegerReal(integer - rhs.integer) + REALTYPE_NOT_SET, null -> throw IllegalStateException("Invalid real: $rhs.") + } + REALTYPE_NOT_SET, null -> throw IllegalStateException("Invalid real: $this.") + } } /** @@ -140,10 +179,27 @@ operator fun Real.minus(rhs: Real): Real { * either an infinity being returned or an exception being thrown. */ operator fun Real.times(rhs: Real): Real { - return combine( - this, rhs, Fraction::times, Fraction::times, Fraction::times, Double::times, Double::times, - Double::times, Int::times, Int::times, Int::multiply - ) + return when (realTypeCase) { + RATIONAL -> when (rhs.realTypeCase) { + RATIONAL -> createRationalReal(rational * rhs.rational) + IRRATIONAL -> createIrrationalReal(rational.toDouble() * rhs.irrational) + INTEGER -> createRationalReal(rational * rhs.integer.toWholeNumberFraction()) + REALTYPE_NOT_SET, null -> throw IllegalStateException("Invalid real: $rhs.") + } + IRRATIONAL -> when (rhs.realTypeCase) { + RATIONAL -> createIrrationalReal(irrational * rhs.rational.toDouble()) + IRRATIONAL -> createIrrationalReal(irrational * rhs.irrational) + INTEGER -> createIrrationalReal(irrational * rhs.integer) + REALTYPE_NOT_SET, null -> throw IllegalStateException("Invalid real: $rhs.") + } + INTEGER -> when (rhs.realTypeCase) { + RATIONAL -> createRationalReal(integer.toWholeNumberFraction() * rhs.rational) + IRRATIONAL -> createIrrationalReal(integer * rhs.irrational) + INTEGER -> createIntegerReal(integer * rhs.integer) + REALTYPE_NOT_SET, null -> throw IllegalStateException("Invalid real: $rhs.") + } + REALTYPE_NOT_SET, null -> throw IllegalStateException("Invalid real: $this.") + } } /** @@ -153,16 +209,35 @@ operator fun Real.times(rhs: Real): Real { * * Note that this function will always succeed (unless one of the [Real]s is malformed or * incomplete), but the type of [Real] that's returned depends on the constituent [Real]s being - * divided. For reference, see [Real.plus] (the same type conversion is used). + * divided. For reference, see [Real.plus] for type conversion. It's the same for this method except + * one case: integer divided by integers. If the division is perfect (e.g. 4/2), an integer will be + * returned. Otherwise, a rational [Fraction] will be returned. * * Note also that divisions by zero may result in either an exception being thrown, or an infinity * being returned. */ operator fun Real.div(rhs: Real): Real { - return combine( - this, rhs, Fraction::div, Fraction::div, Fraction::div, Double::div, Double::div, Double::div, - Int::div, Int::div, Int::divide - ) + return when (realTypeCase) { + RATIONAL -> when (rhs.realTypeCase) { + RATIONAL -> createRationalReal(rational / rhs.rational) + IRRATIONAL -> createIrrationalReal(rational.toDouble() / rhs.irrational) + INTEGER -> createRationalReal(rational / rhs.integer.toWholeNumberFraction()) + REALTYPE_NOT_SET, null -> throw IllegalStateException("Invalid real: $rhs.") + } + IRRATIONAL -> when (rhs.realTypeCase) { + RATIONAL -> createIrrationalReal(irrational / rhs.rational.toDouble()) + IRRATIONAL -> createIrrationalReal(irrational / rhs.irrational) + INTEGER -> createIrrationalReal(irrational / rhs.integer) + REALTYPE_NOT_SET, null -> throw IllegalStateException("Invalid real: $rhs.") + } + INTEGER -> when (rhs.realTypeCase) { + RATIONAL -> createRationalReal(integer.toWholeNumberFraction() / rhs.rational) + IRRATIONAL -> createIrrationalReal(integer / rhs.irrational) + INTEGER -> integer.divide(rhs.integer) + REALTYPE_NOT_SET, null -> throw IllegalStateException("Invalid real: $rhs.") + } + REALTYPE_NOT_SET, null -> throw IllegalStateException("Invalid real: $this.") + } } /** @@ -218,17 +293,17 @@ infix fun Real.pow(rhs: Real): Real { RATIONAL -> rhs.rational.toImproperForm().let { power -> (rational pow power.numerator).root(power.denominator, power.isNegative) } - IRRATIONAL -> recompute { it.setIrrational(rational.pow(rhs.irrational)) } - INTEGER -> recompute { it.setRational(rational pow rhs.integer) } + IRRATIONAL -> createIrrationalReal(rational.toDouble().pow(rhs.irrational)) + INTEGER -> createRationalReal(rational pow rhs.integer) REALTYPE_NOT_SET, null -> throw IllegalStateException("Invalid real: $rhs.") } } IRRATIONAL -> { // Left-hand side is a double. when (rhs.realTypeCase) { - RATIONAL -> recompute { it.setIrrational(irrational.pow(rhs.rational)) } - IRRATIONAL -> recompute { it.setIrrational(irrational.pow(rhs.irrational)) } - INTEGER -> recompute { it.setIrrational(irrational.pow(rhs.integer)) } + RATIONAL -> createIrrationalReal(irrational.pow(rhs.rational.toDouble())) + IRRATIONAL -> createIrrationalReal(irrational.pow(rhs.irrational)) + INTEGER -> createIrrationalReal(irrational.pow(rhs.integer)) REALTYPE_NOT_SET, null -> throw IllegalStateException("Invalid real: $rhs.") } } @@ -242,7 +317,7 @@ infix fun Real.pow(rhs: Real): Real { power.denominator, power.isNegative ) } - IRRATIONAL -> recompute { it.setIrrational(integer.toDouble().pow(rhs.irrational)) } + IRRATIONAL -> createIrrationalReal(integer.toDouble().pow(rhs.irrational)) INTEGER -> integer.pow(rhs.integer) REALTYPE_NOT_SET, null -> throw IllegalStateException("Invalid real: $rhs.") } @@ -278,9 +353,9 @@ infix fun Real.pow(rhs: Real): Real { */ fun sqrt(real: Real): Real { return when (real.realTypeCase) { - RATIONAL -> sqrt(real.rational) - IRRATIONAL -> real.recompute { it.setIrrational(kotlin.math.sqrt(real.irrational)) } - INTEGER -> sqrt(real.integer) + RATIONAL -> real.rational.root(base = 2, invert = false) + IRRATIONAL -> createIrrationalReal(kotlin.math.sqrt(real.irrational)) + INTEGER -> root(real.integer, base = 2) REALTYPE_NOT_SET, null -> throw IllegalStateException("Invalid real: $real.") } } @@ -292,30 +367,6 @@ fun sqrt(real: Real): Real { */ fun abs(real: Real): Real = if (real.isNegative()) -real else real -private operator fun Double.plus(rhs: Fraction): Double = this + rhs.toDouble() -private operator fun Fraction.plus(rhs: Double): Double = toDouble() + rhs -private operator fun Fraction.plus(rhs: Int): Fraction = this + rhs.toWholeNumberFraction() -private operator fun Int.plus(rhs: Fraction): Fraction = toWholeNumberFraction() + rhs -private operator fun Double.minus(rhs: Fraction): Double = this - rhs.toDouble() -private operator fun Fraction.minus(rhs: Double): Double = toDouble() - rhs -private operator fun Fraction.minus(rhs: Int): Fraction = this - rhs.toWholeNumberFraction() -private operator fun Int.minus(rhs: Fraction): Fraction = toWholeNumberFraction() - rhs -private operator fun Double.times(rhs: Fraction): Double = this * rhs.toDouble() -private operator fun Fraction.times(rhs: Double): Double = toDouble() * rhs -private operator fun Fraction.times(rhs: Int): Fraction = this * rhs.toWholeNumberFraction() -private operator fun Int.times(rhs: Fraction): Fraction = toWholeNumberFraction() * rhs -private operator fun Double.div(rhs: Fraction): Double = this / rhs.toDouble() -private operator fun Fraction.div(rhs: Double): Double = toDouble() / rhs -private operator fun Fraction.div(rhs: Int): Fraction = this / rhs.toWholeNumberFraction() -private operator fun Int.div(rhs: Fraction): Fraction = toWholeNumberFraction() / rhs - -private fun Int.add(rhs: Int): Real = Real.newBuilder().apply { integer = this@add + rhs }.build() -private fun Int.subtract(rhs: Int): Real = Real.newBuilder().apply { - integer = this@subtract - rhs -}.build() -private fun Int.multiply(rhs: Int): Real = Real.newBuilder().apply { - integer = this@multiply * rhs -}.build() private fun Int.divide(rhs: Int): Real = Real.newBuilder().apply { // If rhs divides this integer, retain the integer. val lhs = this@divide @@ -331,9 +382,6 @@ private fun Int.divide(rhs: Int): Real = Real.newBuilder().apply { } }.build() -private fun Double.pow(rhs: Fraction): Double = this.pow(rhs.toDouble()) -private fun Fraction.pow(rhs: Double): Double = toDouble().pow(rhs) - private fun Int.pow(exp: Int): Real { return when { exp == 0 -> Real.newBuilder().apply { integer = 1 }.build() @@ -348,8 +396,6 @@ private fun Int.pow(exp: Int): Real { } } -private fun sqrt(fraction: Fraction): Real = fraction.root(base = 2, invert = false) - private fun Fraction.root(base: Int, invert: Boolean): Real { check(base > 0) { "Expected base of 1 or higher, not: $base" } @@ -376,8 +422,6 @@ private fun Fraction.root(base: Int, invert: Boolean): Real { } } -private fun sqrt(int: Int): Real = root(int, base = 2) - private fun root(int: Int, base: Int): Real { // First, check if the integer is a root. Base reference for possible methods: // https://www.researchgate.net/post/How-to-decide-if-a-given-number-will-have-integer-square-root-or-not. @@ -440,70 +484,14 @@ private fun root(int: Int, base: Int): Real { private fun Int.isOdd() = this % 2 == 1 -private fun Real.recompute(transform: (Real.Builder) -> Real.Builder): Real { - return transform(newBuilderForType()).build() -} +private fun createRationalReal(value: Fraction): Real = Real.newBuilder().apply { + rational = value +}.build() -// TODO: consider replacing this with inline alternatives since they'll probably be simpler. -private fun combine( - lhs: Real, - rhs: Real, - leftRationalRightRationalOp: (Fraction, Fraction) -> Fraction, - leftRationalRightIrrationalOp: (Fraction, Double) -> Double, - leftRationalRightIntegerOp: (Fraction, Int) -> Fraction, - leftIrrationalRightRationalOp: (Double, Fraction) -> Double, - leftIrrationalRightIrrationalOp: (Double, Double) -> Double, - leftIrrationalRightIntegerOp: (Double, Int) -> Double, - leftIntegerRightRationalOp: (Int, Fraction) -> Fraction, - leftIntegerRightIrrationalOp: (Int, Double) -> Double, - leftIntegerRightIntegerOp: (Int, Int) -> Real, -): Real { - return when (lhs.realTypeCase) { - RATIONAL -> { - // Left-hand side is Fraction. - when (rhs.realTypeCase) { - RATIONAL -> - lhs.recompute { it.setRational(leftRationalRightRationalOp(lhs.rational, rhs.rational)) } - IRRATIONAL -> - lhs.recompute { - it.setIrrational(leftRationalRightIrrationalOp(lhs.rational, rhs.irrational)) - } - INTEGER -> - lhs.recompute { it.setRational(leftRationalRightIntegerOp(lhs.rational, rhs.integer)) } - REALTYPE_NOT_SET, null -> throw IllegalStateException("Invalid real: $rhs.") - } - } - IRRATIONAL -> { - // Left-hand side is a double. - when (rhs.realTypeCase) { - RATIONAL -> - lhs.recompute { - it.setIrrational(leftIrrationalRightRationalOp(lhs.irrational, rhs.rational)) - } - IRRATIONAL -> - lhs.recompute { - it.setIrrational(leftIrrationalRightIrrationalOp(lhs.irrational, rhs.irrational)) - } - INTEGER -> - lhs.recompute { - it.setIrrational(leftIrrationalRightIntegerOp(lhs.irrational, rhs.integer)) - } - REALTYPE_NOT_SET, null -> throw IllegalStateException("Invalid real: $rhs.") - } - } - INTEGER -> { - // Left-hand side is an integer. - when (rhs.realTypeCase) { - RATIONAL -> - lhs.recompute { it.setRational(leftIntegerRightRationalOp(lhs.integer, rhs.rational)) } - IRRATIONAL -> - lhs.recompute { - it.setIrrational(leftIntegerRightIrrationalOp(lhs.integer, rhs.irrational)) - } - INTEGER -> leftIntegerRightIntegerOp(lhs.integer, rhs.integer) - REALTYPE_NOT_SET, null -> throw IllegalStateException("Invalid real: $rhs.") - } - } - REALTYPE_NOT_SET, null -> throw IllegalStateException("Invalid real: $lhs.") - } -} +private fun createIrrationalReal(value: Double): Real = Real.newBuilder().apply { + irrational = value +}.build() + +private fun createIntegerReal(value: Int): Real = Real.newBuilder().apply { + integer = value +}.build() From 98d6939803f3e2cc7f952332e23f15edb81ec6b5 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 27 Jan 2022 16:13:26 -0800 Subject: [PATCH 076/162] Lint fixes. --- .../java/org/oppia/android/util/math/FractionExtensions.kt | 1 - .../main/java/org/oppia/android/util/math/RealExtensions.kt | 2 +- .../android/util/math/ExpressionToLatexConverterTest.kt | 5 +++-- .../android/util/math/NumericExpressionEvaluatorTest.kt | 3 ++- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt index 8a91dff56a2..bd0c8093ede 100644 --- a/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt @@ -236,7 +236,6 @@ fun Int.toWholeNumberFraction(): Fraction { }.build() } - /** Returns the greatest common divisor between two integers. */ private fun gcd(x: Int, y: Int): Int { return if (y == 0) x else gcd(y, x % y) diff --git a/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt index 99434f5e47a..cc3f3687e91 100644 --- a/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt @@ -1,12 +1,12 @@ package org.oppia.android.util.math -import kotlin.math.absoluteValue import org.oppia.android.app.model.Fraction import org.oppia.android.app.model.Real import org.oppia.android.app.model.Real.RealTypeCase.INTEGER import org.oppia.android.app.model.Real.RealTypeCase.IRRATIONAL import org.oppia.android.app.model.Real.RealTypeCase.RATIONAL import org.oppia.android.app.model.Real.RealTypeCase.REALTYPE_NOT_SET +import kotlin.math.absoluteValue import kotlin.math.pow /** diff --git a/utility/src/test/java/org/oppia/android/util/math/ExpressionToLatexConverterTest.kt b/utility/src/test/java/org/oppia/android/util/math/ExpressionToLatexConverterTest.kt index 87be7c78451..1af737e0583 100644 --- a/utility/src/test/java/org/oppia/android/util/math/ExpressionToLatexConverterTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/ExpressionToLatexConverterTest.kt @@ -188,9 +188,10 @@ class ExpressionToLatexConverterTest { ErrorCheckingMode.ALL_ERRORS ).getExpectedSuccess() } - + private fun parseNumericExpressionInternal( - expression: String, errorCheckingMode: ErrorCheckingMode + expression: String, + errorCheckingMode: ErrorCheckingMode ): MathExpression { return MathExpressionParser.parseNumericExpression( expression, errorCheckingMode diff --git a/utility/src/test/java/org/oppia/android/util/math/NumericExpressionEvaluatorTest.kt b/utility/src/test/java/org/oppia/android/util/math/NumericExpressionEvaluatorTest.kt index a16afda5a0e..1f4786bde4b 100644 --- a/utility/src/test/java/org/oppia/android/util/math/NumericExpressionEvaluatorTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/NumericExpressionEvaluatorTest.kt @@ -218,7 +218,8 @@ class NumericExpressionEvaluatorTest { private companion object { private fun parseNumericExpression( - expression: String, errorCheckingMode: ErrorCheckingMode = ALL_ERRORS + expression: String, + errorCheckingMode: ErrorCheckingMode = ALL_ERRORS ): MathExpression { return MathExpressionParser.parseNumericExpression( expression, errorCheckingMode From ca412f7dcb2d24ce9af1bd884a73a0d8f450e685 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 27 Jan 2022 17:32:39 -0800 Subject: [PATCH 077/162] Simplify operation list converter a lot. This inlines three recursive operations to be done during the actual computation to simplify the overall converter complexity (and to make determining the test matrix easier). --- ...ssionToComparableOperationListConverter.kt | 160 +++++++----------- .../util/math/MathExpressionExtensions.kt | 35 +--- .../oppia/android/util/math/RealExtensions.kt | 1 + 3 files changed, 60 insertions(+), 136 deletions(-) diff --git a/utility/src/main/java/org/oppia/android/util/math/ExpressionToComparableOperationListConverter.kt b/utility/src/main/java/org/oppia/android/util/math/ExpressionToComparableOperationListConverter.kt index 4cfa9acec66..ef1008cab54 100644 --- a/utility/src/main/java/org/oppia/android/util/math/ExpressionToComparableOperationListConverter.kt +++ b/utility/src/main/java/org/oppia/android/util/math/ExpressionToComparableOperationListConverter.kt @@ -2,17 +2,15 @@ package org.oppia.android.util.math import org.oppia.android.app.model.ComparableOperationList import org.oppia.android.app.model.ComparableOperationList.CommutativeAccumulation -import org.oppia.android.app.model.ComparableOperationList.CommutativeAccumulation.AccumulationType -import org.oppia.android.app.model.ComparableOperationList.CommutativeAccumulation.AccumulationType.ACCUMULATION_TYPE_UNSPECIFIED import org.oppia.android.app.model.ComparableOperationList.CommutativeAccumulation.AccumulationType.PRODUCT import org.oppia.android.app.model.ComparableOperationList.CommutativeAccumulation.AccumulationType.SUMMATION import org.oppia.android.app.model.ComparableOperationList.ComparableOperation -import org.oppia.android.app.model.ComparableOperationList.ComparableOperation.ComparisonTypeCase.COMPARISONTYPE_NOT_SET import org.oppia.android.app.model.ComparableOperationList.ComparableOperation.ComparisonTypeCase.CONSTANT_TERM import org.oppia.android.app.model.ComparableOperationList.ComparableOperation.ComparisonTypeCase.NON_COMMUTATIVE_OPERATION import org.oppia.android.app.model.ComparableOperationList.ComparableOperation.ComparisonTypeCase.VARIABLE_TERM import org.oppia.android.app.model.ComparableOperationList.NonCommutativeOperation import org.oppia.android.app.model.ComparableOperationList.NonCommutativeOperation.OperationTypeCase +import org.oppia.android.app.model.MathBinaryOperation.Operator as BinaryOperator import org.oppia.android.app.model.MathBinaryOperation.Operator.ADD import org.oppia.android.app.model.MathBinaryOperation.Operator.DIVIDE import org.oppia.android.app.model.MathBinaryOperation.Operator.EXPONENTIATE @@ -28,13 +26,15 @@ import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.UNARY_OPERA import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.VARIABLE import org.oppia.android.app.model.MathFunctionCall.FunctionType import org.oppia.android.app.model.MathFunctionCall.FunctionType.SQUARE_ROOT +import org.oppia.android.app.model.MathUnaryOperation.Operator as UnaryOperator import org.oppia.android.app.model.MathUnaryOperation.Operator.NEGATE import org.oppia.android.app.model.MathUnaryOperation.Operator.POSITIVE -import org.oppia.android.app.model.MathBinaryOperation.Operator as BinaryOperator -import org.oppia.android.app.model.MathUnaryOperation.Operator as UnaryOperator class ExpressionToComparableOperationListConverter private constructor() { companion object { + // TODO: consider eliminating the comparator extensions. Probably should verify full test suite + // & the old tests before deleting the old tests. + private val COMPARABLE_OPERATION_COMPARATOR: Comparator by lazy { // Some of the comparators must be deferred since they indirectly reference this comparator // (which isn't valid until it's fully assembled). @@ -87,9 +87,9 @@ class ExpressionToComparableOperationListConverter private constructor() { ) } - fun MathExpression.toComparable(): ComparableOperationList { + fun MathExpression.toComparableOperationList(): ComparableOperationList { return ComparableOperationList.newBuilder().apply { - rootOperation = toComparableOperation().stabilizeNegation().sort() + rootOperation = toComparableOperation() }.build() } @@ -137,6 +137,7 @@ class ExpressionToComparableOperationListConverter private constructor() { accumulationType = SUMMATION addOperationToSum(binaryOperation.leftOperand, forceNegative = false) addOperationToSum(binaryOperation.rightOperand, forceNegative = isRhsNegative) + sort() }.build() }.build() } @@ -145,8 +146,12 @@ class ExpressionToComparableOperationListConverter private constructor() { return ComparableOperation.newBuilder().apply { commutativeAccumulation = CommutativeAccumulation.newBuilder().apply { accumulationType = PRODUCT - addOperationToProduct(binaryOperation.leftOperand, forceInverse = false) - addOperationToProduct(binaryOperation.rightOperand, forceInverse = isRhsInverted) + val negativeCount = + addOperationToProduct(binaryOperation.leftOperand, forceInverse = false) + + addOperationToProduct(binaryOperation.rightOperand, forceInverse = isRhsInverted) + // If an odd number of terms were negative then the overall product is negative. + isNegated = (negativeCount % 2) != 0 + sort() }.build() }.build() } @@ -165,32 +170,61 @@ class ExpressionToComparableOperationListConverter private constructor() { addOperationToSum(expression.binaryOperation.leftOperand, forceNegative) addOperationToSum(expression.binaryOperation.rightOperand, forceNegative = true) } - else -> if (forceNegative) { - addCombinedOperations(expression.toComparableOperation().makeNegative()) - } else addCombinedOperations(expression.toComparableOperation()) + else -> when { + // Skip groups so that nested operations can be properly combined. + expression.expressionTypeCase == GROUP -> + addOperationToSum(expression.group, forceNegative) + forceNegative -> addCombinedOperations(expression.toComparableOperation().makeNegative()) + else -> addCombinedOperations(expression.toComparableOperation()) + } } } + /** + * Recursively adds [expression] tp the ongoing product [CommutativeAccumulation.Builder] by + * collapsing subsequent products into a single list. + * + * @param forceInverse whether this expression is being divided rather than multiplied + * @return the number of negative operations that were made positive before being added to the + * accumulation + */ private fun CommutativeAccumulation.Builder.addOperationToProduct( expression: MathExpression, forceInverse: Boolean - ) { - when (expression.binaryOperation.operator) { - MULTIPLY -> { + ): Int { + return when { + expression.binaryOperation.operator == MULTIPLY -> { // If the whole operation is inverted, carry it to the left-hand side of the operation. - addOperationToProduct(expression.binaryOperation.leftOperand, forceInverse) - addOperationToProduct(expression.binaryOperation.rightOperand, forceInverse = false) + addOperationToProduct(expression.binaryOperation.leftOperand, forceInverse) + + addOperationToProduct(expression.binaryOperation.rightOperand, forceInverse = false) + } + expression.binaryOperation.operator == DIVIDE -> { + addOperationToProduct(expression.binaryOperation.leftOperand, forceInverse) + + addOperationToProduct(expression.binaryOperation.rightOperand, forceInverse = true) } - DIVIDE -> { - addOperationToProduct(expression.binaryOperation.leftOperand, forceInverse) - addOperationToProduct(expression.binaryOperation.rightOperand, forceInverse = true) + // Skip groups so that nested operations can be properly combined. + expression.expressionTypeCase == GROUP -> + addOperationToProduct(expression.group, forceInverse) + else -> { + val operationExpression = expression.toComparableOperation() + val positiveConvertedOperation = operationExpression.makePositive() + if (forceInverse) { + addCombinedOperations(positiveConvertedOperation.makeInverted()) + } else addCombinedOperations(positiveConvertedOperation) + if (operationExpression.isNegated) 1 else 0 } - else -> if (forceInverse) { - addCombinedOperations(expression.toComparableOperation().makeInverted()) - } else addCombinedOperations(expression.toComparableOperation()) } } + private fun CommutativeAccumulation.Builder.sort() { + // Replace the list operations with a sorted list of operations. Note that the inner elements + // are already sorted since this is called during operation creation time (so nested + // operations would have already been sorted). + val operationsList = combinedOperationsList.toMutableList() + clearCombinedOperations() + addAllCombinedOperations(operationsList.sortedWith(COMPARABLE_OPERATION_COMPARATOR)) + } + private fun MathExpression.toNonCommutativeOperation( setOperation: NonCommutativeOperation.Builder.( NonCommutativeOperation.BinaryOperation @@ -216,85 +250,5 @@ class ExpressionToComparableOperationListConverter private constructor() { private fun ComparableOperation.makeInverted(): ComparableOperation = toBuilder().apply { isInverted = true }.build() - - private fun ComparableOperation.stabilizeNegation(): ComparableOperation { - return when (comparisonTypeCase) { - ComparableOperation.ComparisonTypeCase.COMMUTATIVE_ACCUMULATION -> { - val stabilizedOperations = - commutativeAccumulation.combinedOperationsList.map { it.stabilizeNegation() } - when (commutativeAccumulation.accumulationType) { - SUMMATION -> toBuilder().apply { - commutativeAccumulation = commutativeAccumulation.toBuilder().apply { - clearCombinedOperations() - addAllCombinedOperations(stabilizedOperations) - }.build() - }.build() - PRODUCT -> { - // Negations can be combined for all constituent operations & brought up to the - // top-level operation. - val negativeCount = stabilizedOperations.count { - it.isNegated - } + if (isNegated) 1 else 0 - val positiveOperations = stabilizedOperations.map { it.makePositive() } - toBuilder().apply { - isNegated = (negativeCount % 2) == 1 - commutativeAccumulation = commutativeAccumulation.toBuilder().apply { - clearCombinedOperations() - addAllCombinedOperations(positiveOperations) - }.build() - }.build() - } - ACCUMULATION_TYPE_UNSPECIFIED, AccumulationType.UNRECOGNIZED, null -> this - } - } - NON_COMMUTATIVE_OPERATION -> toBuilder().apply { - // Negation can't be extracted from commutative operations. - nonCommutativeOperation = when (nonCommutativeOperation.operationTypeCase) { - OperationTypeCase.EXPONENTIATION -> nonCommutativeOperation.toBuilder().apply { - exponentiation = nonCommutativeOperation.exponentiation.toBuilder().apply { - leftOperand = nonCommutativeOperation.exponentiation.leftOperand.stabilizeNegation() - rightOperand = - nonCommutativeOperation.exponentiation.rightOperand.stabilizeNegation() - }.build() - }.build() - OperationTypeCase.SQUARE_ROOT -> nonCommutativeOperation.toBuilder().apply { - squareRoot = nonCommutativeOperation.squareRoot.stabilizeNegation() - }.build() - OperationTypeCase.OPERATIONTYPE_NOT_SET, null -> nonCommutativeOperation - } - }.build() - CONSTANT_TERM -> this - VARIABLE_TERM -> this - COMPARISONTYPE_NOT_SET, null -> this - } - } - - private fun ComparableOperation.sort(): ComparableOperation { - return when (comparisonTypeCase) { - ComparableOperation.ComparisonTypeCase.COMMUTATIVE_ACCUMULATION -> toBuilder().apply { - commutativeAccumulation = commutativeAccumulation.toBuilder().apply { - clearCombinedOperations() - // Sort the operations themselves before sorting them relative to each other. - val innerSortedList = commutativeAccumulation.combinedOperationsList.map { it.sort() } - addAllCombinedOperations(innerSortedList.sortedWith(COMPARABLE_OPERATION_COMPARATOR)) - }.build() - }.build() - NON_COMMUTATIVE_OPERATION -> toBuilder().apply { - nonCommutativeOperation = when (nonCommutativeOperation.operationTypeCase) { - OperationTypeCase.EXPONENTIATION -> nonCommutativeOperation.toBuilder().apply { - exponentiation = nonCommutativeOperation.exponentiation.toBuilder().apply { - leftOperand = nonCommutativeOperation.exponentiation.leftOperand.sort() - rightOperand = nonCommutativeOperation.exponentiation.rightOperand.sort() - }.build() - }.build() - OperationTypeCase.SQUARE_ROOT -> nonCommutativeOperation.toBuilder().apply { - squareRoot = nonCommutativeOperation.squareRoot.sort() - }.build() - OperationTypeCase.OPERATIONTYPE_NOT_SET, null -> nonCommutativeOperation - } - }.build() - CONSTANT_TERM, VARIABLE_TERM, COMPARISONTYPE_NOT_SET, null -> this - } - } } } diff --git a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt index 523c3b9a6cc..62ee01b9175 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt @@ -3,15 +3,8 @@ package org.oppia.android.util.math import org.oppia.android.app.model.ComparableOperationList import org.oppia.android.app.model.MathEquation import org.oppia.android.app.model.MathExpression -import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.BINARY_OPERATION -import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.CONSTANT -import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.EXPRESSIONTYPE_NOT_SET -import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.FUNCTION_CALL -import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.GROUP -import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.UNARY_OPERATION -import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.VARIABLE import org.oppia.android.app.model.Real -import org.oppia.android.util.math.ExpressionToComparableOperationListConverter.Companion.toComparable +import org.oppia.android.util.math.ExpressionToComparableOperationListConverter.Companion.toComparableOperationList import org.oppia.android.util.math.ExpressionToLatexConverter.Companion.convertToLatex import org.oppia.android.util.math.NumericExpressionEvaluator.Companion.evaluate @@ -38,28 +31,4 @@ fun MathEquation.toRawLatex(divAsFraction: Boolean): String = convertToLatex(div */ fun MathExpression.evaluateAsNumericExpression(): Real? = evaluate() -fun MathExpression.toComparableOperationList(): ComparableOperationList = - stripGroups().toComparable() - -private fun MathExpression.stripGroups(): MathExpression { - return when (expressionTypeCase) { - BINARY_OPERATION -> toBuilder().apply { - binaryOperation = binaryOperation.toBuilder().apply { - leftOperand = binaryOperation.leftOperand.stripGroups() - rightOperand = binaryOperation.rightOperand.stripGroups() - }.build() - }.build() - UNARY_OPERATION -> toBuilder().apply { - unaryOperation = unaryOperation.toBuilder().apply { - operand = unaryOperation.operand.stripGroups() - }.build() - }.build() - FUNCTION_CALL -> toBuilder().apply { - functionCall = functionCall.toBuilder().apply { - argument = functionCall.argument.stripGroups() - }.build() - }.build() - GROUP -> group.stripGroups() - CONSTANT, VARIABLE, EXPRESSIONTYPE_NOT_SET, null -> this - } -} +fun MathExpression.toComparableOperationList(): ComparableOperationList = toComparableOperationList() diff --git a/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt index 0006df1e5d4..ef0e21a6bcd 100644 --- a/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt @@ -9,6 +9,7 @@ import org.oppia.android.app.model.Real.RealTypeCase.REALTYPE_NOT_SET import kotlin.math.absoluteValue import kotlin.math.pow +// TODO: add tests. val REAL_COMPARATOR: Comparator by lazy { Comparator.comparing(Real::toDouble) } /** From f172bcf594c41a095f1110c8b8fd251ac83f5b88 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 27 Jan 2022 18:43:10 -0800 Subject: [PATCH 078/162] Prepare for new tests. --- .../org/oppia/android/util/math/BUILD.bazel | 24 +- .../util/math/ComparatorExtensionsTest.kt | 20 + ...ToComparableOperationListConverterTest.kt} | 717 ++++++++++++++---- 3 files changed, 600 insertions(+), 161 deletions(-) create mode 100644 utility/src/test/java/org/oppia/android/util/math/ComparatorExtensionsTest.kt rename utility/src/test/java/org/oppia/android/util/math/{ExpressionToComparableOperationListTest.kt => ExpressionToComparableOperationListConverterTest.kt} (66%) diff --git a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel index 1b49c30a3b1..40e676057f9 100644 --- a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel @@ -43,10 +43,26 @@ oppia_android_test( ) oppia_android_test( - name = "ExpressionToComparableOperationListTest", - srcs = ["ExpressionToComparableOperationListTest.kt"], + name = "ComparatorExtensionsTest", + srcs = ["ComparatorExtensionsTest.kt"], custom_package = "org.oppia.android.util.math", - test_class = "org.oppia.android.util.math.ExpressionToComparableOperationListTest", + test_class = "org.oppia.android.util.math.ComparatorExtensionsTest", + test_manifest = "//utility:test_manifest", + deps = [ + "//third_party:androidx_test_ext_junit", + "//third_party:com_google_truth_truth", + "//third_party:junit_junit", + "//third_party:org_robolectric_robolectric", + "//third_party:robolectric_android-all", + "//utility/src/main/java/org/oppia/android/util/math:extensions", + ], +) + +oppia_android_test( + name = "ExpressionToComparableOperationListConverterTest", + srcs = ["ExpressionToComparableOperationListConverterTest.kt"], + custom_package = "org.oppia.android.util.math", + test_class = "org.oppia.android.util.math.ExpressionToComparableOperationListConverterTest", test_manifest = "//utility:test_manifest", deps = [ "//model/src/main/proto:math_java_proto_lite", @@ -57,7 +73,7 @@ oppia_android_test( "//third_party:org_robolectric_robolectric", "//third_party:robolectric_android-all", "//utility/src/main/java/org/oppia/android/util/math:extensions", - "//utility/src/main/java/org/oppia/android/util/math:parser", + "//utility/src/main/java/org/oppia/android/util/math:math_expression_parser", ], ) diff --git a/utility/src/test/java/org/oppia/android/util/math/ComparatorExtensionsTest.kt b/utility/src/test/java/org/oppia/android/util/math/ComparatorExtensionsTest.kt new file mode 100644 index 00000000000..bb27e759bb2 --- /dev/null +++ b/utility/src/test/java/org/oppia/android/util/math/ComparatorExtensionsTest.kt @@ -0,0 +1,20 @@ +package org.oppia.android.util.math + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.annotation.LooperMode + +/** Tests for [Comparator] extensions. */ +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") +@RunWith(AndroidJUnit4::class) +@LooperMode(LooperMode.Mode.PAUSED) +class ComparatorExtensionsTest { + // TODO: finish tests + + @Test + fun test() { + throw Exception() + } +} diff --git a/utility/src/test/java/org/oppia/android/util/math/ExpressionToComparableOperationListTest.kt b/utility/src/test/java/org/oppia/android/util/math/ExpressionToComparableOperationListConverterTest.kt similarity index 66% rename from utility/src/test/java/org/oppia/android/util/math/ExpressionToComparableOperationListTest.kt rename to utility/src/test/java/org/oppia/android/util/math/ExpressionToComparableOperationListConverterTest.kt index d9599c0b32d..144a7ec273e 100644 --- a/utility/src/test/java/org/oppia/android/util/math/ExpressionToComparableOperationListTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/ExpressionToComparableOperationListConverterTest.kt @@ -11,39 +11,204 @@ import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingM import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult import org.robolectric.annotation.LooperMode -/** Tests for [MathExpressionParser]. */ +/** Tests for [ExpressionToComparableOperationListConverter]. */ // SameParameterValue: tests should have specific context included/excluded for readability. @Suppress("SameParameterValue") @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) -class ExpressionToComparableOperationListTest { +class ExpressionToComparableOperationListConverterTest { // TODO: add high-level checks for the three types, but don't test in detail since there are // separate suites. Also, document the separate suites' existence in this suites's KDoc. - @Test - fun testToComparableOperation() { - // TODO: split up & move to separate test suites. Finish test cases (if anymore are needed). + // TODO: use utility directly + // TODO: finish tests. + // TODO: add tests for comparator/sorting & negation simplification? + + /* Operation creation */ + // testConvert_constantExpression_returnsConstantOperation + // testConvert_variableExpression_returnsVariableOperation + // testConvert_addition_returnsSummation + // testConvert_subtraction_returnsSummationOfNegative + // testConvert_multiplication_returnsProduct + // testConvert_division_returnsProductOfInverted + // testConvert_exponentiation_returnsNonCommutativeOperation + // testConvert_squareRoot_returnsNonCommutativeOperation + // testConvert_negatedVariable_returnsNegativeVariableOperation + // testConvert_positiveVariable_returnsVariableOperation + // testConvert_positiveOfNegativeVariable_returnsNegativeVariableOperation + // testConvert_subtractionOfNegative_returnsSummationWithPositives + // testConvert_multipleAdditions_returnsCombinedSummation + // testConvert_multipleSubtractions_returnsCombinedSummation + // testConvert_additionsAndSubtractions_returnsCombinedSummation + // testConvert_additionsWithNestedAdds_returnsCompletelyCombinedSummation + // testConvert_subtractsWithNestedSubs_returnsCompletelyCombinedSummation + // testConvert_additionsAndSubtractionsWithNested_returnsCombinedSummation + // testConvert_multipleMultiplications_returnsCombinedProduct + // testConvert_multipleDivisions_returnsCombinedProduct + // testConvert_multiplicationsAndDivisions_returnsCombinedProduct + // testConvert_multiplicationsWithNestedMults_returnsCompletelyCombinedProduct + // testConvert_divisionsWithNestedDivs_returnsCompletelyCombinedProduct + // testConvert_multiplicationsAndDivisionsWithNested_returnsCombinedProduct + // testConvert_multiplicationWithOneNegative_returnsNegativeProduct + // testConvert_multiplicationWithTwoNegatives_returnsPositiveProduct + // testConvert_multiplicationWithThreeNegatives_returnsNegativeProduct + // testConvert_combinedMultDivWithNested_evenNegatives_returnsPositiveProduct + // testConvert_combinedMultDivWithNested_oddNegatives_returnsNegativeProduct + // testConvert_additionAndExp_returnsSummationWithNonCommutative + // testConvert_additionAndSquareRoot_returnsSummationWithNonCommutative + // testConvert_additionWithinExp_returnsSummationWithinNonCommutative + // testConvert_additionWithinSquareRoot_returnsSummationWithinNonCommutative + // testConvert_multiplicationAndExp_returnsProductWithNonCommutative + // testConvert_multiplicationAndSquareRoot_returnsProductWithNonCommutative + // testConvert_multiplicationWithinExp_returnsProductWithinNonCommutative + // testConvert_multiplicationWithinSquareRoot_returnsProductWithinNonCommutative + // testConvert_additionAndMultiplication_returnsSummationOfProduct + // testConvert_multiplicationAndGroupedAddition_returnsProductOfSummation + + /* Top-level operation sorting */ + // testConvert_additionThenSquareRoot_samePrecedence_returnsOpWithSummationFirst + // testConvert_squareRootThenAddition_samePrecedence_returnsOpWithSummationFirst + // testConvert_additionThenExp_samePrecedence_returnsOpWithSummationFirst + // testConvert_exponentiationThenAddition_samePrecedence_returnsOpWithSummationFirst + // testConvert_constantThenSquareRoot_samePrecedence_returnsOpWithNonCommutativeFirst + // testConvert_squareRootThenConstant_samePrecedence_returnsOpWithNonCommutativeFirst + // testConvert_constantThenExp_samePrecedence_returnsOpWithNonCommutativeFirst + // testConvert_exponentiationThenConstant_samePrecedence_returnsOpWithNonCommutativeFirst + // testConvert_constantThenVariable_samePrecedence_returnsOpWithConstantFirst + // testConvert_variableThenConstant_samePrecedence_returnsOpWithConstantFirst + // testConvert_twoVariables_negatedThenInverted_returnsOpWithNegatedFirst + // testConvert_twoVariables_invertedThenNegated_returnsOpWithNegatedFirst + + /* Accumulator sorting */ + // TODO: mention no tiebreakers since there can't be summations or products adjacent with others + // of the same type. + // testConvert_additionAndMult_samePrecedence_returnsOpWithSummationFirst + // testConvert_multiplicationAndAddition_samePrecedence_returnsOpWithSummationFirst + // testConvert_additionAndMult_samePrecedenceAsNested_returnsOpWithSummationFirst + // testConvert_multiplicationAndAddition_samePrecedenceAsNested_returnsOpWithSummationFirst + + /* Non-commutative sorting */ + // testConvert_addExpThenSqrt_samePrecedence_returnsOpWithExpThenSqrt + // testConvert_addSqrtThenExp_samePrecedence_returnsOpWithExpThenSqrt + // testConvert_addTwoExps_lhs1Const_rhs1Const_lhs2Const_rhs2Const_returnsOpWithExp1Then2 + // ... parameterized: + // const^const const^const + // var^const const^const + // const^var const^const + // var^var const^const + // + // const^const var^const + // var^const var^const + // const^var var^const + // var^var var^const + // + // const^const const^var + // var^const const^var + // const^var const^var + // var^var const^var + // + // const^const var^var + // var^const var^var + // const^var var^var + // var^var var^var + // ... + // testConvert_addTwoSqrts_leftConst_rightConst_returnsOpWithSqrt1ThenSqrt2 + // testConvert_addTwoSqrts_leftVar_rightConst_returnsOpWithSqrt2ThenSqrt1 + // testConvert_addTwoSqrts_leftConst_rightVar_returnsOpWithSqrt1ThenSqrt2 + // testConvert_addTwoSqrts_leftVar_rightVar_returnsOpWithSqrt1ThenSqrt2 + + // testConvert_addTwoExps_lhs1Var_rhs1Const_lhs2Const_rhs2Const_returnsOpWithExp2Then1 + // testConvert_addTwoExps_lhs1Const_rhs1Var_lhs2Const_rhs2Const_returnsOpWithExp2Then1 + // testConvert_addTwoExps_lhs1Const_rhs1Var_lhs2Const_rhs2Var_returnsOpWithExp1Then2 + // testConvert_addTwoExps_lhs1Const_rhs1Var_lhs2Var_rhs2Const_returnsOpWithExp1Then2 + + /* Constant sorting */ + // testConvert_addTwoConstants_leftInteger2_rightInteger3_returnsOpWith2Then3 + // ... parameterized: + // left: 2 right: 3 + // left: 3 right: 2 + // + // left: 2 right: 3.14 + // left: 3.14 right: 2 + // + // left: 4 right: 3.14 + // left: 3.14 right: 4 + // + // left: 3.14 right: 6.28 + // left: 6.28 right: 3.14 + // ... + + /* Variable sorting */ + // testConvert_addTwoVariables_leftX_rightX_returnsOpBothXs + // testConvert_addTwoVariables_leftX_rightY_returnsOpWithXThenY + // ... parameterized: + // x, y; y, x; y, z; z, y; x, y, z; z, y, x + // ... + + /* Combined operations */ + // testConvert_allOperations_withNestedGroups_returnsCorrectlyStructuredAndOrderedOperation + + /* Equivalence checks */ + // testEquals_twoAdditionOps_differentByCommutativity_areEqual + // testEquals_twoAdditionOps_differentByAssociativity_areEqual + // testEquals_twoAdditionOps_differentByAssociativityAndCommutativity_areEqual + // testEquals_twoAdditionOps_differentByValue_areNotEqual + // testEquals_twoAdditionOps_differentByEvaluation_areNotEqual + // testEquals_twoMultOps_differentByCommutativity_areEqual + // testEquals_twoMultOps_differentByAssociativity_areEqual + // testEquals_twoMultOps_differentByAssociativityAndCommutativity_areEqual + // testEquals_twoMultOps_differentByValue_areNotEqual + // testEquals_twoMultOps_differentByEvaluation_areNotEqual + // TODO: for this & the next one, test with three operations (e.g. 2 / 3 / 4). + // testEquals_twoSubOps_same_areEqual + // testEquals_twoSubOps_differentByOrder_areNotEqual + // testEquals_twoSubOps_differentByValue_areNotEqual + // testEquals_twoDivOps_same_areEqual + // testEquals_twoDivOps_differentByOrder_areNotEqual + // testEquals_twoDivOps_differentByValue_areNotEqual + // testEquals_twoExps_same_areEqual + // testEquals_twoExps_differentByOrder_areNotEqual + // testEquals_twoExps_differentByValue_areNotEqual + // testEquals_twoSqrts_same_areEqual + // testEquals_twoSqrts_differentByValue_areNotEqual + // testEquals_twoOps_addsAndSubs_differentByOrder_areEqual + // testEquals_twoOps_multsAndDivs_differentByOrder_areEqual + // testEquals_twoOps_addsSubsMultsAndDivs_differentByOrder_areEqual + // testEquals_twoOps_allOperations_differentByOrder_areEqual + // testEquals_twoOps_allOperations_oneNestedDifferentByValue_areNotEqual + // testEquals_twoOps_allOperations_oneMissingTerm_areNotEqual - val exp1 = parseNumericExpressionSuccessfullyWithAllErrors("1") - assertThat(exp1.toComparableOperationList()).hasStructureThatMatches { + @Test + fun test1() { + // TODO: do something with this + val exp = parseNumericExpressionSuccessfullyWithAllErrors("1") + assertThat(exp.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() constantTerm { withValueThat().isIntegerThat().isEqualTo(1) } } + } - val exp2 = parseNumericExpressionSuccessfullyWithAllErrors("-1") - assertThat(exp2.toComparableOperationList()).hasStructureThatMatches { + @Test + fun test2() { + // TODO: do something with this + val exp = parseNumericExpressionSuccessfullyWithAllErrors("-1") + assertThat(exp.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isTrue() hasInvertedPropertyThat().isFalse() constantTerm { withValueThat().isIntegerThat().isEqualTo(1) } } + } - val exp3 = parseNumericExpressionSuccessfullyWithAllErrors("1+3+4") - assertThat(exp3.toComparableOperationList()).hasStructureThatMatches { + @Test + fun test3() { + // TODO: do something with this + val exp = parseNumericExpressionSuccessfullyWithAllErrors("1+3+4") + assertThat(exp.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { @@ -71,9 +236,13 @@ class ExpressionToComparableOperationListTest { } } } + } - val exp4 = parseNumericExpressionSuccessfullyWithAllErrors("-1-2-3") - assertThat(exp4.toComparableOperationList()).hasStructureThatMatches { + @Test + fun test4() { + // TODO: do something with this + val exp = parseNumericExpressionSuccessfullyWithAllErrors("-1-2-3") + assertThat(exp.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { @@ -101,9 +270,13 @@ class ExpressionToComparableOperationListTest { } } } + } - val exp5 = parseNumericExpressionSuccessfullyWithAllErrors("1+2-3") - assertThat(exp5.toComparableOperationList()).hasStructureThatMatches { + @Test + fun test5() { + // TODO: do something with this + val exp = parseNumericExpressionSuccessfullyWithAllErrors("1+2-3") + assertThat(exp.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { @@ -131,9 +304,13 @@ class ExpressionToComparableOperationListTest { } } } + } - val exp6 = parseNumericExpressionSuccessfullyWithAllErrors("2*3*4") - assertThat(exp6.toComparableOperationList()).hasStructureThatMatches { + @Test + fun test6() { + // TODO: do something with this + val exp = parseNumericExpressionSuccessfullyWithAllErrors("2*3*4") + assertThat(exp.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(PRODUCT) { @@ -161,9 +338,13 @@ class ExpressionToComparableOperationListTest { } } } + } - val exp7 = parseNumericExpressionSuccessfullyWithAllErrors("1-2*3") - assertThat(exp7.toComparableOperationList()).hasStructureThatMatches { + @Test + fun test7() { + // TODO: do something with this + val exp = parseNumericExpressionSuccessfullyWithAllErrors("1-2*3") + assertThat(exp.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { @@ -198,9 +379,13 @@ class ExpressionToComparableOperationListTest { } } } + } - val exp8 = parseNumericExpressionSuccessfullyWithAllErrors("2*3-4") - assertThat(exp8.toComparableOperationList()).hasStructureThatMatches { + @Test + fun test8() { + // TODO: do something with this + val exp = parseNumericExpressionSuccessfullyWithAllErrors("2*3-4") + assertThat(exp.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { @@ -235,9 +420,13 @@ class ExpressionToComparableOperationListTest { } } } + } - val exp9 = parseNumericExpressionSuccessfullyWithAllErrors("1+2*3-4+8*7*6-9") - assertThat(exp9.toComparableOperationList()).hasStructureThatMatches { + @Test + fun test9() { + // TODO: do something with this + val exp = parseNumericExpressionSuccessfullyWithAllErrors("1+2*3-4+8*7*6-9") + assertThat(exp.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { @@ -314,9 +503,13 @@ class ExpressionToComparableOperationListTest { } } } + } - val exp10 = parseNumericExpressionSuccessfullyWithAllErrors("2/3/4") - assertThat(exp10.toComparableOperationList()).hasStructureThatMatches { + @Test + fun test10() { + // TODO: do something with this + val exp = parseNumericExpressionSuccessfullyWithAllErrors("2/3/4") + assertThat(exp.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(PRODUCT) { @@ -344,9 +537,13 @@ class ExpressionToComparableOperationListTest { } } } + } - val exp11 = parseNumericExpressionSuccessfullyWithoutOptionalErrors("2^3^4") - assertThat(exp11.toComparableOperationList()).hasStructureThatMatches { + @Test + fun test11() { + // TODO: do something with this + val exp = parseNumericExpressionSuccessfullyWithoutOptionalErrors("2^3^4") + assertThat(exp.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() nonCommutativeOperation { @@ -383,9 +580,13 @@ class ExpressionToComparableOperationListTest { } } } + } - val exp12 = parseNumericExpressionSuccessfullyWithAllErrors("1+2/3+3") - assertThat(exp12.toComparableOperationList()).hasStructureThatMatches { + @Test + fun test12() { + // TODO: do something with this + val exp = parseNumericExpressionSuccessfullyWithAllErrors("1+2/3+3") + assertThat(exp.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { @@ -427,9 +628,13 @@ class ExpressionToComparableOperationListTest { } } } + } - val exp13 = parseNumericExpressionSuccessfullyWithAllErrors("1+(2/3)+3") - assertThat(exp13.toComparableOperationList()).hasStructureThatMatches { + @Test + fun test13() { + // TODO: do something with this + val exp = parseNumericExpressionSuccessfullyWithAllErrors("1+(2/3)+3") + assertThat(exp.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { @@ -471,9 +676,13 @@ class ExpressionToComparableOperationListTest { } } } + } - val exp14 = parseNumericExpressionSuccessfullyWithAllErrors("1+2^3+3") - assertThat(exp14.toComparableOperationList()).hasStructureThatMatches { + @Test + fun test14() { + // TODO: do something with this + val exp = parseNumericExpressionSuccessfullyWithAllErrors("1+2^3+3") + assertThat(exp.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { @@ -516,9 +725,13 @@ class ExpressionToComparableOperationListTest { } } } + } - val exp15 = parseNumericExpressionSuccessfullyWithAllErrors("1+(2^3)+3") - assertThat(exp15.toComparableOperationList()).hasStructureThatMatches { + @Test + fun test15() { + // TODO: do something with this + val exp = parseNumericExpressionSuccessfullyWithAllErrors("1+(2^3)+3") + assertThat(exp.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { @@ -561,9 +774,13 @@ class ExpressionToComparableOperationListTest { } } } + } - val exp16 = parseNumericExpressionSuccessfullyWithAllErrors("2*3/4*7") - assertThat(exp16.toComparableOperationList()).hasStructureThatMatches { + @Test + fun test16() { + // TODO: do something with this + val exp = parseNumericExpressionSuccessfullyWithAllErrors("2*3/4*7") + assertThat(exp.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(PRODUCT) { @@ -598,9 +815,13 @@ class ExpressionToComparableOperationListTest { } } } + } - val exp17 = parseNumericExpressionSuccessfullyWithAllErrors("2*(3/4)*7") - assertThat(exp17.toComparableOperationList()).hasStructureThatMatches { + @Test + fun test17() { + // TODO: do something with this + val exp = parseNumericExpressionSuccessfullyWithAllErrors("2*(3/4)*7") + assertThat(exp.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(PRODUCT) { @@ -635,9 +856,13 @@ class ExpressionToComparableOperationListTest { } } } + } - val exp18 = parseNumericExpressionSuccessfullyWithAllErrors("-3*sqrt(2)") - assertThat(exp18.toComparableOperationList()).hasStructureThatMatches { + @Test + fun test18() { + // TODO: do something with this + val exp = parseNumericExpressionSuccessfullyWithAllErrors("-3*sqrt(2)") + assertThat(exp.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isTrue() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(PRODUCT) { @@ -664,9 +889,13 @@ class ExpressionToComparableOperationListTest { } } } + } - val exp19 = parseNumericExpressionSuccessfullyWithAllErrors("1+(2+(3+(4+5)))") - assertThat(exp19.toComparableOperationList()).hasStructureThatMatches { + @Test + fun test19() { + // TODO: do something with this + val exp = parseNumericExpressionSuccessfullyWithAllErrors("1+(2+(3+(4+5)))") + assertThat(exp.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { @@ -708,9 +937,13 @@ class ExpressionToComparableOperationListTest { } } } + } - val exp20 = parseNumericExpressionSuccessfullyWithAllErrors("2*(3*(4*(5*6)))") - assertThat(exp20.toComparableOperationList()).hasStructureThatMatches { + @Test + fun test20() { + // TODO: do something with this + val exp = parseNumericExpressionSuccessfullyWithAllErrors("2*(3*(4*(5*6)))") + assertThat(exp.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(PRODUCT) { @@ -752,27 +985,39 @@ class ExpressionToComparableOperationListTest { } } } + } - val exp21 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x") - assertThat(exp21.toComparableOperationList()).hasStructureThatMatches { + @Test + fun test21() { + // TODO: do something with this + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("x") + assertThat(exp.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() variableTerm { withNameThat().isEqualTo("x") } } + } - val exp22 = parseAlgebraicExpressionSuccessfullyWithAllErrors("-x") - assertThat(exp22.toComparableOperationList()).hasStructureThatMatches { + @Test + fun test22() { + // TODO: do something with this + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("-x") + assertThat(exp.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isTrue() hasInvertedPropertyThat().isFalse() variableTerm { withNameThat().isEqualTo("x") } } + } - val exp23 = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+x+y") - assertThat(exp23.toComparableOperationList()).hasStructureThatMatches { + @Test + fun test23() { + // TODO: do something with this + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+x+y") + assertThat(exp.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { @@ -800,9 +1045,13 @@ class ExpressionToComparableOperationListTest { } } } + } - val exp24 = parseAlgebraicExpressionSuccessfullyWithAllErrors("-1-x-y") - assertThat(exp24.toComparableOperationList()).hasStructureThatMatches { + @Test + fun test24() { + // TODO: do something with this + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("-1-x-y") + assertThat(exp.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { @@ -830,9 +1079,13 @@ class ExpressionToComparableOperationListTest { } } } + } - val exp25 = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+x-y") - assertThat(exp25.toComparableOperationList()).hasStructureThatMatches { + @Test + fun test25() { + // TODO: do something with this + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+x-y") + assertThat(exp.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { @@ -860,9 +1113,13 @@ class ExpressionToComparableOperationListTest { } } } + } - val exp26 = parseAlgebraicExpressionSuccessfullyWithAllErrors("2xy") - assertThat(exp26.toComparableOperationList()).hasStructureThatMatches { + @Test + fun test26() { + // TODO: do something with this + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("2xy") + assertThat(exp.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(PRODUCT) { @@ -890,9 +1147,13 @@ class ExpressionToComparableOperationListTest { } } } + } - val exp27 = parseAlgebraicExpressionSuccessfullyWithAllErrors("1-xy") - assertThat(exp27.toComparableOperationList()).hasStructureThatMatches { + @Test + fun test27() { + // TODO: do something with this + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("1-xy") + assertThat(exp.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { @@ -927,9 +1188,13 @@ class ExpressionToComparableOperationListTest { } } } + } - val exp28 = parseAlgebraicExpressionSuccessfullyWithAllErrors("xy-4") - assertThat(exp28.toComparableOperationList()).hasStructureThatMatches { + @Test + fun test28() { + // TODO: do something with this + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("xy-4") + assertThat(exp.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { @@ -964,9 +1229,13 @@ class ExpressionToComparableOperationListTest { } } } + } - val exp29 = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+xy-4+yz-9") - assertThat(exp29.toComparableOperationList()).hasStructureThatMatches { + @Test + fun test29() { + // TODO: do something with this + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+xy-4+yz-9") + assertThat(exp.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { @@ -1036,9 +1305,13 @@ class ExpressionToComparableOperationListTest { } } } + } - val exp30 = parseAlgebraicExpressionSuccessfullyWithAllErrors("2/x/y") - assertThat(exp30.toComparableOperationList()).hasStructureThatMatches { + @Test + fun test30() { + // TODO: do something with this + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("2/x/y") + assertThat(exp.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(PRODUCT) { @@ -1066,9 +1339,13 @@ class ExpressionToComparableOperationListTest { } } } + } - val exp31 = parseAlgebraicExpressionSuccessfullyWithoutOptionalErrors("x^3^4") - assertThat(exp31.toComparableOperationList()).hasStructureThatMatches { + @Test + fun test31() { + // TODO: do something with this + val exp = parseAlgebraicExpressionSuccessfullyWithoutOptionalErrors("x^3^4") + assertThat(exp.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() nonCommutativeOperation { @@ -1105,9 +1382,13 @@ class ExpressionToComparableOperationListTest { } } } + } - val exp32 = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+x/y+z") - assertThat(exp32.toComparableOperationList()).hasStructureThatMatches { + @Test + fun test32() { + // TODO: do something with this + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+x/y+z") + assertThat(exp.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { @@ -1149,9 +1430,13 @@ class ExpressionToComparableOperationListTest { } } } + } - val exp33 = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+(x/y)+z") - assertThat(exp33.toComparableOperationList()).hasStructureThatMatches { + @Test + fun test33() { + // TODO: do something with this + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+(x/y)+z") + assertThat(exp.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { @@ -1193,9 +1478,13 @@ class ExpressionToComparableOperationListTest { } } } + } - val exp34 = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+x^3+y") - assertThat(exp34.toComparableOperationList()).hasStructureThatMatches { + @Test + fun test34() { + // TODO: do something with this + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+x^3+y") + assertThat(exp.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { @@ -1238,9 +1527,13 @@ class ExpressionToComparableOperationListTest { } } } + } - val exp35 = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+(x^3)+y") - assertThat(exp35.toComparableOperationList()).hasStructureThatMatches { + @Test + fun test35() { + // TODO: do something with this + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+(x^3)+y") + assertThat(exp.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { @@ -1283,9 +1576,13 @@ class ExpressionToComparableOperationListTest { } } } + } - val exp36 = parseAlgebraicExpressionSuccessfullyWithAllErrors("2*x/y*z") - assertThat(exp36.toComparableOperationList()).hasStructureThatMatches { + @Test + fun test36() { + // TODO: do something with this + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("2*x/y*z") + assertThat(exp.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(PRODUCT) { @@ -1320,9 +1617,13 @@ class ExpressionToComparableOperationListTest { } } } + } - val exp37 = parseAlgebraicExpressionSuccessfullyWithAllErrors("2*(x/y)*z") - assertThat(exp37.toComparableOperationList()).hasStructureThatMatches { + @Test + fun test37() { + // TODO: do something with this + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("2*(x/y)*z") + assertThat(exp.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(PRODUCT) { @@ -1357,9 +1658,13 @@ class ExpressionToComparableOperationListTest { } } } + } - val exp38 = parseAlgebraicExpressionSuccessfullyWithAllErrors("-2*sqrt(x)") - assertThat(exp38.toComparableOperationList()).hasStructureThatMatches { + @Test + fun test38() { + // TODO: do something with this + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("-2*sqrt(x)") + assertThat(exp.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isTrue() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(PRODUCT) { @@ -1386,9 +1691,13 @@ class ExpressionToComparableOperationListTest { } } } + } - val exp39 = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+(x+(3+(z+y)))") - assertThat(exp39.toComparableOperationList()).hasStructureThatMatches { + @Test + fun test39() { + // TODO: do something with this + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+(x+(3+(z+y)))") + assertThat(exp.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { @@ -1430,9 +1739,13 @@ class ExpressionToComparableOperationListTest { } } } + } - val exp40 = parseAlgebraicExpressionSuccessfullyWithAllErrors("2*(x*(4*(zy)))") - assertThat(exp40.toComparableOperationList()).hasStructureThatMatches { + @Test + fun test40() { + // TODO: do something with this + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("2*(x*(4*(zy)))") + assertThat(exp.toComparableOperationList()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(PRODUCT) { @@ -1474,101 +1787,191 @@ class ExpressionToComparableOperationListTest { } } } + } - // Equality tests: - val list1 = createComparableOperationListFromNumericExpression("(1+2)+3") - val list2 = createComparableOperationListFromNumericExpression("1+(2+3)") - assertThat(list1).isEqualTo(list2) - - val list3 = createComparableOperationListFromNumericExpression("1+2+3") - val list4 = createComparableOperationListFromNumericExpression("3+2+1") - assertThat(list3).isEqualTo(list4) + // TODO: Equality tests: + @Test + fun test41() { + // TODO: do something with this + val firstList = createComparableOperationListFromNumericExpression("(1+2)+3") + val secondList = createComparableOperationListFromNumericExpression("1+(2+3)") + assertThat(firstList).isEqualTo(secondList) + } - val list5 = createComparableOperationListFromNumericExpression("1-2-3") - val list6 = createComparableOperationListFromNumericExpression("-3 + -2 + 1") - assertThat(list5).isEqualTo(list6) + @Test + fun test42() { + // TODO: do something with this + val firstList = createComparableOperationListFromNumericExpression("1+2+3") + val secondList = createComparableOperationListFromNumericExpression("3+2+1") + assertThat(firstList).isEqualTo(secondList) + } - val list7 = createComparableOperationListFromNumericExpression("1-2-3") - val list8 = createComparableOperationListFromNumericExpression("-3-2+1") - assertThat(list7).isEqualTo(list8) + @Test + fun test43() { + // TODO: do something with this + val firstList = createComparableOperationListFromNumericExpression("1-2-3") + val secondList = createComparableOperationListFromNumericExpression("-3 + -2 + 1") + assertThat(firstList).isEqualTo(secondList) + } - val list9 = createComparableOperationListFromNumericExpression("1-2-3") - val list10 = createComparableOperationListFromNumericExpression("-3-2+1") - assertThat(list9).isEqualTo(list10) + @Test + fun test44() { + // TODO: do something with this + val firstList = createComparableOperationListFromNumericExpression("1-2-3") + val secondList = createComparableOperationListFromNumericExpression("-3-2+1") + assertThat(firstList).isEqualTo(secondList) + } - val list11 = createComparableOperationListFromNumericExpression("1-2-3") - val list12 = createComparableOperationListFromNumericExpression("3-2-1") - assertThat(list11).isNotEqualTo(list12) + @Test + fun test45() { + // TODO: do something with this + val firstList = createComparableOperationListFromNumericExpression("1-2-3") + val secondList = createComparableOperationListFromNumericExpression("-3-2+1") + assertThat(firstList).isEqualTo(secondList) + } - val list13 = createComparableOperationListFromNumericExpression("2*3*4") - val list14 = createComparableOperationListFromNumericExpression("4*3*2") - assertThat(list13).isEqualTo(list14) + @Test + fun test46() { + // TODO: do something with this + val firstList = createComparableOperationListFromNumericExpression("1-2-3") + val secondList = createComparableOperationListFromNumericExpression("3-2-1") + assertThat(firstList).isNotEqualTo(secondList) + } - val list15 = createComparableOperationListFromNumericExpression("2*(3/4)") - val list16 = createComparableOperationListFromNumericExpression("3/4*2") - assertThat(list15).isEqualTo(list16) + @Test + fun test47() { + // TODO: do something with this + val firstList = createComparableOperationListFromNumericExpression("2*3*4") + val secondList = createComparableOperationListFromNumericExpression("4*3*2") + assertThat(firstList).isEqualTo(secondList) + } - val list17 = createComparableOperationListFromNumericExpression("2*3/4") - val list18 = createComparableOperationListFromNumericExpression("3/4*2") - assertThat(list17).isEqualTo(list18) + @Test + fun test48() { + // TODO: do something with this + val firstList = createComparableOperationListFromNumericExpression("2*(3/4)") + val secondList = createComparableOperationListFromNumericExpression("3/4*2") + assertThat(firstList).isEqualTo(secondList) + } - val list45 = createComparableOperationListFromNumericExpression("2*3/4") - val list46 = createComparableOperationListFromNumericExpression("2*3*4") - assertThat(list45).isNotEqualTo(list46) + @Test + fun test49() { + // TODO: do something with this + val firstList = createComparableOperationListFromNumericExpression("2*3/4") + val secondList = createComparableOperationListFromNumericExpression("3/4*2") + assertThat(firstList).isEqualTo(secondList) + } - val list19 = createComparableOperationListFromNumericExpression("2*3/4") - val list20 = createComparableOperationListFromNumericExpression("2*4/3") - assertThat(list19).isNotEqualTo(list20) + @Test + fun test50() { + // TODO: do something with this + val firstList = createComparableOperationListFromNumericExpression("2*3/4") + val secondList = createComparableOperationListFromNumericExpression("2*3*4") + assertThat(firstList).isNotEqualTo(secondList) + } - val list21 = createComparableOperationListFromNumericExpression("2*3/4*7") - val list22 = createComparableOperationListFromNumericExpression("3/4*7*2") - assertThat(list21).isEqualTo(list22) + @Test + fun test51() { + // TODO: do something with this + val firstList = createComparableOperationListFromNumericExpression("2*3/4") + val secondList = createComparableOperationListFromNumericExpression("2*4/3") + assertThat(firstList).isNotEqualTo(secondList) + } - val list23 = createComparableOperationListFromNumericExpression("2*3/4*7") - val list24 = createComparableOperationListFromNumericExpression("7*(3*2/4)") - assertThat(list23).isEqualTo(list24) + @Test + fun test52() { + // TODO: do something with this + val firstList = createComparableOperationListFromNumericExpression("2*3/4*7") + val secondList = createComparableOperationListFromNumericExpression("3/4*7*2") + assertThat(firstList).isEqualTo(secondList) + } - val list25 = createComparableOperationListFromNumericExpression("2*3/4*7") - val list26 = createComparableOperationListFromNumericExpression("7*3*2/4") - assertThat(list25).isEqualTo(list26) + @Test + fun test53() { + // TODO: do something with this + val firstList = createComparableOperationListFromNumericExpression("2*3/4*7") + val secondList = createComparableOperationListFromNumericExpression("7*(3*2/4)") + assertThat(firstList).isEqualTo(secondList) + } - val list27 = createComparableOperationListFromNumericExpression("-2*3") - val list28 = createComparableOperationListFromNumericExpression("3*-2") - assertThat(list27).isEqualTo(list28) + @Test + fun test54() { + // TODO: do something with this + val firstList = createComparableOperationListFromNumericExpression("2*3/4*7") + val secondList = createComparableOperationListFromNumericExpression("7*3*2/4") + assertThat(firstList).isEqualTo(secondList) + } - val list29 = createComparableOperationListFromNumericExpression("2^3") - val list30 = createComparableOperationListFromNumericExpression("3^2") - assertThat(list29).isNotEqualTo(list30) + @Test + fun test55() { + // TODO: do something with this + val firstList = createComparableOperationListFromNumericExpression("-2*3") + val secondList = createComparableOperationListFromNumericExpression("3*-2") + assertThat(firstList).isEqualTo(secondList) + } - val list31 = createComparableOperationListFromNumericExpression("-(1+2)") - val list32 = createComparableOperationListFromNumericExpression("-1+2") - assertThat(list31).isNotEqualTo(list32) + @Test + fun test56() { + // TODO: do something with this + val firstList = createComparableOperationListFromNumericExpression("2^3") + val secondList = createComparableOperationListFromNumericExpression("3^2") + assertThat(firstList).isNotEqualTo(secondList) + } - val list33 = createComparableOperationListFromNumericExpression("-(1+2)") - val list34 = createComparableOperationListFromNumericExpression("-1-2") - assertThat(list33).isNotEqualTo(list34) + @Test + fun test57() { + // TODO: do something with this + val firstList = createComparableOperationListFromNumericExpression("-(1+2)") + val secondList = createComparableOperationListFromNumericExpression("-1+2") + assertThat(firstList).isNotEqualTo(secondList) + } - val list35 = createComparableOperationListFromAlgebraicExpression("x(x+1)") - val list36 = createComparableOperationListFromAlgebraicExpression("(1+x)x") - assertThat(list35).isEqualTo(list36) + @Test + fun test58() { + // TODO: do something with this + val firstList = createComparableOperationListFromNumericExpression("-(1+2)") + val secondList = createComparableOperationListFromNumericExpression("-1-2") + assertThat(firstList).isNotEqualTo(secondList) + } - val list37 = createComparableOperationListFromAlgebraicExpression("x(x+1)") - val list38 = createComparableOperationListFromAlgebraicExpression("x^2+x") - assertThat(list37).isNotEqualTo(list38) + @Test + fun test59() { + // TODO: do something with this + val firstList = createComparableOperationListFromAlgebraicExpression("x(x+1)") + val secondList = createComparableOperationListFromAlgebraicExpression("(1+x)x") + assertThat(firstList).isEqualTo(secondList) + } - val list39 = createComparableOperationListFromAlgebraicExpression("x^2*sqrt(x)") - val list40 = createComparableOperationListFromAlgebraicExpression("x") - assertThat(list39).isNotEqualTo(list40) + @Test + fun test60() { + // TODO: do something with this + val firstList = createComparableOperationListFromAlgebraicExpression("x(x+1)") + val secondList = createComparableOperationListFromAlgebraicExpression("x^2+x") + assertThat(firstList).isNotEqualTo(secondList) + } - val list41 = createComparableOperationListFromAlgebraicExpression("xyz") - val list42 = createComparableOperationListFromAlgebraicExpression("zyx") - assertThat(list41).isEqualTo(list42) + @Test + fun test61() { + // TODO: do something with this + val firstList = createComparableOperationListFromAlgebraicExpression("x^2*sqrt(x)") + val secondList = createComparableOperationListFromAlgebraicExpression("x") + assertThat(firstList).isNotEqualTo(secondList) + } - val list43 = createComparableOperationListFromAlgebraicExpression("1+xy-2") - val list44 = createComparableOperationListFromAlgebraicExpression("-2+1+yx") - assertThat(list43).isEqualTo(list44) + @Test + fun test62() { + // TODO: do something with this + val firstList = createComparableOperationListFromAlgebraicExpression("xyz") + val secondList = createComparableOperationListFromAlgebraicExpression("zyx") + assertThat(firstList).isEqualTo(secondList) + } - // TODO: add tests for comparator/sorting & negation simplification? + @Test + fun test63() { + // TODO: do something with this + val firstList = createComparableOperationListFromAlgebraicExpression("1+xy-2") + val secondList = createComparableOperationListFromAlgebraicExpression("-2+1+yx") + assertThat(firstList).isEqualTo(secondList) } private fun createComparableOperationListFromNumericExpression(expression: String) = From 65a9fe1e65e8e97deaadbc9213f632ee6c4af683 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 27 Jan 2022 18:53:39 -0800 Subject: [PATCH 079/162] Remove the ComparableOperationList wrapper. --- model/src/main/proto/math.proto | 56 +++++++++---------- scripts/assets/test_file_exemptions.textproto | 2 +- .../oppia/android/testing/math/BUILD.bazel | 4 +- ...bject.kt => ComparableOperationSubject.kt} | 56 +++++++++---------- 4 files changed, 55 insertions(+), 63 deletions(-) rename testing/src/main/java/org/oppia/android/testing/math/{ComparableOperationListSubject.kt => ComparableOperationSubject.kt} (87%) diff --git a/model/src/main/proto/math.proto b/model/src/main/proto/math.proto index d1fc9f36419..4e3989ec3e3 100644 --- a/model/src/main/proto/math.proto +++ b/model/src/main/proto/math.proto @@ -189,7 +189,8 @@ message MathEquation { MathExpression right_side = 2; } -// Represents a list of comparable mathematics operations. +// An operation that can be compared in a way that does not change the value based on commutativity +// or associativity. // // 'Comparable' here means that this structure provides a trivial way to compare commutative and // associative operations (i.e. by extracting terms from multiple subsequent commutative & @@ -199,33 +200,29 @@ message MathEquation { // using standard proto equals checking). Also note that care must be taken when performing equality // checking since this structure can contain floating point values that require an epsilon check to // approximate equality. -message ComparableOperationList { - // An operation that can be compared in a way that does not change the value based on - // commutativity or associativity. - message ComparableOperation { - // Indicates that this operation (e.g. x) should be treated as negated (e.g. -x). - bool is_negated = 1; - - // Indicates that this operation (e.g. x) should be treated as a multiplicative inverse - // (e.g. 1/x). - bool is_inverted = 2; - - // The supported comparison types. - oneof comparison_type { - // Indicates that this operation is a commutative accumulation (that is, a list of subsequent - // operations of the same type that are commutative, e.g. addition or multiplication). - CommutativeAccumulation commutative_accumulation = 3; - - // Indicates that this operation is a non-commutative operation and thus cannot be - // accumulated (e.g. exponentiation). - NonCommutativeOperation non_commutative_operation = 4; - - // Indicates that this operation represents a constant value. - Real constant_term = 5; - - // Indicates that this operation represents a variable. - string variable_term = 6; - } +message ComparableOperation { + // Indicates that this operation (e.g. x) should be treated as negated (e.g. -x). + bool is_negated = 1; + + // Indicates that this operation (e.g. x) should be treated as a multiplicative inverse + // (e.g. 1/x). + bool is_inverted = 2; + + // The supported comparison types. + oneof comparison_type { + // Indicates that this operation is a commutative accumulation (that is, a list of subsequent + // operations of the same type that are commutative, e.g. addition or multiplication). + CommutativeAccumulation commutative_accumulation = 3; + + // Indicates that this operation is a non-commutative operation and thus cannot be + // accumulated (e.g. exponentiation). + NonCommutativeOperation non_commutative_operation = 4; + + // Indicates that this operation represents a constant value. + Real constant_term = 5; + + // Indicates that this operation represents a variable. + string variable_term = 6; } // Represents an accumulation of operations (such as a summation or product). @@ -285,7 +282,4 @@ message ComparableOperationList { ComparableOperation right_operand = 2; } } - - // The root of the operation list (i.e. the lowest precedent operation of the expression). - ComparableOperation root_operation = 1; } diff --git a/scripts/assets/test_file_exemptions.textproto b/scripts/assets/test_file_exemptions.textproto index b0578bf3b80..b2f2a5f5a99 100644 --- a/scripts/assets/test_file_exemptions.textproto +++ b/scripts/assets/test_file_exemptions.textproto @@ -646,7 +646,7 @@ exempted_file_path: "testing/src/main/java/org/oppia/android/testing/espresso/Ge exempted_file_path: "testing/src/main/java/org/oppia/android/testing/espresso/ImageViewMatcher.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/espresso/KonfettiViewMatcher.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/junit/DefineAppLanguageLocaleContext.kt" -exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/ComparableOperationListSubject.kt" +exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/ComparableOperationSubject.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/FractionSubject.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/MathEquationSubject.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/math/MathExpressionSubject.kt" diff --git a/testing/src/main/java/org/oppia/android/testing/math/BUILD.bazel b/testing/src/main/java/org/oppia/android/testing/math/BUILD.bazel index 98eb5f9cdd1..0e6b8bf7989 100644 --- a/testing/src/main/java/org/oppia/android/testing/math/BUILD.bazel +++ b/testing/src/main/java/org/oppia/android/testing/math/BUILD.bazel @@ -7,10 +7,10 @@ load("@io_bazel_rules_kotlin//kotlin:kotlin.bzl", "kt_android_library") # TODO(#2747): Move these libraries to be under utility/.../math/testing. kt_android_library( - name = "comparable_operation_list_subject", + name = "comparable_operation_subject", testonly = True, srcs = [ - "ComparableOperationListSubject.kt", + "ComparableOperationSubject.kt", ], visibility = [ "//:oppia_testing_visibility", diff --git a/testing/src/main/java/org/oppia/android/testing/math/ComparableOperationListSubject.kt b/testing/src/main/java/org/oppia/android/testing/math/ComparableOperationSubject.kt similarity index 87% rename from testing/src/main/java/org/oppia/android/testing/math/ComparableOperationListSubject.kt rename to testing/src/main/java/org/oppia/android/testing/math/ComparableOperationSubject.kt index 3d71f819677..06dd8bae7ba 100644 --- a/testing/src/main/java/org/oppia/android/testing/math/ComparableOperationListSubject.kt +++ b/testing/src/main/java/org/oppia/android/testing/math/ComparableOperationSubject.kt @@ -7,16 +7,15 @@ import com.google.common.truth.StringSubject import com.google.common.truth.Truth.assertAbout import com.google.common.truth.Truth.assertThat import com.google.common.truth.extensions.proto.LiteProtoSubject -import org.oppia.android.app.model.ComparableOperationList -import org.oppia.android.app.model.ComparableOperationList.ComparableOperation -import org.oppia.android.app.model.ComparableOperationList.ComparableOperation.ComparisonTypeCase +import org.oppia.android.app.model.ComparableOperation +import org.oppia.android.app.model.ComparableOperation.ComparisonTypeCase import org.oppia.android.app.model.Real import org.oppia.android.testing.math.RealSubject.Companion.assertThat // TODO(#4098): Add tests for this class. /** - * Truth subject for verifying properties of [ComparableOperationList]s. + * Truth subject for verifying properties of [ComparableOperation]s. * * This subject makes use of a custom Kotlin DSL to test the structure of a comparable operation * list. This structure allows for recursive verification of the structure since the structure @@ -25,7 +24,7 @@ import org.oppia.android.testing.math.RealSubject.Companion.assertThat * comparators for all syntactical options): * * ```kotlin - * assertThat(comparableOperationList).hasStructureThatMatches { + * assertThat(ComparableOperation).hasStructureThatMatches { * hasNegatedPropertyThat().isFalse() * hasInvertedPropertyThat().isFalse() * commutativeAccumulationWithType(SUMMATION) { @@ -58,22 +57,21 @@ import org.oppia.android.testing.math.RealSubject.Companion.assertThat * The above verifies the following structure corresponding to the expression 1+3+4. * * Note that this class is also a [LiteProtoSubject] so other aspects of the underlying - * [ComparableOperationList] proto can be verified through inherited methods. + * [ComparableOperation] proto can be verified through inherited methods. * * Call [assertThat] to create the subject. */ -class ComparableOperationListSubject private constructor( +class ComparableOperationSubject private constructor( metadata: FailureMetadata, - private val actual: ComparableOperationList + private val actual: ComparableOperation ) : LiteProtoSubject(metadata, actual) { /** - * Begins the structure syntax matcher for the root of the [ComparableOperationList] corresponding - * to this subject (per [ComparableOperationList.getRootOperation]). + * Begins the structure syntax matcher for [ComparableOperation] being tested as the subject. * * See [ComparableOperationComparator] for syntax. */ fun hasStructureThatMatches(init: ComparableOperationComparator.() -> Unit) { - ComparableOperationComparator.createFrom(actual.rootOperation).also(init) + ComparableOperationComparator.createFrom(actual).also(init) } /** @@ -124,7 +122,7 @@ class ComparableOperationListSubject private constructor( * specified type. See [CommutativeAccumulationComparator] for example syntax. */ fun commutativeAccumulationWithType( - type: ComparableOperationList.CommutativeAccumulation.AccumulationType, + type: ComparableOperation.CommutativeAccumulation.AccumulationType, init: CommutativeAccumulationComparator.() -> Unit ) { CommutativeAccumulationComparator.createFrom(type, operation).also(init) @@ -199,11 +197,11 @@ class ComparableOperationListSubject private constructor( */ @ComparableOperationComparatorMarker class CommutativeAccumulationComparator private constructor( - private val accumulation: ComparableOperationList.CommutativeAccumulation + private val accumulation: ComparableOperation.CommutativeAccumulation ) { /** * Returns a [IntegerSubject] to test - * [ComparableOperationList.CommutativeAccumulation.getCombinedOperationsCount]. + * [ComparableOperation.CommutativeAccumulation.getCombinedOperationsCount]. * * This method never fails since the underlying property defaults to 0 if there are no * operations in the accumulation. @@ -234,7 +232,7 @@ class ComparableOperationListSubject private constructor( * specified type. */ fun createFrom( - type: ComparableOperationList.CommutativeAccumulation.AccumulationType, + type: ComparableOperation.CommutativeAccumulation.AccumulationType, operation: ComparableOperation ): CommutativeAccumulationComparator { assertThat(operation.comparisonTypeCase) @@ -259,11 +257,11 @@ class ComparableOperationListSubject private constructor( */ @ComparableOperationComparatorMarker class NonCommutativeOperationComparator private constructor( - private val operation: ComparableOperationList.NonCommutativeOperation + private val operation: ComparableOperation.NonCommutativeOperation ) { /** * Begins structure matching for this operation as an exponentiation per - * [ComparableOperationList.NonCommutativeOperation.getExponentiation]. + * [ComparableOperation.NonCommutativeOperation.getExponentiation]. * * This method will fail if the operation corresponding to the subject is not an exponentiation. * See [BinaryOperationComparator] for specifics on the operation comparator used here. Example @@ -277,14 +275,14 @@ class ComparableOperationListSubject private constructor( */ fun exponentiation(init: BinaryOperationComparator.() -> Unit) { verifyTypeAs( - ComparableOperationList.NonCommutativeOperation.OperationTypeCase.EXPONENTIATION + ComparableOperation.NonCommutativeOperation.OperationTypeCase.EXPONENTIATION ) BinaryOperationComparator.createFrom(operation.exponentiation).also(init) } /** * Begins structure matching for this operation as a square root operation per - * [ComparableOperationList.NonCommutativeOperation.getSquareRoot]. + * [ComparableOperation.NonCommutativeOperation.getSquareRoot]. * * This method will fail if the operation corresponding to the subject is not a square root. The * argument is another [ComparableOperation] hence the utilization of @@ -299,12 +297,12 @@ class ComparableOperationListSubject private constructor( fun squareRootWithArgument( init: ComparableOperationComparator.() -> Unit ) { - verifyTypeAs(ComparableOperationList.NonCommutativeOperation.OperationTypeCase.SQUARE_ROOT) + verifyTypeAs(ComparableOperation.NonCommutativeOperation.OperationTypeCase.SQUARE_ROOT) ComparableOperationComparator.createFrom(operation.squareRoot).also(init) } private fun verifyTypeAs( - type: ComparableOperationList.NonCommutativeOperation.OperationTypeCase + type: ComparableOperation.NonCommutativeOperation.OperationTypeCase ) { assertThat(operation.operationTypeCase).isEqualTo(type) } @@ -344,11 +342,11 @@ class ComparableOperationListSubject private constructor( */ @ComparableOperationComparatorMarker class BinaryOperationComparator private constructor( - private val operation: ComparableOperationList.NonCommutativeOperation.BinaryOperation + private val operation: ComparableOperation.NonCommutativeOperation.BinaryOperation ) { /** * Begins structure matching this operation's left operand per - * [ComparableOperationList.NonCommutativeOperation.BinaryOperation.getLeftOperand] for the + * [ComparableOperation.NonCommutativeOperation.BinaryOperation.getLeftOperand] for the * operation represented by this comparator. * * This method provides an [ComparableOperationComparator] to use to verify the constituent @@ -362,7 +360,7 @@ class ComparableOperationListSubject private constructor( /** * Begins structure matching this operation's right operand per - * [ComparableOperationList.NonCommutativeOperation.BinaryOperation.getRightOperand] for the + * [ComparableOperation.NonCommutativeOperation.BinaryOperation.getRightOperand] for the * operation represented by this comparator. * * This method provides an [ComparableOperationComparator] to use to verify the constituent @@ -380,7 +378,7 @@ class ComparableOperationListSubject private constructor( * binary operation. */ fun createFrom( - operation: ComparableOperationList.NonCommutativeOperation.BinaryOperation + operation: ComparableOperation.NonCommutativeOperation.BinaryOperation ): BinaryOperationComparator = BinaryOperationComparator(operation) } } @@ -458,10 +456,10 @@ class ComparableOperationListSubject private constructor( @DslMarker private annotation class ComparableOperationComparatorMarker /** - * Returns a new [ComparableOperationListSubject] to verify aspects of the specified - * [ComparableOperationList] value. + * Returns a new [ComparableOperationSubject] to verify aspects of the specified + * [ComparableOperation] value. */ - fun assertThat(actual: ComparableOperationList): ComparableOperationListSubject = - assertAbout(::ComparableOperationListSubject).that(actual) + fun assertThat(actual: ComparableOperation): ComparableOperationSubject = + assertAbout(::ComparableOperationSubject).that(actual) } } From fbd935cc170254f965a712d407403b14220367c2 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 27 Jan 2022 19:06:13 -0800 Subject: [PATCH 080/162] Change parameterized method delimiter. --- .../oppia/android/testing/junit/OppiaParameterizedTestRunner.kt | 2 +- .../oppia/android/testing/junit/ParameterizedRunnerDelegate.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/testing/src/main/java/org/oppia/android/testing/junit/OppiaParameterizedTestRunner.kt b/testing/src/main/java/org/oppia/android/testing/junit/OppiaParameterizedTestRunner.kt index e237553a9fc..218dc1258aa 100644 --- a/testing/src/main/java/org/oppia/android/testing/junit/OppiaParameterizedTestRunner.kt +++ b/testing/src/main/java/org/oppia/android/testing/junit/OppiaParameterizedTestRunner.kt @@ -56,7 +56,7 @@ import java.lang.reflect.Method * e.g.: * * ```bash - * bazel run //...:ExampleParameterizedTest --test_filter=testParams_multipleVals_isConsistent-first + * bazel run //...:ExampleParameterizedTest --test_filter=testParams_multipleVals_isConsistent_first * ``` * * Or, all of the iterations for that test can be run: diff --git a/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedRunnerDelegate.kt b/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedRunnerDelegate.kt index dc9699b31d5..3e32dba92d6 100644 --- a/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedRunnerDelegate.kt +++ b/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedRunnerDelegate.kt @@ -43,7 +43,7 @@ class ParameterizedRunnerDelegate( override fun testName(method: FrameworkMethod?): String { return if (methodName != null) { - "${fetchTestNameFromParent(method)}-$iterationName" + "${fetchTestNameFromParent(method)}_$iterationName" } else fetchTestNameFromParent(method) } From 8420c563a99a859641d43972bdca60a1727a7390 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 27 Jan 2022 19:18:46 -0800 Subject: [PATCH 081/162] Use utility directly in test. --- .../org/oppia/android/util/math/BUILD.bazel | 3 +- .../math/ExpressionToLatexConverterTest.kt | 90 +++++++++++++------ 2 files changed, 64 insertions(+), 29 deletions(-) diff --git a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel index 72b42b3832c..f2b8f412afc 100644 --- a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel @@ -50,14 +50,13 @@ oppia_android_test( test_manifest = "//utility:test_manifest", deps = [ "//model/src/main/proto:math_java_proto_lite", - "//testing/src/main/java/org/oppia/android/testing/math:math_equation_subject", - "//testing/src/main/java/org/oppia/android/testing/math:math_expression_subject", "//third_party:androidx_test_ext_junit", "//third_party:com_google_truth_extensions_truth-liteproto-extension", "//third_party:com_google_truth_truth", "//third_party:junit_junit", "//third_party:org_robolectric_robolectric", "//third_party:robolectric_android-all", + "//utility/src/main/java/org/oppia/android/util/math:extensions", "//utility/src/main/java/org/oppia/android/util/math:math_expression_parser", ], ) diff --git a/utility/src/test/java/org/oppia/android/util/math/ExpressionToLatexConverterTest.kt b/utility/src/test/java/org/oppia/android/util/math/ExpressionToLatexConverterTest.kt index 1af737e0583..d9125d519a5 100644 --- a/utility/src/test/java/org/oppia/android/util/math/ExpressionToLatexConverterTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/ExpressionToLatexConverterTest.kt @@ -6,8 +6,6 @@ import org.junit.Test import org.junit.runner.RunWith import org.oppia.android.app.model.MathEquation import org.oppia.android.app.model.MathExpression -import org.oppia.android.testing.math.MathEquationSubject.Companion.assertThat -import org.oppia.android.testing.math.MathExpressionSubject.Companion.assertThat import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult import org.robolectric.annotation.LooperMode @@ -23,151 +21,189 @@ class ExpressionToLatexConverterTest { fun testConvert_numericExp_number_returnsConstantLatex() { val exp = parseNumericExpressionWithAllErrors("1") - assertThat(exp).convertsToLatexStringThat().isEqualTo("1") + val latex = exp.toRawLatex(divAsFraction = false) + + assertThat(latex).isEqualTo("1") } @Test fun testConvert_numericExp_unaryPlus_withoutOptionalErrors_returnLatexWithUnaryPlus() { val exp = parseNumericExpressionWithoutOptionalErrors("+1") - assertThat(exp).convertsToLatexStringThat().isEqualTo("+1") + val latex = exp.toRawLatex(divAsFraction = false) + + assertThat(latex).isEqualTo("+1") } @Test fun testConvert_numericExp_unaryMinus_returnLatexWithUnaryMinus() { val exp = parseNumericExpressionWithAllErrors("-1") - assertThat(exp).convertsToLatexStringThat().isEqualTo("-1") + val latex = exp.toRawLatex(divAsFraction = false) + + assertThat(latex).isEqualTo("-1") } @Test fun testConvert_numericExp_addition_returnsLatexWithAddition() { val exp = parseNumericExpressionWithAllErrors("1+2") - assertThat(exp).convertsToLatexStringThat().isEqualTo("1 + 2") + val latex = exp.toRawLatex(divAsFraction = false) + + assertThat(latex).isEqualTo("1 + 2") } @Test fun testConvert_numericExp_subtraction_returnsLatexWithSubtract() { val exp = parseNumericExpressionWithAllErrors("1-2") - assertThat(exp).convertsToLatexStringThat().isEqualTo("1 - 2") + val latex = exp.toRawLatex(divAsFraction = false) + + assertThat(latex).isEqualTo("1 - 2") } @Test fun testConvert_numericExp_multiplication_returnsLatexWithMultiplication() { val exp = parseNumericExpressionWithAllErrors("2*3") - assertThat(exp).convertsToLatexStringThat().isEqualTo("2 \\times 3") + val latex = exp.toRawLatex(divAsFraction = false) + + assertThat(latex).isEqualTo("2 \\times 3") } @Test fun testConvert_numericExp_division_returnsLatexWithDivision() { val exp = parseNumericExpressionWithAllErrors("2/3") - assertThat(exp).convertsToLatexStringThat().isEqualTo("2 \\div 3") + val latex = exp.toRawLatex(divAsFraction = false) + + assertThat(latex).isEqualTo("2 \\div 3") } @Test fun testConvert_numericExp_division_divAsFraction_returnsLatexWithFraction() { val exp = parseNumericExpressionWithAllErrors("2/3") - assertThat(exp).convertsWithFractionsToLatexStringThat().isEqualTo("\\frac{2}{3}") + val latex = exp.toRawLatex(divAsFraction = true) + + assertThat(latex).isEqualTo("\\frac{2}{3}") } @Test fun testConvert_numericExp_multipleDivisions_divAsFraction_returnsLatexWithFractions() { val exp = parseNumericExpressionWithAllErrors("2/3/4") - assertThat(exp).convertsWithFractionsToLatexStringThat().isEqualTo("\\frac{\\frac{2}{3}}{4}") + val latex = exp.toRawLatex(divAsFraction = true) + + assertThat(latex).isEqualTo("\\frac{\\frac{2}{3}}{4}") } @Test fun testConvert_numericExp_exponent_returnsLatexWithExponent() { val exp = parseNumericExpressionWithAllErrors("2^3") - assertThat(exp).convertsToLatexStringThat().isEqualTo("2 ^ {3}") + val latex = exp.toRawLatex(divAsFraction = false) + + assertThat(latex).isEqualTo("2 ^ {3}") } @Test fun testConvert_numericExp_inlineSquareRoot_returnsLatexWithSquareRoot() { val exp = parseNumericExpressionWithAllErrors("√2") - assertThat(exp).convertsToLatexStringThat().isEqualTo("\\sqrt{2}") + val latex = exp.toRawLatex(divAsFraction = false) + + assertThat(latex).isEqualTo("\\sqrt{2}") } @Test fun testConvert_numericExp_inlineSquareRoot_operationArg_returnsLatexWithSquareRoot() { val exp = parseNumericExpressionWithAllErrors("√(1+2)") - assertThat(exp).convertsToLatexStringThat().isEqualTo("\\sqrt{(1 + 2)}") + val latex = exp.toRawLatex(divAsFraction = false) + + assertThat(latex).isEqualTo("\\sqrt{(1 + 2)}") } @Test fun testConvert_numericExp_squareRoot_returnsLatexWithSquareRoot() { val exp = parseNumericExpressionWithAllErrors("sqrt(2)") - assertThat(exp).convertsToLatexStringThat().isEqualTo("\\sqrt{2}") + val latex = exp.toRawLatex(divAsFraction = false) + + assertThat(latex).isEqualTo("\\sqrt{2}") } @Test fun testConvert_numericExp_squareRoot_operationArg_returnsLatexWithSquareRoot() { val exp = parseNumericExpressionWithAllErrors("sqrt(1+2)") - assertThat(exp).convertsToLatexStringThat().isEqualTo("\\sqrt{1 + 2}") + val latex = exp.toRawLatex(divAsFraction = false) + + assertThat(latex).isEqualTo("\\sqrt{1 + 2}") } @Test fun testConvert_numericExp_parentheses_returnsLatexWithGroup() { val exp = parseNumericExpressionWithAllErrors("2/(3+4)") - assertThat(exp).convertsToLatexStringThat().isEqualTo("2 \\div (3 + 4)") + val latex = exp.toRawLatex(divAsFraction = false) + + assertThat(latex).isEqualTo("2 \\div (3 + 4)") } @Test fun testConvert_numericExp_exponentToGroup_returnsCorrectlyWrappedLatex() { val exp = parseNumericExpressionWithAllErrors("2^(7-3)") - assertThat(exp).convertsToLatexStringThat().isEqualTo("2 ^ {(7 - 3)}") + val latex = exp.toRawLatex(divAsFraction = false) + + assertThat(latex).isEqualTo("2 ^ {(7 - 3)}") } @Test fun testConvert_algebraicExp_variable_returnsVariableLatex() { val exp = parseAlgebraicExpressionWithAllErrors("x") - assertThat(exp).convertsToLatexStringThat().isEqualTo("x") + val latex = exp.toRawLatex(divAsFraction = false) + + assertThat(latex).isEqualTo("x") } @Test fun testConvert_algebraicExp_twoX_returnsLatexWithImplicitMultiplication() { val exp = parseAlgebraicExpressionWithAllErrors("2x") - assertThat(exp).convertsToLatexStringThat().isEqualTo("2x") + val latex = exp.toRawLatex(divAsFraction = false) + + assertThat(latex).isEqualTo("2x") } @Test fun testConvert_algebraicEq_xEqualsOne_returnsLatexWithEquals() { val exp = parseAlgebraicEquationWithAllErrors("x=1") - assertThat(exp).convertsToLatexStringThat().isEqualTo("x = 1") + val latex = exp.toRawLatex(divAsFraction = false) + + assertThat(latex).isEqualTo("x = 1") } @Test fun testConvert_algebraicEq_complexExpression_returnsCorrectLatex() { val exp = parseAlgebraicEquationWithAllErrors("(x+1)(x-2)=(x^3+2x^2-5x-6)/(x+3)") - assertThat(exp) - .convertsToLatexStringThat() - .isEqualTo("(x + 1)(x - 2) = (x ^ {3} + 2x ^ {2} - 5x - 6) \\div (x + 3)") + val latex = exp.toRawLatex(divAsFraction = false) + + assertThat(latex).isEqualTo("(x + 1)(x - 2) = (x ^ {3} + 2x ^ {2} - 5x - 6) \\div (x + 3)") } @Test fun testConvert_algebraicEq_complexExpression_divAsFraction_returnsCorrectLatex() { val exp = parseAlgebraicEquationWithAllErrors("(x+1)(x-2)=(x^3+2x^2-5x-6)/(x+3)") - assertThat(exp) - .convertsWithFractionsToLatexStringThat() - .isEqualTo("(x + 1)(x - 2) = \\frac{(x ^ {3} + 2x ^ {2} - 5x - 6)}{(x + 3)}") + val latex = exp.toRawLatex(divAsFraction = true) + + assertThat(latex).isEqualTo("(x + 1)(x - 2) = \\frac{(x ^ {3} + 2x ^ {2} - 5x - 6)}{(x + 3)}") } private companion object { From b8a3e2e515aa7bc3316d7ff3a187f132cd81c3cb Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 27 Jan 2022 19:27:04 -0800 Subject: [PATCH 082/162] Post-merge fixes. This adjusts for the removal of ComparableOperationList (i.e. no wrapper proto). --- .../org/oppia/android/util/math/BUILD.bazel | 6 ++-- ...pressionToComparableOperationConverter.kt} | 29 +++++++------------ .../util/math/MathExpressionExtensions.kt | 6 ++-- .../org/oppia/android/util/math/BUILD.bazel | 6 ++-- ...sionToComparableOperationConverterTest.kt} | 4 +-- 5 files changed, 22 insertions(+), 29 deletions(-) rename utility/src/main/java/org/oppia/android/util/math/{ExpressionToComparableOperationListConverter.kt => ExpressionToComparableOperationConverter.kt} (89%) rename utility/src/test/java/org/oppia/android/util/math/{ExpressionToComparableOperationListConverterTest.kt => ExpressionToComparableOperationConverterTest.kt} (99%) diff --git a/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel b/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel index 05542af8731..d74e1683647 100644 --- a/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel @@ -119,7 +119,7 @@ kt_android_library( "MathExpressionExtensions.kt", ], deps = [ - ":expression_to_comparable_operation_list_converter", + ":expression_to_comparable_operation_converter", ":expression_to_latex_converter", ":numeric_expression_evaluator", "//model/src/main/proto:math_java_proto_lite", @@ -161,9 +161,9 @@ kt_android_library( ) kt_android_library( - name = "expression_to_comparable_operation_list_converter", + name = "expression_to_comparable_operation_converter", srcs = [ - "ExpressionToComparableOperationListConverter.kt", + "ExpressionToComparableOperationConverter.kt", ], deps = [ ":comparator_extensions", diff --git a/utility/src/main/java/org/oppia/android/util/math/ExpressionToComparableOperationListConverter.kt b/utility/src/main/java/org/oppia/android/util/math/ExpressionToComparableOperationConverter.kt similarity index 89% rename from utility/src/main/java/org/oppia/android/util/math/ExpressionToComparableOperationListConverter.kt rename to utility/src/main/java/org/oppia/android/util/math/ExpressionToComparableOperationConverter.kt index ef1008cab54..1dd214a78a2 100644 --- a/utility/src/main/java/org/oppia/android/util/math/ExpressionToComparableOperationListConverter.kt +++ b/utility/src/main/java/org/oppia/android/util/math/ExpressionToComparableOperationConverter.kt @@ -1,15 +1,14 @@ package org.oppia.android.util.math -import org.oppia.android.app.model.ComparableOperationList -import org.oppia.android.app.model.ComparableOperationList.CommutativeAccumulation -import org.oppia.android.app.model.ComparableOperationList.CommutativeAccumulation.AccumulationType.PRODUCT -import org.oppia.android.app.model.ComparableOperationList.CommutativeAccumulation.AccumulationType.SUMMATION -import org.oppia.android.app.model.ComparableOperationList.ComparableOperation -import org.oppia.android.app.model.ComparableOperationList.ComparableOperation.ComparisonTypeCase.CONSTANT_TERM -import org.oppia.android.app.model.ComparableOperationList.ComparableOperation.ComparisonTypeCase.NON_COMMUTATIVE_OPERATION -import org.oppia.android.app.model.ComparableOperationList.ComparableOperation.ComparisonTypeCase.VARIABLE_TERM -import org.oppia.android.app.model.ComparableOperationList.NonCommutativeOperation -import org.oppia.android.app.model.ComparableOperationList.NonCommutativeOperation.OperationTypeCase +import org.oppia.android.app.model.ComparableOperation +import org.oppia.android.app.model.ComparableOperation.CommutativeAccumulation +import org.oppia.android.app.model.ComparableOperation.CommutativeAccumulation.AccumulationType.PRODUCT +import org.oppia.android.app.model.ComparableOperation.CommutativeAccumulation.AccumulationType.SUMMATION +import org.oppia.android.app.model.ComparableOperation.ComparisonTypeCase.CONSTANT_TERM +import org.oppia.android.app.model.ComparableOperation.ComparisonTypeCase.NON_COMMUTATIVE_OPERATION +import org.oppia.android.app.model.ComparableOperation.ComparisonTypeCase.VARIABLE_TERM +import org.oppia.android.app.model.ComparableOperation.NonCommutativeOperation +import org.oppia.android.app.model.ComparableOperation.NonCommutativeOperation.OperationTypeCase import org.oppia.android.app.model.MathBinaryOperation.Operator as BinaryOperator import org.oppia.android.app.model.MathBinaryOperation.Operator.ADD import org.oppia.android.app.model.MathBinaryOperation.Operator.DIVIDE @@ -30,7 +29,7 @@ import org.oppia.android.app.model.MathUnaryOperation.Operator as UnaryOperator import org.oppia.android.app.model.MathUnaryOperation.Operator.NEGATE import org.oppia.android.app.model.MathUnaryOperation.Operator.POSITIVE -class ExpressionToComparableOperationListConverter private constructor() { +class ExpressionToComparableOperationConverter private constructor() { companion object { // TODO: consider eliminating the comparator extensions. Probably should verify full test suite // & the old tests before deleting the old tests. @@ -87,13 +86,7 @@ class ExpressionToComparableOperationListConverter private constructor() { ) } - fun MathExpression.toComparableOperationList(): ComparableOperationList { - return ComparableOperationList.newBuilder().apply { - rootOperation = toComparableOperation() - }.build() - } - - private fun MathExpression.toComparableOperation(): ComparableOperation { + fun MathExpression.toComparableOperation(): ComparableOperation { return when (expressionTypeCase) { CONSTANT -> ComparableOperation.newBuilder().apply { constantTerm = constant diff --git a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt index 62ee01b9175..59268fca145 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt @@ -1,10 +1,10 @@ package org.oppia.android.util.math -import org.oppia.android.app.model.ComparableOperationList +import org.oppia.android.app.model.ComparableOperation import org.oppia.android.app.model.MathEquation import org.oppia.android.app.model.MathExpression import org.oppia.android.app.model.Real -import org.oppia.android.util.math.ExpressionToComparableOperationListConverter.Companion.toComparableOperationList +import org.oppia.android.util.math.ExpressionToComparableOperationConverter.Companion.toComparableOperation import org.oppia.android.util.math.ExpressionToLatexConverter.Companion.convertToLatex import org.oppia.android.util.math.NumericExpressionEvaluator.Companion.evaluate @@ -31,4 +31,4 @@ fun MathEquation.toRawLatex(divAsFraction: Boolean): String = convertToLatex(div */ fun MathExpression.evaluateAsNumericExpression(): Real? = evaluate() -fun MathExpression.toComparableOperationList(): ComparableOperationList = toComparableOperationList() +fun MathExpression.toComparableOperationList(): ComparableOperation = toComparableOperation() diff --git a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel index ca47eb44deb..dbf95666418 100644 --- a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel @@ -59,10 +59,10 @@ oppia_android_test( ) oppia_android_test( - name = "ExpressionToComparableOperationListConverterTest", - srcs = ["ExpressionToComparableOperationListConverterTest.kt"], + name = "ExpressionToComparableOperationConverterTest", + srcs = ["ExpressionToComparableOperationConverterTest.kt"], custom_package = "org.oppia.android.util.math", - test_class = "org.oppia.android.util.math.ExpressionToComparableOperationListConverterTest", + test_class = "org.oppia.android.util.math.ExpressionToComparableOperationConverterTest", test_manifest = "//utility:test_manifest", deps = [ "//model/src/main/proto:math_java_proto_lite", diff --git a/utility/src/test/java/org/oppia/android/util/math/ExpressionToComparableOperationListConverterTest.kt b/utility/src/test/java/org/oppia/android/util/math/ExpressionToComparableOperationConverterTest.kt similarity index 99% rename from utility/src/test/java/org/oppia/android/util/math/ExpressionToComparableOperationListConverterTest.kt rename to utility/src/test/java/org/oppia/android/util/math/ExpressionToComparableOperationConverterTest.kt index 144a7ec273e..fd2607b1702 100644 --- a/utility/src/test/java/org/oppia/android/util/math/ExpressionToComparableOperationListConverterTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/ExpressionToComparableOperationConverterTest.kt @@ -11,12 +11,12 @@ import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingM import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult import org.robolectric.annotation.LooperMode -/** Tests for [ExpressionToComparableOperationListConverter]. */ +/** Tests for [ExpressionToComparableOperationConverter]. */ // SameParameterValue: tests should have specific context included/excluded for readability. @Suppress("SameParameterValue") @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) -class ExpressionToComparableOperationListConverterTest { +class ExpressionToComparableOperationConverterTest { // TODO: add high-level checks for the three types, but don't test in detail since there are // separate suites. Also, document the separate suites' existence in this suites's KDoc. From 28811b3d2f243a30bf8c1295f3cb7105e6ff4ea4 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 27 Jan 2022 21:44:12 -0800 Subject: [PATCH 083/162] Add first round of tests. This includes fixes to the converter itself as it wasn't distributing both product inversions and negation correctly in several cases. Tests should now be covering these cases. --- ...xpressionToComparableOperationConverter.kt | 91 +- .../util/math/MathExpressionExtensions.kt | 2 +- .../org/oppia/android/util/math/BUILD.bazel | 2 +- ...ssionToComparableOperationConverterTest.kt | 1683 +++++++++++++++-- 4 files changed, 1566 insertions(+), 212 deletions(-) diff --git a/utility/src/main/java/org/oppia/android/util/math/ExpressionToComparableOperationConverter.kt b/utility/src/main/java/org/oppia/android/util/math/ExpressionToComparableOperationConverter.kt index 1dd214a78a2..5e9cc00b21f 100644 --- a/utility/src/main/java/org/oppia/android/util/math/ExpressionToComparableOperationConverter.kt +++ b/utility/src/main/java/org/oppia/android/util/math/ExpressionToComparableOperationConverter.kt @@ -28,6 +28,8 @@ import org.oppia.android.app.model.MathFunctionCall.FunctionType.SQUARE_ROOT import org.oppia.android.app.model.MathUnaryOperation.Operator as UnaryOperator import org.oppia.android.app.model.MathUnaryOperation.Operator.NEGATE import org.oppia.android.app.model.MathUnaryOperation.Operator.POSITIVE +import org.oppia.android.util.math.ExpressionToComparableOperationConverter.Companion.invertNegation +import org.oppia.android.util.math.ExpressionToComparableOperationConverter.Companion.toComparableOperation class ExpressionToComparableOperationConverter private constructor() { companion object { @@ -105,7 +107,7 @@ class ExpressionToComparableOperationConverter private constructor() { ComparableOperation.getDefaultInstance() } UNARY_OPERATION -> when (unaryOperation.operator) { - NEGATE -> unaryOperation.operand.toComparableOperation().makeNegative() + NEGATE -> unaryOperation.operand.toComparableOperation().invertNegation() POSITIVE -> unaryOperation.operand.toComparableOperation() UnaryOperator.OPERATOR_UNSPECIFIED, UnaryOperator.UNRECOGNIZED, null -> ComparableOperation.getDefaultInstance() @@ -140,8 +142,11 @@ class ExpressionToComparableOperationConverter private constructor() { commutativeAccumulation = CommutativeAccumulation.newBuilder().apply { accumulationType = PRODUCT val negativeCount = - addOperationToProduct(binaryOperation.leftOperand, forceInverse = false) + - addOperationToProduct(binaryOperation.rightOperand, forceInverse = isRhsInverted) + addOperationToProduct( + binaryOperation.leftOperand, forceInverse = false, invertNegation = false + ) + addOperationToProduct( + binaryOperation.rightOperand, forceInverse = isRhsInverted, invertNegation = false + ) // If an odd number of terms were negative then the overall product is negative. isNegated = (negativeCount % 2) != 0 sort() @@ -153,23 +158,27 @@ class ExpressionToComparableOperationConverter private constructor() { expression: MathExpression, forceNegative: Boolean ) { - when (expression.binaryOperation.operator) { - ADD -> { - // If the whole operation is negative, carry it to the left-hand side of the operation. + when { + expression.binaryOperation.operator == ADD -> { + // The whole operation being negative distributes to both sides of the addition. addOperationToSum(expression.binaryOperation.leftOperand, forceNegative) - addOperationToSum(expression.binaryOperation.rightOperand, forceNegative = false) + addOperationToSum(expression.binaryOperation.rightOperand, forceNegative) } - SUBTRACT -> { + expression.binaryOperation.operator == SUBTRACT -> { + // Similar to addition, negation distributes but is inverted by this subtraction for the + // right-hand operand. addOperationToSum(expression.binaryOperation.leftOperand, forceNegative) - addOperationToSum(expression.binaryOperation.rightOperand, forceNegative = true) - } - else -> when { - // Skip groups so that nested operations can be properly combined. - expression.expressionTypeCase == GROUP -> - addOperationToSum(expression.group, forceNegative) - forceNegative -> addCombinedOperations(expression.toComparableOperation().makeNegative()) - else -> addCombinedOperations(expression.toComparableOperation()) + addOperationToSum(expression.binaryOperation.rightOperand, !forceNegative) } + expression.unaryOperation.operator == NEGATE -> + addOperationToSum(expression.unaryOperation.operand, !forceNegative) + // Positive unary can be treated similarly to groups (inline for nesting). + expression.unaryOperation.operator == POSITIVE -> + addOperationToSum(expression.unaryOperation.operand, forceNegative) + // Skip groups so that nested operations can be properly combined. + expression.expressionTypeCase == GROUP -> addOperationToSum(expression.group, forceNegative) + forceNegative -> addCombinedOperations(expression.toComparableOperation().invertNegation()) + else -> addCombinedOperations(expression.toComparableOperation()) } } @@ -178,33 +187,55 @@ class ExpressionToComparableOperationConverter private constructor() { * collapsing subsequent products into a single list. * * @param forceInverse whether this expression is being divided rather than multiplied + * @param invertNegation whether to invert the negation sign for immediate constituent + * operations * @return the number of negative operations that were made positive before being added to the * accumulation */ private fun CommutativeAccumulation.Builder.addOperationToProduct( expression: MathExpression, - forceInverse: Boolean + forceInverse: Boolean, + invertNegation: Boolean ): Int { + // Note that negation only distributes "leftward" since subsequent right-hand operations would + // otherwise actually reverse the negation. return when { expression.binaryOperation.operator == MULTIPLY -> { - // If the whole operation is inverted, carry it to the left-hand side of the operation. - addOperationToProduct(expression.binaryOperation.leftOperand, forceInverse) + - addOperationToProduct(expression.binaryOperation.rightOperand, forceInverse = false) + // If the entire operation is inverted, that means each part of the multiplication should + // be, i.e.: 1/(x*y)=(1/x)*(1/y). + addOperationToProduct( + expression.binaryOperation.leftOperand, forceInverse, invertNegation + ) + addOperationToProduct( + expression.binaryOperation.rightOperand, forceInverse, invertNegation = false + ) } expression.binaryOperation.operator == DIVIDE -> { - addOperationToProduct(expression.binaryOperation.leftOperand, forceInverse) + - addOperationToProduct(expression.binaryOperation.rightOperand, forceInverse = true) + // Similar to multiplication, inversion for the whole operation results in distribution + // except the division inverts for the right-hand operand, i.e.: 1/(x/y)=(1/x)*y. + addOperationToProduct( + expression.binaryOperation.leftOperand, forceInverse, invertNegation + ) + addOperationToProduct( + expression.binaryOperation.rightOperand, !forceInverse, invertNegation = false + ) } + expression.unaryOperation.operator == NEGATE -> + addOperationToProduct(expression.unaryOperation.operand, forceInverse, !invertNegation) + // Positive unary can be treated similarly to groups (inline for nesting). + expression.unaryOperation.operator == POSITIVE -> + addOperationToProduct(expression.unaryOperation.operand, forceInverse, invertNegation) // Skip groups so that nested operations can be properly combined. expression.expressionTypeCase == GROUP -> - addOperationToProduct(expression.group, forceInverse) + addOperationToProduct(expression.group, forceInverse, invertNegation) else -> { val operationExpression = expression.toComparableOperation() - val positiveConvertedOperation = operationExpression.makePositive() + val potentiallyInvertedExpression = if (invertNegation) { + operationExpression.invertNegation() + } else operationExpression + val positiveConvertedOperation = potentiallyInvertedExpression.makePositive() if (forceInverse) { - addCombinedOperations(positiveConvertedOperation.makeInverted()) + addCombinedOperations(positiveConvertedOperation.invertInverted()) } else addCombinedOperations(positiveConvertedOperation) - if (operationExpression.isNegated) 1 else 0 + if (potentiallyInvertedExpression.isNegated) 1 else 0 } } } @@ -238,10 +269,10 @@ class ExpressionToComparableOperationConverter private constructor() { private fun ComparableOperation.makePositive(): ComparableOperation = toBuilder().apply { isNegated = false }.build() - private fun ComparableOperation.makeNegative(): ComparableOperation = - toBuilder().apply { isNegated = true }.build() + private fun ComparableOperation.invertNegation(): ComparableOperation = + toBuilder().apply { isNegated = !isNegated }.build() - private fun ComparableOperation.makeInverted(): ComparableOperation = - toBuilder().apply { isInverted = true }.build() + private fun ComparableOperation.invertInverted(): ComparableOperation = + toBuilder().apply { isInverted = !isInverted }.build() } } diff --git a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt index 59268fca145..07b3d949fee 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt @@ -31,4 +31,4 @@ fun MathEquation.toRawLatex(divAsFraction: Boolean): String = convertToLatex(div */ fun MathExpression.evaluateAsNumericExpression(): Real? = evaluate() -fun MathExpression.toComparableOperationList(): ComparableOperation = toComparableOperation() +fun MathExpression.toComparableOperation(): ComparableOperation = toComparableOperation() diff --git a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel index dbf95666418..cef614efcb0 100644 --- a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel @@ -66,7 +66,7 @@ oppia_android_test( test_manifest = "//utility:test_manifest", deps = [ "//model/src/main/proto:math_java_proto_lite", - "//testing/src/main/java/org/oppia/android/testing/math:comparable_operation_list_subject", + "//testing/src/main/java/org/oppia/android/testing/math:comparable_operation_subject", "//third_party:androidx_test_ext_junit", "//third_party:com_google_truth_truth", "//third_party:junit_junit", diff --git a/utility/src/test/java/org/oppia/android/util/math/ExpressionToComparableOperationConverterTest.kt b/utility/src/test/java/org/oppia/android/util/math/ExpressionToComparableOperationConverterTest.kt index fd2607b1702..de0d3d391ea 100644 --- a/utility/src/test/java/org/oppia/android/util/math/ExpressionToComparableOperationConverterTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/ExpressionToComparableOperationConverterTest.kt @@ -1,69 +1,1426 @@ package org.oppia.android.util.math +import com.google.common.truth.Truth.assertThat import androidx.test.ext.junit.runners.AndroidJUnit4 import org.junit.Test +import org.oppia.android.util.math.ExpressionToComparableOperationConverter.Companion.toComparableOperation import org.junit.runner.RunWith -import org.oppia.android.app.model.ComparableOperationList.CommutativeAccumulation.AccumulationType.PRODUCT -import org.oppia.android.app.model.ComparableOperationList.CommutativeAccumulation.AccumulationType.SUMMATION +import org.oppia.android.app.model.ComparableOperation.CommutativeAccumulation.AccumulationType.PRODUCT +import org.oppia.android.app.model.ComparableOperation.CommutativeAccumulation.AccumulationType.SUMMATION import org.oppia.android.app.model.MathExpression -import org.oppia.android.testing.math.ComparableOperationListSubject.Companion.assertThat +import org.oppia.android.testing.math.ComparableOperationSubject.Companion.assertThat import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode +import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode.ALL_ERRORS +import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode.REQUIRED_ONLY import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult import org.robolectric.annotation.LooperMode /** Tests for [ExpressionToComparableOperationConverter]. */ -// SameParameterValue: tests should have specific context included/excluded for readability. -@Suppress("SameParameterValue") +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) class ExpressionToComparableOperationConverterTest { - // TODO: add high-level checks for the three types, but don't test in detail since there are - // separate suites. Also, document the separate suites' existence in this suites's KDoc. + // TODO: add tests for comparator/sorting & negation simplification? + + /* Operation creation tests */ + + @Test + fun testConvert_integerConstantExpression_returnsConstantOperation() { + val expression = parseNumericExpression("2") + + val comparable = expression.toComparableOperation() + + assertThat(comparable).hasStructureThatMatches { + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + + @Test + fun testConvert_decimalConstantExpression_returnsConstantOperation() { + val expression = parseNumericExpression("3.14") + + val comparable = expression.toComparableOperation() + + assertThat(comparable).hasStructureThatMatches { + constantTerm { + withValueThat().isIrrationalThat().isWithin(1e-5).of(3.14) + } + } + } + + @Test + fun testConvert_variableExpression_returnsVariableOperation() { + val expression = parseAlgebraicExpression("x") + + val comparable = expression.toComparableOperation() + + assertThat(comparable).hasStructureThatMatches { + variableTerm { + withNameThat().isEqualTo("x") + } + } + } + + @Test + fun testConvert_addition_returnsSummation() { + val expression = parseNumericExpression("1+2") + + val comparable = expression.toComparableOperation() + + assertThat(comparable).hasStructureThatMatches { + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + + @Test + fun testConvert_addition_sameValues_returnsSummationWithBoth() { + val expression = parseNumericExpression("1+1") + + val comparable = expression.toComparableOperation() + + assertThat(comparable).hasStructureThatMatches { + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + } + } + } + + @Test + fun testConvert_subtraction_returnsSummationOfNegative() { + val expression = parseNumericExpression("1-2") + + val comparable = expression.toComparableOperation() + + assertThat(comparable).hasStructureThatMatches { + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(1) { + hasNegatedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + + @Test + fun testConvert_multiplication_returnsProduct() { + val expression = parseNumericExpression("2*3") + + val comparable = expression.toComparableOperation() + + assertThat(comparable).hasStructureThatMatches { + commutativeAccumulationWithType(PRODUCT) { + index(0) { + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + + @Test + fun testConvert_division_returnsProductOfInverted() { + val expression = parseNumericExpression("2/3") + + val comparable = expression.toComparableOperation() + + assertThat(comparable).hasStructureThatMatches { + commutativeAccumulationWithType(PRODUCT) { + index(0) { + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasInvertedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + + @Test + fun testConvert_exponentiation_returnsNonCommutativeOperation() { + val expression = parseNumericExpression("2^3") + + val comparable = expression.toComparableOperation() + + assertThat(comparable).hasStructureThatMatches { + nonCommutativeOperation { + exponentiation { + leftOperand { + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + } + + @Test + fun testConvert_squareRoot_returnsNonCommutativeOperation() { + val expression = parseNumericExpression("sqrt(2)") + + val comparable = expression.toComparableOperation() + + assertThat(comparable).hasStructureThatMatches { + nonCommutativeOperation { + squareRootWithArgument { + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + + @Test + fun testConvert_variableTerm_returnsNonNegativeOperation() { + val expression = parseAlgebraicExpression("x") + + val comparable = expression.toComparableOperation() + + assertThat(comparable).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + } + } + + @Test + fun testConvert_negatedVariable_returnsNegativeVariableOperation() { + val expression = parseAlgebraicExpression("-x") + + val comparable = expression.toComparableOperation() + + assertThat(comparable).hasStructureThatMatches { + hasNegatedPropertyThat().isTrue() + variableTerm { + withNameThat().isEqualTo("x") + } + } + } + + @Test + fun testConvert_positiveVariable_returnsVariableOperation() { + val expression = parseAlgebraicExpression("+x", errorCheckingMode = REQUIRED_ONLY) + + val comparable = expression.toComparableOperation() + + assertThat(comparable).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + } + + @Test + fun testConvert_positiveOfNegativeVariable_returnsNegativeVariableOperation() { + val expression = parseAlgebraicExpression("+-x", errorCheckingMode = REQUIRED_ONLY) + + val comparable = expression.toComparableOperation() + + assertThat(comparable).hasStructureThatMatches { + hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + } + + @Test + fun testConvert_subtractionOfNegative_returnsSummationWithPositives() { + val expression = parseNumericExpression("1--2") + + val comparable = expression.toComparableOperation() + + // Verify that the subtraction & negation cancel out each other. + assertThat(comparable).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + + @Test + fun testConvert_negativePlusPositive_returnsSummationWithFirstTermNegative() { + val expression = parseNumericExpression("-2+1") + + val comparable = expression.toComparableOperation() + + // Verify that the negative only applies to the 2, not to the whole expression. + assertThat(comparable).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(1) { + hasNegatedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + + @Test + fun testConvert_multipleAdditions_returnsCombinedSummation() { + val expression = parseNumericExpression("1+2+3") + + val comparable = expression.toComparableOperation() + + assertThat(comparable).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + + @Test + fun testConvert_multipleSubtractions_returnsCombinedSummation() { + val expression = parseNumericExpression("1-2-3") + + val comparable = expression.toComparableOperation() + + assertThat(comparable).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(1) { + hasNegatedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(2) { + hasNegatedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + + @Test + fun testConvert_additionsAndSubtractions_returnsCombinedSummation() { + val expression = parseNumericExpression("1+2-3-4+5") + + val comparable = expression.toComparableOperation() + + assertThat(comparable).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(5) + index(0) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + index(3) { + hasNegatedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + index(4) { + hasNegatedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + + @Test + fun testConvert_additionsWithNestedAdds_returnsCompletelyCombinedSummation() { + val expression = parseNumericExpression("1+((2+(3+4)+5)+6)") + + val comparable = expression.toComparableOperation() + + assertThat(comparable).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(6) + index(0) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + index(3) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + index(4) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + index(5) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(6) + } + } + } + } + } + + @Test + fun testConvert_subtractsWithNesting_returnsSummationWithDistributedNegation() { + val expression = parseNumericExpression("1-(2+3-4)") + + val comparable = expression.toComparableOperation() + + // Both the 2 & 3 are negative since the subtraction distributes, and the 4 becomes positive. + assertThat(comparable).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(4) + index(0) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + index(2) { + hasNegatedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(3) { + hasNegatedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + + @Test + fun testConvert_subtractsWithNestedSubs_returnsCompletelyCombinedSummation() { + val expression = parseNumericExpression("1-((2-(3-4)-5)-6)") + + val comparable = expression.toComparableOperation() + + // Some of these are positive because of distribution. + assertThat(comparable).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(6) + index(0) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + index(3) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(6) + } + } + index(4) { + hasNegatedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(5) { + hasNegatedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + + @Test + fun testConvert_additionsAndSubtractionsWithNested_returnsCombinedSummation() { + val expression = parseNumericExpression("1++(2-3)+-(4+5--(2+3-1))", REQUIRED_ONLY) + + val comparable = expression.toComparableOperation() + + // This also verifies that negation distributes in the same way as subtraction. + assertThat(comparable).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(8) + index(0) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(3) { + hasNegatedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(4) { + hasNegatedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + index(5) { + hasNegatedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + index(6) { + hasNegatedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + index(7) { + hasNegatedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + } + } + } + + @Test + fun testConvert_multipleMultiplications_returnsCombinedProduct() { + val expression = parseNumericExpression("2*3*4") + + val comparable = expression.toComparableOperation() + + assertThat(comparable).hasStructureThatMatches { + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + index(2) { + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + + @Test + fun testConvert_multipleDivisions_returnsCombinedProduct() { + val expression = parseNumericExpression("2/3/4") + + val comparable = expression.toComparableOperation() + + assertThat(comparable).hasStructureThatMatches { + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasInvertedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + index(2) { + hasInvertedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + + @Test + fun testConvert_multiplicationsAndDivisions_returnsCombinedProduct() { + val expression = parseNumericExpression("2*3/4/5*6") + + val comparable = expression.toComparableOperation() + + assertThat(comparable).hasStructureThatMatches { + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(5) + index(0) { + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + index(2) { + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(6) + } + } + index(3) { + hasInvertedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + index(4) { + hasInvertedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + } + } + } + + @Test + fun testConvert_multiplicationsWithNestedMults_returnsCompletelyCombinedProduct() { + val expression = parseNumericExpression("2*((3*(4*5)*6)*7)") + + val comparable = expression.toComparableOperation() + + assertThat(comparable).hasStructureThatMatches { + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(6) + index(0) { + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + index(2) { + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + index(3) { + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + index(4) { + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(6) + } + } + index(5) { + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(7) + } + } + } + } + } + + @Test + fun testConvert_dividesWithNesting_returnsProductWithDistributedInversion() { + val expression = parseNumericExpression("2/(3*4/5)") + + val comparable = expression.toComparableOperation() + + // Both the 3 & 5 become inverted, and the 5 becomes regular multiplication due to the division + // distribution. + // 2*5*inv(3)*inv(4) + assertThat(comparable).hasStructureThatMatches { + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(4) + index(0) { + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + index(2) { + hasInvertedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + index(3) { + hasInvertedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + + @Test + fun testConvert_divisionsWithNestedDivs_returnsCompletelyCombinedProduct() { + val expression = parseNumericExpression("2/((3/(4/5)/6)/7)") + + val comparable = expression.toComparableOperation() + + // Some of these are non-inverted because of distribution. + assertThat(comparable).hasStructureThatMatches { + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(6) + index(0) { + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + index(2) { + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(6) + } + } + index(3) { + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(7) + } + } + index(4) { + hasInvertedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + index(5) { + hasInvertedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + } + } + } + + @Test + fun testConvert_multiplicationsAndDivisionsWithNested_returnsCombinedProduct() { + val expression = parseNumericExpression("1*(2/3)/(4*5*(2*3/1))") + + val comparable = expression.toComparableOperation() + + assertThat(comparable).hasStructureThatMatches { + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(8) + index(0) { + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(1) { + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(2) { + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(3) { + hasInvertedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(4) { + hasInvertedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + index(5) { + hasInvertedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + index(6) { + hasInvertedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + index(7) { + hasInvertedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + } + } + } + + @Test + fun testConvert_multiplicationWithNoNegatives_returnsPositiveProduct() { + val expression = parseNumericExpression("2*3") + + val comparable = expression.toComparableOperation() + + assertThat(comparable).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + } + } + } + } + + @Test + fun testConvert_multiplicationWithOneNegative_returnsNegativeProduct() { + val expression = parseNumericExpression("2*-3") + + val comparable = expression.toComparableOperation() + + // The entire accumulation is considered negative. + assertThat(comparable).hasStructureThatMatches { + hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + + @Test + fun testConvert_multiplicationWithTwoNegatives_returnsPositiveProduct() { + val expression = parseNumericExpression("-2*-3") + + val comparable = expression.toComparableOperation() + + // The two negatives cancel out. This also verifies that negation can pipe up to top-level + // negation. + assertThat(comparable).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + + @Test + fun testConvert_multiplicationWithThreeNegatives_returnsNegativeProduct() { + val expression = parseNumericExpression("-2*-3*-4") + + val comparable = expression.toComparableOperation() + + // 3 negative operands results in the overall product being negative. + assertThat(comparable).hasStructureThatMatches { + hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + + @Test + fun testConvert_combinedMultDivWithNested_evenNegatives_returnsPositiveProduct() { + val expression = parseNumericExpression("-2*-3/-(4/-(3*2))") + + val comparable = expression.toComparableOperation() + + // There are four negatives, so the overall expression is positive. + assertThat(comparable).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + } + } + + @Test + fun testConvert_combinedMultDivWithNested_oddNegatives_returnsNegativeProduct() { + val expression = parseNumericExpression("-2*-3/-(4/-(3*2*+(-3*7)))", REQUIRED_ONLY) + + val comparable = expression.toComparableOperation() + + // There are five negatives, so the overall expression is negative. Note that this is also + // verifying that the negation properly distributes with the group. + assertThat(comparable).hasStructureThatMatches { + hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() + commutativeAccumulationWithType(PRODUCT) { + // This is a side extra check to ensure that unary positive groups are correctly folded into + // the product. + hasOperandCountThat().isEqualTo(7) + } + } + } + + @Test + fun testConvert_additionAndExp_returnsSummationWithNonCommutative() { + val expression = parseNumericExpression("1+2^3") + + val comparable = expression.toComparableOperation() + + assertThat(comparable).hasStructureThatMatches { + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(2) + index(0) { + nonCommutativeOperation { + exponentiation { + leftOperand { + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + index(1) { + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + } + } + } - // TODO: use utility directly - // TODO: finish tests. - // TODO: add tests for comparator/sorting & negation simplification? + @Test + fun testConvert_additionAndSquareRoot_returnsSummationWithNonCommutative() { + val expression = parseNumericExpression("1+sqrt(2)") + + val comparable = expression.toComparableOperation() + + assertThat(comparable).hasStructureThatMatches { + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(2) + index(0) { + nonCommutativeOperation { + squareRootWithArgument { + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + index(1) { + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + } + } + } + + @Test + fun testConvert_additionWithinExp_returnsSummationWithinNonCommutative() { + val expression = parseNumericExpression("2^(1+3)") + + val comparable = expression.toComparableOperation() + + assertThat(comparable).hasStructureThatMatches { + nonCommutativeOperation { + exponentiation { + leftOperand { + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(2) + index(0) { + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(1) { + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + } + } + } + + @Test + fun testConvert_additionWithinSquareRoot_returnsSummationWithinNonCommutative() { + val expression = parseNumericExpression("sqrt(1+3)") + + val comparable = expression.toComparableOperation() + + assertThat(comparable).hasStructureThatMatches { + nonCommutativeOperation { + squareRootWithArgument { + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(2) + index(0) { + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(1) { + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + } + } + + @Test + fun testConvert_multiplicationAndExp_returnsProductWithNonCommutative() { + val expression = parseNumericExpression("2*3^4") + + val comparable = expression.toComparableOperation() + + assertThat(comparable).hasStructureThatMatches { + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + nonCommutativeOperation { + exponentiation { + leftOperand { + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + index(1) { + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + + @Test + fun testConvert_multiplicationAndSquareRoot_returnsProductWithNonCommutative() { + val expression = parseNumericExpression("2*sqrt(3)") + + val comparable = expression.toComparableOperation() + + assertThat(comparable).hasStructureThatMatches { + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + nonCommutativeOperation { + squareRootWithArgument { + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + index(1) { + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + + @Test + fun testConvert_multiplicationWithinExp_returnsProductWithinNonCommutative() { + val expression = parseNumericExpression("2^(3*4)") + + val comparable = expression.toComparableOperation() + + assertThat(comparable).hasStructureThatMatches { + nonCommutativeOperation { + exponentiation { + leftOperand { + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + index(1) { + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + } + } + + @Test + fun testConvert_multiplicationWithinSquareRoot_returnsProductWithinNonCommutative() { + val expression = parseNumericExpression("sqrt(2*3)") + + val comparable = expression.toComparableOperation() + + assertThat(comparable).hasStructureThatMatches { + nonCommutativeOperation { + squareRootWithArgument { + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + } + } - /* Operation creation */ - // testConvert_constantExpression_returnsConstantOperation - // testConvert_variableExpression_returnsVariableOperation - // testConvert_addition_returnsSummation - // testConvert_subtraction_returnsSummationOfNegative - // testConvert_multiplication_returnsProduct - // testConvert_division_returnsProductOfInverted - // testConvert_exponentiation_returnsNonCommutativeOperation - // testConvert_squareRoot_returnsNonCommutativeOperation - // testConvert_negatedVariable_returnsNegativeVariableOperation - // testConvert_positiveVariable_returnsVariableOperation - // testConvert_positiveOfNegativeVariable_returnsNegativeVariableOperation - // testConvert_subtractionOfNegative_returnsSummationWithPositives - // testConvert_multipleAdditions_returnsCombinedSummation - // testConvert_multipleSubtractions_returnsCombinedSummation - // testConvert_additionsAndSubtractions_returnsCombinedSummation - // testConvert_additionsWithNestedAdds_returnsCompletelyCombinedSummation - // testConvert_subtractsWithNestedSubs_returnsCompletelyCombinedSummation - // testConvert_additionsAndSubtractionsWithNested_returnsCombinedSummation - // testConvert_multipleMultiplications_returnsCombinedProduct - // testConvert_multipleDivisions_returnsCombinedProduct - // testConvert_multiplicationsAndDivisions_returnsCombinedProduct - // testConvert_multiplicationsWithNestedMults_returnsCompletelyCombinedProduct - // testConvert_divisionsWithNestedDivs_returnsCompletelyCombinedProduct - // testConvert_multiplicationsAndDivisionsWithNested_returnsCombinedProduct - // testConvert_multiplicationWithOneNegative_returnsNegativeProduct - // testConvert_multiplicationWithTwoNegatives_returnsPositiveProduct - // testConvert_multiplicationWithThreeNegatives_returnsNegativeProduct - // testConvert_combinedMultDivWithNested_evenNegatives_returnsPositiveProduct - // testConvert_combinedMultDivWithNested_oddNegatives_returnsNegativeProduct - // testConvert_additionAndExp_returnsSummationWithNonCommutative - // testConvert_additionAndSquareRoot_returnsSummationWithNonCommutative - // testConvert_additionWithinExp_returnsSummationWithinNonCommutative - // testConvert_additionWithinSquareRoot_returnsSummationWithinNonCommutative - // testConvert_multiplicationAndExp_returnsProductWithNonCommutative - // testConvert_multiplicationAndSquareRoot_returnsProductWithNonCommutative - // testConvert_multiplicationWithinExp_returnsProductWithinNonCommutative - // testConvert_multiplicationWithinSquareRoot_returnsProductWithinNonCommutative - // testConvert_additionAndMultiplication_returnsSummationOfProduct - // testConvert_multiplicationAndGroupedAddition_returnsProductOfSummation + @Test + fun testConvert_additionAndMultiplication_returnsSummationOfProduct() { + val expression = parseNumericExpression("2*3+1") + + val comparable = expression.toComparableOperation() + + assertThat(comparable).hasStructureThatMatches { + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(2) + index(0) { + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + index(1) { + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + } + } + } + + @Test + fun testConvert_multiplicationAndGroupedAddition_returnsProductOfSummation() { + val expression = parseNumericExpression("2*(3+1)") + + val comparable = expression.toComparableOperation() + + assertThat(comparable).hasStructureThatMatches { + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(2) + index(0) { + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(1) { + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + index(1) { + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } /* Top-level operation sorting */ // testConvert_additionThenSquareRoot_samePrecedence_returnsOpWithSummationFirst @@ -80,6 +1437,7 @@ class ExpressionToComparableOperationConverterTest { // testConvert_twoVariables_invertedThenNegated_returnsOpWithNegatedFirst /* Accumulator sorting */ + // TODO: add sorting for negatives & inverteds. // TODO: mention no tiebreakers since there can't be summations or products adjacent with others // of the same type. // testConvert_additionAndMult_samePrecedence_returnsOpWithSummationFirst @@ -181,8 +1539,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun test1() { // TODO: do something with this - val exp = parseNumericExpressionSuccessfullyWithAllErrors("1") - assertThat(exp.toComparableOperationList()).hasStructureThatMatches { + val exp = parseNumericExpression("1") + assertThat(exp.toComparableOperation()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() constantTerm { @@ -194,8 +1552,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun test2() { // TODO: do something with this - val exp = parseNumericExpressionSuccessfullyWithAllErrors("-1") - assertThat(exp.toComparableOperationList()).hasStructureThatMatches { + val exp = parseNumericExpression("-1") + assertThat(exp.toComparableOperation()).hasStructureThatMatches { hasNegatedPropertyThat().isTrue() hasInvertedPropertyThat().isFalse() constantTerm { @@ -207,8 +1565,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun test3() { // TODO: do something with this - val exp = parseNumericExpressionSuccessfullyWithAllErrors("1+3+4") - assertThat(exp.toComparableOperationList()).hasStructureThatMatches { + val exp = parseNumericExpression("1+3+4") + assertThat(exp.toComparableOperation()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { @@ -241,8 +1599,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun test4() { // TODO: do something with this - val exp = parseNumericExpressionSuccessfullyWithAllErrors("-1-2-3") - assertThat(exp.toComparableOperationList()).hasStructureThatMatches { + val exp = parseNumericExpression("-1-2-3") + assertThat(exp.toComparableOperation()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { @@ -275,8 +1633,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun test5() { // TODO: do something with this - val exp = parseNumericExpressionSuccessfullyWithAllErrors("1+2-3") - assertThat(exp.toComparableOperationList()).hasStructureThatMatches { + val exp = parseNumericExpression("1+2-3") + assertThat(exp.toComparableOperation()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { @@ -309,8 +1667,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun test6() { // TODO: do something with this - val exp = parseNumericExpressionSuccessfullyWithAllErrors("2*3*4") - assertThat(exp.toComparableOperationList()).hasStructureThatMatches { + val exp = parseNumericExpression("2*3*4") + assertThat(exp.toComparableOperation()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(PRODUCT) { @@ -343,8 +1701,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun test7() { // TODO: do something with this - val exp = parseNumericExpressionSuccessfullyWithAllErrors("1-2*3") - assertThat(exp.toComparableOperationList()).hasStructureThatMatches { + val exp = parseNumericExpression("1-2*3") + assertThat(exp.toComparableOperation()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { @@ -384,8 +1742,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun test8() { // TODO: do something with this - val exp = parseNumericExpressionSuccessfullyWithAllErrors("2*3-4") - assertThat(exp.toComparableOperationList()).hasStructureThatMatches { + val exp = parseNumericExpression("2*3-4") + assertThat(exp.toComparableOperation()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { @@ -425,8 +1783,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun test9() { // TODO: do something with this - val exp = parseNumericExpressionSuccessfullyWithAllErrors("1+2*3-4+8*7*6-9") - assertThat(exp.toComparableOperationList()).hasStructureThatMatches { + val exp = parseNumericExpression("1+2*3-4+8*7*6-9") + assertThat(exp.toComparableOperation()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { @@ -508,8 +1866,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun test10() { // TODO: do something with this - val exp = parseNumericExpressionSuccessfullyWithAllErrors("2/3/4") - assertThat(exp.toComparableOperationList()).hasStructureThatMatches { + val exp = parseNumericExpression("2/3/4") + assertThat(exp.toComparableOperation()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(PRODUCT) { @@ -542,8 +1900,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun test11() { // TODO: do something with this - val exp = parseNumericExpressionSuccessfullyWithoutOptionalErrors("2^3^4") - assertThat(exp.toComparableOperationList()).hasStructureThatMatches { + val exp = parseNumericExpression("2^3^4", REQUIRED_ONLY) + assertThat(exp.toComparableOperation()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() nonCommutativeOperation { @@ -585,8 +1943,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun test12() { // TODO: do something with this - val exp = parseNumericExpressionSuccessfullyWithAllErrors("1+2/3+3") - assertThat(exp.toComparableOperationList()).hasStructureThatMatches { + val exp = parseNumericExpression("1+2/3+3") + assertThat(exp.toComparableOperation()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { @@ -633,8 +1991,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun test13() { // TODO: do something with this - val exp = parseNumericExpressionSuccessfullyWithAllErrors("1+(2/3)+3") - assertThat(exp.toComparableOperationList()).hasStructureThatMatches { + val exp = parseNumericExpression("1+(2/3)+3") + assertThat(exp.toComparableOperation()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { @@ -681,8 +2039,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun test14() { // TODO: do something with this - val exp = parseNumericExpressionSuccessfullyWithAllErrors("1+2^3+3") - assertThat(exp.toComparableOperationList()).hasStructureThatMatches { + val exp = parseNumericExpression("1+2^3+3") + assertThat(exp.toComparableOperation()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { @@ -730,8 +2088,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun test15() { // TODO: do something with this - val exp = parseNumericExpressionSuccessfullyWithAllErrors("1+(2^3)+3") - assertThat(exp.toComparableOperationList()).hasStructureThatMatches { + val exp = parseNumericExpression("1+(2^3)+3") + assertThat(exp.toComparableOperation()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { @@ -779,8 +2137,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun test16() { // TODO: do something with this - val exp = parseNumericExpressionSuccessfullyWithAllErrors("2*3/4*7") - assertThat(exp.toComparableOperationList()).hasStructureThatMatches { + val exp = parseNumericExpression("2*3/4*7") + assertThat(exp.toComparableOperation()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(PRODUCT) { @@ -820,8 +2178,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun test17() { // TODO: do something with this - val exp = parseNumericExpressionSuccessfullyWithAllErrors("2*(3/4)*7") - assertThat(exp.toComparableOperationList()).hasStructureThatMatches { + val exp = parseNumericExpression("2*(3/4)*7") + assertThat(exp.toComparableOperation()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(PRODUCT) { @@ -861,8 +2219,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun test18() { // TODO: do something with this - val exp = parseNumericExpressionSuccessfullyWithAllErrors("-3*sqrt(2)") - assertThat(exp.toComparableOperationList()).hasStructureThatMatches { + val exp = parseNumericExpression("-3*sqrt(2)") + assertThat(exp.toComparableOperation()).hasStructureThatMatches { hasNegatedPropertyThat().isTrue() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(PRODUCT) { @@ -894,8 +2252,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun test19() { // TODO: do something with this - val exp = parseNumericExpressionSuccessfullyWithAllErrors("1+(2+(3+(4+5)))") - assertThat(exp.toComparableOperationList()).hasStructureThatMatches { + val exp = parseNumericExpression("1+(2+(3+(4+5)))") + assertThat(exp.toComparableOperation()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { @@ -942,8 +2300,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun test20() { // TODO: do something with this - val exp = parseNumericExpressionSuccessfullyWithAllErrors("2*(3*(4*(5*6)))") - assertThat(exp.toComparableOperationList()).hasStructureThatMatches { + val exp = parseNumericExpression("2*(3*(4*(5*6)))") + assertThat(exp.toComparableOperation()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(PRODUCT) { @@ -990,8 +2348,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun test21() { // TODO: do something with this - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("x") - assertThat(exp.toComparableOperationList()).hasStructureThatMatches { + val exp = parseAlgebraicExpression("x") + assertThat(exp.toComparableOperation()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() variableTerm { @@ -1003,8 +2361,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun test22() { // TODO: do something with this - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("-x") - assertThat(exp.toComparableOperationList()).hasStructureThatMatches { + val exp = parseAlgebraicExpression("-x") + assertThat(exp.toComparableOperation()).hasStructureThatMatches { hasNegatedPropertyThat().isTrue() hasInvertedPropertyThat().isFalse() variableTerm { @@ -1016,8 +2374,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun test23() { // TODO: do something with this - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+x+y") - assertThat(exp.toComparableOperationList()).hasStructureThatMatches { + val exp = parseAlgebraicExpression("1+x+y") + assertThat(exp.toComparableOperation()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { @@ -1050,8 +2408,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun test24() { // TODO: do something with this - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("-1-x-y") - assertThat(exp.toComparableOperationList()).hasStructureThatMatches { + val exp = parseAlgebraicExpression("-1-x-y") + assertThat(exp.toComparableOperation()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { @@ -1084,8 +2442,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun test25() { // TODO: do something with this - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+x-y") - assertThat(exp.toComparableOperationList()).hasStructureThatMatches { + val exp = parseAlgebraicExpression("1+x-y") + assertThat(exp.toComparableOperation()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { @@ -1118,8 +2476,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun test26() { // TODO: do something with this - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("2xy") - assertThat(exp.toComparableOperationList()).hasStructureThatMatches { + val exp = parseAlgebraicExpression("2xy") + assertThat(exp.toComparableOperation()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(PRODUCT) { @@ -1152,8 +2510,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun test27() { // TODO: do something with this - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("1-xy") - assertThat(exp.toComparableOperationList()).hasStructureThatMatches { + val exp = parseAlgebraicExpression("1-xy") + assertThat(exp.toComparableOperation()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { @@ -1193,8 +2551,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun test28() { // TODO: do something with this - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("xy-4") - assertThat(exp.toComparableOperationList()).hasStructureThatMatches { + val exp = parseAlgebraicExpression("xy-4") + assertThat(exp.toComparableOperation()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { @@ -1234,8 +2592,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun test29() { // TODO: do something with this - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+xy-4+yz-9") - assertThat(exp.toComparableOperationList()).hasStructureThatMatches { + val exp = parseAlgebraicExpression("1+xy-4+yz-9") + assertThat(exp.toComparableOperation()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { @@ -1310,8 +2668,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun test30() { // TODO: do something with this - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("2/x/y") - assertThat(exp.toComparableOperationList()).hasStructureThatMatches { + val exp = parseAlgebraicExpression("2/x/y") + assertThat(exp.toComparableOperation()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(PRODUCT) { @@ -1344,8 +2702,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun test31() { // TODO: do something with this - val exp = parseAlgebraicExpressionSuccessfullyWithoutOptionalErrors("x^3^4") - assertThat(exp.toComparableOperationList()).hasStructureThatMatches { + val exp = parseAlgebraicExpression("x^3^4", REQUIRED_ONLY) + assertThat(exp.toComparableOperation()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() nonCommutativeOperation { @@ -1387,8 +2745,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun test32() { // TODO: do something with this - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+x/y+z") - assertThat(exp.toComparableOperationList()).hasStructureThatMatches { + val exp = parseAlgebraicExpression("1+x/y+z") + assertThat(exp.toComparableOperation()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { @@ -1435,8 +2793,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun test33() { // TODO: do something with this - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+(x/y)+z") - assertThat(exp.toComparableOperationList()).hasStructureThatMatches { + val exp = parseAlgebraicExpression("1+(x/y)+z") + assertThat(exp.toComparableOperation()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { @@ -1483,8 +2841,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun test34() { // TODO: do something with this - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+x^3+y") - assertThat(exp.toComparableOperationList()).hasStructureThatMatches { + val exp = parseAlgebraicExpression("1+x^3+y") + assertThat(exp.toComparableOperation()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { @@ -1532,8 +2890,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun test35() { // TODO: do something with this - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+(x^3)+y") - assertThat(exp.toComparableOperationList()).hasStructureThatMatches { + val exp = parseAlgebraicExpression("1+(x^3)+y") + assertThat(exp.toComparableOperation()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { @@ -1581,8 +2939,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun test36() { // TODO: do something with this - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("2*x/y*z") - assertThat(exp.toComparableOperationList()).hasStructureThatMatches { + val exp = parseAlgebraicExpression("2*x/y*z") + assertThat(exp.toComparableOperation()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(PRODUCT) { @@ -1622,8 +2980,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun test37() { // TODO: do something with this - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("2*(x/y)*z") - assertThat(exp.toComparableOperationList()).hasStructureThatMatches { + val exp = parseAlgebraicExpression("2*(x/y)*z") + assertThat(exp.toComparableOperation()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(PRODUCT) { @@ -1663,8 +3021,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun test38() { // TODO: do something with this - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("-2*sqrt(x)") - assertThat(exp.toComparableOperationList()).hasStructureThatMatches { + val exp = parseAlgebraicExpression("-2*sqrt(x)") + assertThat(exp.toComparableOperation()).hasStructureThatMatches { hasNegatedPropertyThat().isTrue() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(PRODUCT) { @@ -1696,8 +3054,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun test39() { // TODO: do something with this - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+(x+(3+(z+y)))") - assertThat(exp.toComparableOperationList()).hasStructureThatMatches { + val exp = parseAlgebraicExpression("1+(x+(3+(z+y)))") + assertThat(exp.toComparableOperation()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(SUMMATION) { @@ -1744,8 +3102,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun test40() { // TODO: do something with this - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("2*(x*(4*(zy)))") - assertThat(exp.toComparableOperationList()).hasStructureThatMatches { + val exp = parseAlgebraicExpression("2*(x*(4*(zy)))") + assertThat(exp.toComparableOperation()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() commutativeAccumulationWithType(PRODUCT) { @@ -1975,66 +3333,31 @@ class ExpressionToComparableOperationConverterTest { } private fun createComparableOperationListFromNumericExpression(expression: String) = - parseNumericExpressionSuccessfullyWithAllErrors(expression).toComparableOperationList() + parseNumericExpression(expression).toComparableOperation() private fun createComparableOperationListFromAlgebraicExpression(expression: String) = - parseAlgebraicExpressionSuccessfullyWithAllErrors(expression).toComparableOperationList() + parseAlgebraicExpression(expression).toComparableOperation() private companion object { - // TODO: fix helper API. - - private fun parseNumericExpressionSuccessfullyWithAllErrors( - expression: String + private fun parseNumericExpression( + expression: String, errorCheckingMode: ErrorCheckingMode = ALL_ERRORS ): MathExpression { - val result = parseNumericExpressionInternal(expression, ErrorCheckingMode.ALL_ERRORS) - return (result as MathParsingResult.Success).result + return MathExpressionParser.parseNumericExpression( + expression, errorCheckingMode + ).retrieveExpectedSuccessfulResult() } - private fun parseNumericExpressionSuccessfullyWithoutOptionalErrors( - expression: String + private fun parseAlgebraicExpression( + expression: String, errorCheckingMode: ErrorCheckingMode = ALL_ERRORS ): MathExpression { - val result = - parseNumericExpressionInternal( - expression, ErrorCheckingMode.REQUIRED_ONLY - ) - return (result as MathParsingResult.Success).result - } - - private fun parseNumericExpressionInternal( - expression: String, - errorCheckingMode: ErrorCheckingMode - ): MathParsingResult { - return MathExpressionParser.parseNumericExpression(expression, errorCheckingMode) - } - - private fun parseAlgebraicExpressionSuccessfullyWithAllErrors( - expression: String, - allowedVariables: List = listOf("x", "y", "z") - ): MathExpression { - val result = - parseAlgebraicExpressionInternal(expression, ErrorCheckingMode.ALL_ERRORS, allowedVariables) - return (result as MathParsingResult.Success).result + return MathExpressionParser.parseAlgebraicExpression( + expression, allowedVariables = listOf("x", "y", "z"), errorCheckingMode + ).retrieveExpectedSuccessfulResult() } - private fun parseAlgebraicExpressionSuccessfullyWithoutOptionalErrors( - expression: String, - allowedVariables: List = listOf("x", "y", "z") - ): MathExpression { - val result = - parseAlgebraicExpressionInternal( - expression, ErrorCheckingMode.REQUIRED_ONLY, allowedVariables - ) - return (result as MathParsingResult.Success).result - } - - private fun parseAlgebraicExpressionInternal( - expression: String, - errorCheckingMode: ErrorCheckingMode, - allowedVariables: List = listOf("x", "y", "z") - ): MathParsingResult { - return MathExpressionParser.parseAlgebraicExpression( - expression, allowedVariables, errorCheckingMode - ) + private fun MathParsingResult.retrieveExpectedSuccessfulResult(): T { + assertThat(this).isInstanceOf(MathParsingResult.Success::class.java) + return (this as MathParsingResult.Success).result } } } From b4001767fd3ea16b4f872e8d4819690959397cc3 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 28 Jan 2022 17:12:20 -0800 Subject: [PATCH 084/162] Finish initial test suite. Still needs to be cleaned up, but after converter refactoring attempts. --- .../org/oppia/android/util/math/BUILD.bazel | 3 +- ...ssionToComparableOperationConverterTest.kt | 1438 +++++++++++++++-- 2 files changed, 1321 insertions(+), 120 deletions(-) diff --git a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel index cef614efcb0..a6fe0270cea 100644 --- a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel @@ -67,10 +67,11 @@ oppia_android_test( deps = [ "//model/src/main/proto:math_java_proto_lite", "//testing/src/main/java/org/oppia/android/testing/math:comparable_operation_subject", - "//third_party:androidx_test_ext_junit", "//third_party:com_google_truth_truth", "//third_party:junit_junit", "//third_party:org_robolectric_robolectric", + "//testing/src/main/java/org/oppia/android/testing/junit:oppia_parameterized_test_runner", + "//testing/src/main/java/org/oppia/android/testing/junit:parameterized_robolectric_test_runner", "//third_party:robolectric_android-all", "//utility/src/main/java/org/oppia/android/util/math:extensions", "//utility/src/main/java/org/oppia/android/util/math:math_expression_parser", diff --git a/utility/src/test/java/org/oppia/android/util/math/ExpressionToComparableOperationConverterTest.kt b/utility/src/test/java/org/oppia/android/util/math/ExpressionToComparableOperationConverterTest.kt index de0d3d391ea..5d4dc8495fe 100644 --- a/utility/src/test/java/org/oppia/android/util/math/ExpressionToComparableOperationConverterTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/ExpressionToComparableOperationConverterTest.kt @@ -1,13 +1,16 @@ package org.oppia.android.util.math import com.google.common.truth.Truth.assertThat -import androidx.test.ext.junit.runners.AndroidJUnit4 import org.junit.Test import org.oppia.android.util.math.ExpressionToComparableOperationConverter.Companion.toComparableOperation import org.junit.runner.RunWith import org.oppia.android.app.model.ComparableOperation.CommutativeAccumulation.AccumulationType.PRODUCT import org.oppia.android.app.model.ComparableOperation.CommutativeAccumulation.AccumulationType.SUMMATION import org.oppia.android.app.model.MathExpression +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Iteration +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Parameter +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.RunParameterized import org.oppia.android.testing.math.ComparableOperationSubject.Companion.assertThat import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode.ALL_ERRORS @@ -15,13 +18,19 @@ import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingM import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult import org.robolectric.annotation.LooperMode -/** Tests for [ExpressionToComparableOperationConverter]. */ +/** + * Tests for [ExpressionToComparableOperationConverter]. + * + * Note that this suite is broken up into distinct sections (designated by block comments) to better + * help organize the different behaviors being tested. + */ // FunctionName: test names are conventionally named with underscores. @Suppress("FunctionName") -@RunWith(AndroidJUnit4::class) +@RunWith(OppiaParameterizedTestRunner::class) @LooperMode(LooperMode.Mode.PAUSED) class ExpressionToComparableOperationConverterTest { - // TODO: add tests for comparator/sorting & negation simplification? + @Parameter lateinit var op1: String + @Parameter lateinit var op2: String /* Operation creation tests */ @@ -586,7 +595,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun testConvert_additionsAndSubtractionsWithNested_returnsCombinedSummation() { - val expression = parseNumericExpression("1++(2-3)+-(4+5--(2+3-1))", REQUIRED_ONLY) + val expression = + parseNumericExpression("1++(2-3)+-(4+5--(2+3-1))", errorCheckingMode = REQUIRED_ONLY) val comparable = expression.toComparableOperation() @@ -1095,7 +1105,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun testConvert_combinedMultDivWithNested_oddNegatives_returnsNegativeProduct() { - val expression = parseNumericExpression("-2*-3/-(4/-(3*2*+(-3*7)))", REQUIRED_ONLY) + val expression = + parseNumericExpression("-2*-3/-(4/-(3*2*+(-3*7)))", errorCheckingMode = REQUIRED_ONLY) val comparable = expression.toComparableOperation() @@ -1422,119 +1433,1306 @@ class ExpressionToComparableOperationConverterTest { } } - /* Top-level operation sorting */ - // testConvert_additionThenSquareRoot_samePrecedence_returnsOpWithSummationFirst - // testConvert_squareRootThenAddition_samePrecedence_returnsOpWithSummationFirst - // testConvert_additionThenExp_samePrecedence_returnsOpWithSummationFirst - // testConvert_exponentiationThenAddition_samePrecedence_returnsOpWithSummationFirst - // testConvert_constantThenSquareRoot_samePrecedence_returnsOpWithNonCommutativeFirst - // testConvert_squareRootThenConstant_samePrecedence_returnsOpWithNonCommutativeFirst - // testConvert_constantThenExp_samePrecedence_returnsOpWithNonCommutativeFirst - // testConvert_exponentiationThenConstant_samePrecedence_returnsOpWithNonCommutativeFirst - // testConvert_constantThenVariable_samePrecedence_returnsOpWithConstantFirst - // testConvert_variableThenConstant_samePrecedence_returnsOpWithConstantFirst - // testConvert_twoVariables_negatedThenInverted_returnsOpWithNegatedFirst - // testConvert_twoVariables_invertedThenNegated_returnsOpWithNegatedFirst + /* + * Top-level operation sorting. Note that negation & inversion can't be sorted relative to each + * other since they'll never co-occur (though the underlying sorting is set up to prioritize + * negative operations over inverted). + * + * Note also that accumulators can't be cross-verified for order since whether a summation or + * product is first entirely depends on the expression itself (since multiplication and division + * are higher precedence than addition and subtraction). Thus, these cases can't be tested for + * sorting order. + */ - /* Accumulator sorting */ - // TODO: add sorting for negatives & inverteds. - // TODO: mention no tiebreakers since there can't be summations or products adjacent with others - // of the same type. - // testConvert_additionAndMult_samePrecedence_returnsOpWithSummationFirst - // testConvert_multiplicationAndAddition_samePrecedence_returnsOpWithSummationFirst - // testConvert_additionAndMult_samePrecedenceAsNested_returnsOpWithSummationFirst - // testConvert_multiplicationAndAddition_samePrecedenceAsNested_returnsOpWithSummationFirst + @Test + @RunParameterized( + Iteration(name = "(1+2)*sqrt(3)", "op1=(1+2)", "op2=sqrt(3)"), + Iteration(name = "sqrt(3)*(1+2)", "op1=sqrt(3)", "op2=(1+2)"), + Iteration(name = "(1+2)*(3^4)", "op1=(1+2)", "op2=(3^4)"), + Iteration(name = "(3^4)*(1+2)", "op1=(3^4)", "op2=(1+2)") + ) + fun testConvert_additionAndNonCommutativeOp_samePrecedence_returnsOpWithSummationFirst() { + val expression = parseNumericExpression("$op1 * $op2") + + val comparable = expression.toComparableOperation() + + // Verify that the summation is still first since it's higher priority during sorting. + assertThat(comparable).hasStructureThatMatches { + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + commutativeAccumulationWithType(SUMMATION) {} + } + index(1) { + nonCommutativeOperation {} + } + } + } + } + + @Test + @RunParameterized( + Iteration(name = "2+sqrt(3)", "op1=2", "op2=sqrt(3)"), + Iteration(name = "sqrt(3)+2", "op1=sqrt(3)", "op2=2"), + Iteration(name = "2+3^4", "op1=2", "op2=3^4"), + Iteration(name = "3^4+2", "op1=3^4", "op2=2") + ) + fun testConvert_constantAndNonCommutativeOp_samePrecedence_returnsOpWithNonCommutativeFirst() { + val expression = parseNumericExpression("$op1 + $op2") + + val comparable = expression.toComparableOperation() + + // Verify that the non-commutative operation is first since it's higher priority during sorting. + assertThat(comparable).hasStructureThatMatches { + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(2) + index(0) { + nonCommutativeOperation {} + } + index(1) { + constantTerm {} + } + } + } + } + + @Test + @RunParameterized( + Iteration(name = "2*x", "op1=2", "op2=x"), + Iteration(name = "x*2", "op1=x", "op2=2") + ) + fun testConvert_constantAndVariable_samePrecedence_returnsOpWithConstantFirst() { + val expression = parseAlgebraicExpression("$op1 * $op2") + + val comparable = expression.toComparableOperation() + + // Verify that the constant is first since it's higher priority during sorting. + assertThat(comparable).hasStructureThatMatches { + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + constantTerm {} + } + index(1) { + variableTerm {} + } + } + } + } + + @Test + @RunParameterized( + Iteration(name = "x+(-y)", "op1=x", "op2=(-y)"), + Iteration(name = "(-y)+x", "op1=(-y)", "op2=x") + ) + fun testConvert_positiveAndNegativeVariables_returnsOpWithNegatedLast() { + val expression = parseAlgebraicExpression("$op1 + $op2") + + val comparable = expression.toComparableOperation() + + // Verify that the positive term is first since it's higher priority during sorting. + assertThat(comparable).hasStructureThatMatches { + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + variableTerm {} + } + index(1) { + hasNegatedPropertyThat().isTrue() + variableTerm {} + } + } + } + } + + @Test + @RunParameterized( + Iteration(name = "x*(1/y)", "op1=x", "op2=(1/y)"), + Iteration(name = "(1/y)*x", "op1=(1/y)", "op2=x") + ) + fun testConvert_invertedAndNonInvertedVariables_returnsOpWithInvertedLast() { + val expression = parseAlgebraicExpression("$op1 * $op2") + + val comparable = expression.toComparableOperation() + + // Verify that the non-inverted term is first since it's higher priority during sorting. + assertThat(comparable).hasStructureThatMatches { + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasInvertedPropertyThat().isFalse() + constantTerm {} + } + index(1) { + hasInvertedPropertyThat().isFalse() + variableTerm {} + } + index(2) { + hasInvertedPropertyThat().isTrue() + variableTerm {} + } + } + } + } + + @Test + @RunParameterized( + Iteration(name = "(1+2)*(2+3)", "op1=1+2", "op2=2+3"), + Iteration(name = "(2+1)*(2+3)", "op1=2+1", "op2=2+3"), + Iteration(name = "(1+2)*(3+2)", "op1=1+2", "op2=3+2"), + Iteration(name = "(2+1)*(3+2)", "op1=2+1", "op2=3+2"), + Iteration(name = "(2+3)*(1+2)", "op1=2+3", "op2=1+2"), + Iteration(name = "(2+3)*(2+1)", "op1=2+3", "op2=2+1"), + Iteration(name = "(3+2)*(1+2)", "op1=3+2", "op2=1+2"), + Iteration(name = "(3+2)*(2+1)", "op1=3+2", "op2=2+1") + ) + fun testConvert_twoAdditionsInProduct_smallerSumIsFirst() { + val expression = parseNumericExpression("($op1)*($op2)") + + val comparable = expression.toComparableOperation() + + // Summations are deterministically sorted regardless of how the original expression structures + // them. + assertThat(comparable).hasStructureThatMatches { + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(2) + index(0) { + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + } + } + index(1) { + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(2) + index(0) { + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + + @Test + @RunParameterized( + Iteration(name = "(2*3)+(4*5)", "op1=2*3", "op2=4*5"), + Iteration(name = "(3*2)+(4*5)", "op1=3*2", "op2=4*5"), + Iteration(name = "(2*3)+(5*4)", "op1=2*3", "op2=5*4"), + Iteration(name = "(3*2)+(5*4)", "op1=3*2", "op2=5*4"), + Iteration(name = "(4*5)+(2*3)", "op1=4*5", "op2=2*3"), + Iteration(name = "(4*5)+(3*2)", "op1=4*5", "op2=3*2"), + Iteration(name = "(5*4)+(2*3)", "op1=5*4", "op2=2*3"), + Iteration(name = "(5*4)+(3*2)", "op1=5*4", "op2=3*2") + ) + fun testConvert_twoMultiplicationsInSum_smallerProductIsFirst() { + val expression = parseNumericExpression("($op1)+($op2)") + + val comparable = expression.toComparableOperation() + + // Products are deterministically sorted regardless of how the original expression structures + // them. + assertThat(comparable).hasStructureThatMatches { + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(2) + index(0) { + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + index(1) { + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + } + + /* Non-commutative sorting */ + + @Test + @RunParameterized( + Iteration(name = "(2^3)+sqrt(2)", "op1=(2^3)", "op2=sqrt(2)"), + Iteration(name = "sqrt(2)+(2^3)", "op1=sqrt(2)", "op2=(2^3)") + ) + fun testConvert_expAndSqrt_samePrecedence_returnsOpWithExpThenSqrt() { + val expression = parseNumericExpression("$op1+$op2") + + val comparable = expression.toComparableOperation() + + // Verify that the exponentiation is first since it's higher priority during sorting. + assertThat(comparable).hasStructureThatMatches { + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(2) + index(0) { + nonCommutativeOperation { + exponentiation {} + } + } + index(1) { + nonCommutativeOperation { + squareRootWithArgument {} + } + } + } + } + } + + @Test + @RunParameterized( + // const^const + const^const + Iteration(name = "(2^3)+(4^5)", "op1=2^3", "op2=4^5"), + Iteration(name = "(2^5)+(4^3)", "op1=2^5", "op2=4^3"), + Iteration(name = "(4^3)+(2^5)", "op1=4^3", "op2=2^5"), + Iteration(name = "(4^5)+(2^3)", "op1=4^5", "op2=2^3"), + // const^var + const^var + Iteration(name = "(2^x)+(4^5)", "op1=2^x", "op2=4^5"), + Iteration(name = "(2^5)+(4^x)", "op1=2^5", "op2=4^x"), + Iteration(name = "(4^x)+(2^5)", "op1=4^x", "op2=2^5"), + Iteration(name = "(4^5)+(2^x)", "op1=4^5", "op2=2^x"), + // const^(var or const) + const^(const or var) + Iteration(name = "(2^x)+(4^y)", "op1=2^x", "op2=4^y"), + Iteration(name = "(2^y)+(4^x)", "op1=2^y", "op2=4^x"), + Iteration(name = "(4^x)+(2^y)", "op1=4^x", "op2=2^y"), + Iteration(name = "(4^y)+(2^x)", "op1=4^y", "op2=2^x") + ) + fun testConvert_addTwoExps_lhs1Const_rhs1Any_lhs2Const_rhs2Any_returnsOpWithLhsSizeBasedOrder() { + // Note that optional errors need to be disabled as part of testing exponents as powers. + val expression = + parseAlgebraicExpression("($op1)+($op2)", errorCheckingMode = REQUIRED_ONLY) + + val comparable = expression.toComparableOperation() + + // Verify that the exponentiations are ordered based on the left-hand operand's size. + assertThat(comparable).hasStructureThatMatches { + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(2) + index(0) { + nonCommutativeOperation { + exponentiation { + leftOperand { + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + index(1) { + nonCommutativeOperation { + exponentiation { + leftOperand { + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + } + } + } + + @Test + @RunParameterized( + // var^const + var^const + Iteration(name = "(u^3)+(v^5)", "op1=u^3", "op2=v^5"), + Iteration(name = "(u^5)+(v^3)", "op1=u^5", "op2=v^3"), + Iteration(name = "(v^3)+(u^5)", "op1=v^3", "op2=u^5"), + Iteration(name = "(v^5)+(u^3)", "op1=v^5", "op2=u^3"), + // var^var + var^var + Iteration(name = "(u^x)+(v^5)", "op1=u^x", "op2=v^5"), + Iteration(name = "(u^5)+(v^x)", "op1=u^5", "op2=v^x"), + Iteration(name = "(v^x)+(u^5)", "op1=v^x", "op2=u^5"), + Iteration(name = "(v^5)+(u^x)", "op1=v^5", "op2=u^x"), + // var^(var or const) + var^(const or var) + Iteration(name = "(u^x)+(v^y)", "op1=u^x", "op2=v^y"), + Iteration(name = "(u^y)+(v^x)", "op1=u^y", "op2=v^x"), + Iteration(name = "(v^x)+(u^y)", "op1=v^x", "op2=u^y"), + Iteration(name = "(v^y)+(u^x)", "op1=v^y", "op2=u^x") + ) + fun testConvert_addTwoExps_lhs1Var_rhs1Any_lhs2Var_rhs2Any_returnsOpWithLhsLetterBasedOrder() { + // Note that optional errors need to be disabled as part of testing exponents as powers. + val expression = + parseAlgebraicExpression( + "($op1)+($op2)", + allowedVariables = listOf("u", "v", "x", "y"), + errorCheckingMode = REQUIRED_ONLY + ) + + val comparable = expression.toComparableOperation() + + // Verify that the exponentiations are ordered based on the left-hand operand's lexicographical + // ordering. + assertThat(comparable).hasStructureThatMatches { + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(2) + index(0) { + nonCommutativeOperation { + exponentiation { + leftOperand { + variableTerm { + withNameThat().isEqualTo("u") + } + } + } + } + } + index(1) { + nonCommutativeOperation { + exponentiation { + leftOperand { + variableTerm { + withNameThat().isEqualTo("v") + } + } + } + } + } + } + } + } + + @Test + @RunParameterized( + Iteration(name = "sqrt(2)+sqrt(3)", "op1=2", "op2=3"), + Iteration(name = "sqrt(3)+sqrt(2)", "op1=3", "op2=2") + ) + fun testConvert_addTwoSqrts_leftConst_rightConst_returnsOpWithSqrtsByArgSize() { + val expression = parseNumericExpression("sqrt($op1)+sqrt($op2)") + + val comparable = expression.toComparableOperation() + + // The square roots should be ordered based on their argument sorting. + assertThat(comparable).hasStructureThatMatches { + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(2) + index(0) { + nonCommutativeOperation { + squareRootWithArgument { + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + index(1) { + nonCommutativeOperation { + squareRootWithArgument { + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + } + } + + @Test + @RunParameterized( + Iteration(name = "sqrt(x)+sqrt(y)", "op1=x", "op2=y"), + Iteration(name = "sqrt(y)+sqrt(x)", "op1=y", "op2=x") + ) + fun testConvert_addTwoSqrts_leftVar_rightVar_returnsOpWithSqrtsByVariableOrder() { + val expression = parseAlgebraicExpression("sqrt($op1)+sqrt($op2)") + + val comparable = expression.toComparableOperation() + + // The square roots should be ordered based on their argument lexicographical sorting. + assertThat(comparable).hasStructureThatMatches { + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(2) + index(0) { + nonCommutativeOperation { + squareRootWithArgument { + variableTerm { + withNameThat().isEqualTo("x") + } + } + } + } + index(1) { + nonCommutativeOperation { + squareRootWithArgument { + variableTerm { + withNameThat().isEqualTo("y") + } + } + } + } + } + } + } + + @Test + @RunParameterized( + Iteration(name = "sqrt(2)+sqrt(x)", "op1=2", "op2=x"), + Iteration(name = "sqrt(x)+sqrt(2)", "op1=x", "op2=2") + ) + fun testConvert_addTwoSqrts_oneConst_oneVar_returnsOpWithSqrtsByConstFirst() { + val expression = parseAlgebraicExpression("sqrt($op1)+sqrt($op2)") + + val comparable = expression.toComparableOperation() + + // Constant-before-variable ordering also affects peer square root orders. + assertThat(comparable).hasStructureThatMatches { + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(2) + index(0) { + nonCommutativeOperation { + squareRootWithArgument { + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + index(1) { + nonCommutativeOperation { + squareRootWithArgument { + variableTerm { + withNameThat().isEqualTo("x") + } + } + } + } + } + } + } + + /* Constant & variable sorting */ + + @Test + @RunParameterized( + Iteration(name = "2+3", "op1=2", "op2=3"), + Iteration(name = "3+2", "op1=3", "op2=2") + ) + fun testConvert_addTwoConstants_leftInteger_rightInteger_returnsOpSortedByValues() { + val expression = parseNumericExpression("$op1 + $op2") + + val comparable = expression.toComparableOperation() + + // The order of the summation should be based on the constants' values. + assertThat(comparable).hasStructureThatMatches { + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(2) + index(0) { + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + + @Test + @RunParameterized( + Iteration(name = "3.2+6.3", "op1=3.2", "op2=6.3"), + Iteration(name = "6.3+3.2", "op1=6.3", "op2=3.2") + ) + fun testConvert_addTwoConstants_leftDouble_rightDouble_returnsOpSortedByValues() { + val expression = parseNumericExpression("$op1 + $op2") + + val comparable = expression.toComparableOperation() + + // The order of the summation should be based on the constants' values. + assertThat(comparable).hasStructureThatMatches { + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(2) + index(0) { + constantTerm { + withValueThat().isIrrationalThat().isWithin(1e-5).of(3.2) + } + } + index(1) { + constantTerm { + withValueThat().isIrrationalThat().isWithin(1e-5).of(6.3) + } + } + } + } + } + + @Test + @RunParameterized( + Iteration(name = "3+6.3", "op1=3", "op2=6.3"), + Iteration(name = "6.3+3", "op1=6.3", "op2=3") + ) + fun testConvert_addTwoConstants_smallInt_largeDouble_returnsOpWithIntFirst() { + val expression = parseNumericExpression("$op1 + $op2") + + val comparable = expression.toComparableOperation() + + // The order of the summation should be based on the constants' values. + assertThat(comparable).hasStructureThatMatches { + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(2) + index(0) { + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + index(1) { + constantTerm { + withValueThat().isIrrationalThat().isWithin(1e-5).of(6.3) + } + } + } + } + } + + @Test + @RunParameterized( + Iteration(name = "8+6.3", "op1=8", "op2=6.3"), + Iteration(name = "6.3+8", "op1=6.3", "op2=8") + ) + fun testConvert_addTwoConstants_largeInt_smallDouble_returnsOpWithDoubleFirst() { + val expression = parseNumericExpression("$op1 + $op2") + + val comparable = expression.toComparableOperation() + + // The order of the summation should be based on the constants' values. + assertThat(comparable).hasStructureThatMatches { + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(2) + index(0) { + constantTerm { + withValueThat().isIrrationalThat().isWithin(1e-5).of(6.3) + } + } + index(1) { + constantTerm { + withValueThat().isIntegerThat().isEqualTo(8) + } + } + } + } + } + + @Test + @RunParameterized( + Iteration(name = "x+6", "op1=x", "op2=6"), + Iteration(name = "6+x", "op1=6", "op2=x") + ) + fun testConvert_addVarAndIntConstant_returnsOpWithConstantFirst() { + val expression = parseAlgebraicExpression("$op1 + $op2") + + val comparable = expression.toComparableOperation() + + // Constants are always ordered before variables. + assertThat(comparable).hasStructureThatMatches { + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(2) + index(0) { + constantTerm { + withValueThat().isIntegerThat().isEqualTo(6) + } + } + index(1) { + variableTerm { + withNameThat().isEqualTo("x") + } + } + } + } + } + + @Test + @RunParameterized( + Iteration(name = "3.6+x", "op1=3.6", "op2=x"), + Iteration(name = "x+3.6", "op1=x", "op2=3.6") + ) + fun testConvert_addVarAndDoubleConstant_returnsOpWithConstantFirst() { + val expression = parseAlgebraicExpression("$op1 + $op2") + + val comparable = expression.toComparableOperation() + + // The order of the summation should be based on the constants' values. + assertThat(comparable).hasStructureThatMatches { + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(2) + index(0) { + constantTerm { + withValueThat().isIrrationalThat().isWithin(1e-5).of(3.6) + } + } + index(1) { + variableTerm { + withNameThat().isEqualTo("x") + } + } + } + } + } + + @Test + fun testConvert_addTwoVariables_leftX_rightX_returnsOpBothXs() { + val expression = parseAlgebraicExpression("x + x") + + val comparable = expression.toComparableOperation() + + assertThat(comparable).hasStructureThatMatches { + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(2) + index(0) { + variableTerm { + withNameThat().isEqualTo("x") + } + } + index(1) { + variableTerm { + withNameThat().isEqualTo("x") + } + } + } + } + } + + @Test + @RunParameterized( + Iteration(name = "x+y", "op1=x", "op2=y"), + Iteration(name = "y+x", "op1=y", "op2=x") + ) + fun testConvert_addTwoVariables_oneX_oneY_returnsOpWithXThenY() { + val expression = parseAlgebraicExpression("$op1 + $op2") + + val comparable = expression.toComparableOperation() + + // Variables are sorted lexicographically. + assertThat(comparable).hasStructureThatMatches { + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(2) + index(0) { + variableTerm { + withNameThat().isEqualTo("x") + } + } + index(1) { + variableTerm { + withNameThat().isEqualTo("y") + } + } + } + } + } + + @Test + fun testConvert_addMultipleVars_returnsOpWithThemInOrder() { + val expression = parseAlgebraicExpression("x + z + x + y + x") - /* Non-commutative sorting */ - // testConvert_addExpThenSqrt_samePrecedence_returnsOpWithExpThenSqrt - // testConvert_addSqrtThenExp_samePrecedence_returnsOpWithExpThenSqrt - // testConvert_addTwoExps_lhs1Const_rhs1Const_lhs2Const_rhs2Const_returnsOpWithExp1Then2 - // ... parameterized: - // const^const const^const - // var^const const^const - // const^var const^const - // var^var const^const - // - // const^const var^const - // var^const var^const - // const^var var^const - // var^var var^const - // - // const^const const^var - // var^const const^var - // const^var const^var - // var^var const^var - // - // const^const var^var - // var^const var^var - // const^var var^var - // var^var var^var - // ... - // testConvert_addTwoSqrts_leftConst_rightConst_returnsOpWithSqrt1ThenSqrt2 - // testConvert_addTwoSqrts_leftVar_rightConst_returnsOpWithSqrt2ThenSqrt1 - // testConvert_addTwoSqrts_leftConst_rightVar_returnsOpWithSqrt1ThenSqrt2 - // testConvert_addTwoSqrts_leftVar_rightVar_returnsOpWithSqrt1ThenSqrt2 - - // testConvert_addTwoExps_lhs1Var_rhs1Const_lhs2Const_rhs2Const_returnsOpWithExp2Then1 - // testConvert_addTwoExps_lhs1Const_rhs1Var_lhs2Const_rhs2Const_returnsOpWithExp2Then1 - // testConvert_addTwoExps_lhs1Const_rhs1Var_lhs2Const_rhs2Var_returnsOpWithExp1Then2 - // testConvert_addTwoExps_lhs1Const_rhs1Var_lhs2Var_rhs2Const_returnsOpWithExp1Then2 - - /* Constant sorting */ - // testConvert_addTwoConstants_leftInteger2_rightInteger3_returnsOpWith2Then3 - // ... parameterized: - // left: 2 right: 3 - // left: 3 right: 2 - // - // left: 2 right: 3.14 - // left: 3.14 right: 2 - // - // left: 4 right: 3.14 - // left: 3.14 right: 4 - // - // left: 3.14 right: 6.28 - // left: 6.28 right: 3.14 - // ... - - /* Variable sorting */ - // testConvert_addTwoVariables_leftX_rightX_returnsOpBothXs - // testConvert_addTwoVariables_leftX_rightY_returnsOpWithXThenY - // ... parameterized: - // x, y; y, x; y, z; z, y; x, y, z; z, y, x - // ... + val comparable = expression.toComparableOperation() + + // Variables are sorted lexicographically. + assertThat(comparable).hasStructureThatMatches { + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(5) + index(0) { + variableTerm { + withNameThat().isEqualTo("x") + } + } + index(1) { + variableTerm { + withNameThat().isEqualTo("x") + } + } + index(2) { + variableTerm { + withNameThat().isEqualTo("x") + } + } + index(3) { + variableTerm { + withNameThat().isEqualTo("y") + } + } + index(4) { + variableTerm { + withNameThat().isEqualTo("z") + } + } + } + } + } /* Combined operations */ - // testConvert_allOperations_withNestedGroups_returnsCorrectlyStructuredAndOrderedOperation - - /* Equivalence checks */ - // testEquals_twoAdditionOps_differentByCommutativity_areEqual - // testEquals_twoAdditionOps_differentByAssociativity_areEqual - // testEquals_twoAdditionOps_differentByAssociativityAndCommutativity_areEqual - // testEquals_twoAdditionOps_differentByValue_areNotEqual - // testEquals_twoAdditionOps_differentByEvaluation_areNotEqual - // testEquals_twoMultOps_differentByCommutativity_areEqual - // testEquals_twoMultOps_differentByAssociativity_areEqual - // testEquals_twoMultOps_differentByAssociativityAndCommutativity_areEqual - // testEquals_twoMultOps_differentByValue_areNotEqual - // testEquals_twoMultOps_differentByEvaluation_areNotEqual - // TODO: for this & the next one, test with three operations (e.g. 2 / 3 / 4). - // testEquals_twoSubOps_same_areEqual - // testEquals_twoSubOps_differentByOrder_areNotEqual - // testEquals_twoSubOps_differentByValue_areNotEqual - // testEquals_twoDivOps_same_areEqual - // testEquals_twoDivOps_differentByOrder_areNotEqual - // testEquals_twoDivOps_differentByValue_areNotEqual - // testEquals_twoExps_same_areEqual - // testEquals_twoExps_differentByOrder_areNotEqual - // testEquals_twoExps_differentByValue_areNotEqual - // testEquals_twoSqrts_same_areEqual - // testEquals_twoSqrts_differentByValue_areNotEqual - // testEquals_twoOps_addsAndSubs_differentByOrder_areEqual - // testEquals_twoOps_multsAndDivs_differentByOrder_areEqual - // testEquals_twoOps_addsSubsMultsAndDivs_differentByOrder_areEqual - // testEquals_twoOps_allOperations_differentByOrder_areEqual - // testEquals_twoOps_allOperations_oneNestedDifferentByValue_areNotEqual - // testEquals_twoOps_allOperations_oneMissingTerm_areNotEqual + + @Test + fun testConvert_allOperations_withNestedGroups_returnsCorrectlyStructuredAndOrderedOperation() { + val expression = parseAlgebraicExpression("√(1+2*3)+-2^3*4/7-(2yx+x^(2+1)*(17/3))/-(x+(y+1.2))") + + val comparable = expression.toComparableOperation() + + assertThat(comparable).hasStructureThatMatches { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + // Sum of (ordered based on expected sorting criteria): + // - -(2yx+x^(2+1)*(17/3))/-(x+(y+1.2)) -> evaluates to positive + // - -2^3*4/7 + // - √(1+2*3) + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + // Product of (in sorted order): + // - 2yx+x^(2+1)*(17/3) + // - inverse of x+(y+1.2) + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + // Sum of (in sorted order): + // - x^(2+1)*(17/3) + // - 2yx + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + // Product of (in sorted order): + // - x^(2+1) + // - 17 + // - inverse of 3 + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + // Exponentiation of: x and 2+1. + nonCommutativeOperation { + exponentiation { + leftOperand { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + rightOperand { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + // Summation of (in sorted order): 1 and 2. + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(17) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + // Product of (in sorted order): 2, x, and y. + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("y") + } + } + } + } + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isTrue() + // Sum of (in sorted order): 1.2, x, and y. + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIrrationalThat().isWithin(1e-5).of(1.2) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("x") + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + variableTerm { + withNameThat().isEqualTo("y") + } + } + } + } + } + } + index(1) { + hasNegatedPropertyThat().isTrue() + hasInvertedPropertyThat().isFalse() + // Product of: + // - 2^3 + // - 4** + // - inverse of 7 + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(3) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + // Exponentiation of: 2 and 3. + nonCommutativeOperation { + exponentiation { + leftOperand { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isTrue() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(7) + } + } + } + } + index(2) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + nonCommutativeOperation { + squareRootWithArgument { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + // Sum of (in sorted order): + // - 2*3 + // - 1 + commutativeAccumulationWithType(SUMMATION) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + // Product of: 2 and 3. + commutativeAccumulationWithType(PRODUCT) { + hasOperandCountThat().isEqualTo(2) + index(0) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + index(1) { + hasNegatedPropertyThat().isFalse() + hasInvertedPropertyThat().isFalse() + constantTerm { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + } + } + } + } + } + } + } + + /* + * Equivalence checks. Note that these don't specifically verify doubles since they may not have + * reliable equivalence checking (and may instead require threshold checking for approximated + * equivalence). + * + * Further, these checks are using vanilla equivalence checking since they rely on the opreations + * being properly sorted. + */ + + @Test + fun testEquals_additionOps_differentByCommutativity_areEqual() { + val comparable1 = parseNumericExpression("1 + 2").toComparableOperation() + val comparable2 = parseNumericExpression("2 + 1").toComparableOperation() + + assertThat(comparable1).isEqualTo(comparable2) + } + + @Test + fun testEquals_additionOps_differentByAssociativity_areEqual() { + val comparable1 = parseNumericExpression("1 + (2 + 3)").toComparableOperation() + val comparable2 = parseNumericExpression("(1 + 2) + 3").toComparableOperation() + + assertThat(comparable1).isEqualTo(comparable2) + } + + @Test + fun testEquals_additionOps_differentByAssociativityAndCommutativity_areEqual() { + val comparable1 = parseNumericExpression("1 + (2 + 3)").toComparableOperation() + val comparable2 = parseNumericExpression("(2 + 1) + 3").toComparableOperation() + + assertThat(comparable1).isEqualTo(comparable2) + } + + @Test + fun testEquals_additionOps_differentByValue_areNotEqual() { + val comparable1 = parseNumericExpression("1 + 2").toComparableOperation() + val comparable2 = parseNumericExpression("1 + 3").toComparableOperation() + + assertThat(comparable1).isNotEqualTo(comparable2) + } + + @Test + fun testEquals_additionOps_sameOnlyByEvaluation_areNotEqual() { + val comparable1 = parseNumericExpression("1 + 2 + 2 + 1").toComparableOperation() + val comparable2 = parseNumericExpression("1 + 2 + 3").toComparableOperation() + + // While the two expressions are numerically equivalent, they aren't comparable since there are + // extra terms in one (more than trivial rearranging is required to determine that they're + // equal). + assertThat(comparable1).isNotEqualTo(comparable2) + } + + @Test + fun testEquals_multiplicationOps_differentByCommutativity_areEqual() { + val comparable1 = parseNumericExpression("2 * 3").toComparableOperation() + val comparable2 = parseNumericExpression("3 * 2").toComparableOperation() + + assertThat(comparable1).isEqualTo(comparable2) + } + + @Test + fun testEquals_multiplicationOps_differentByAssociativity_areEqual() { + val comparable1 = parseNumericExpression("2 * (3 * 4)").toComparableOperation() + val comparable2 = parseNumericExpression("(2 * 3) * 4").toComparableOperation() + + assertThat(comparable1).isEqualTo(comparable2) + } + + @Test + fun testEquals_multiplicationOps_differentByAssociativityAndCommutativity_areEqual() { + val comparable1 = parseNumericExpression("2 * (3 * 4)").toComparableOperation() + val comparable2 = parseNumericExpression("(3 * 2) * 4").toComparableOperation() + + assertThat(comparable1).isEqualTo(comparable2) + } + + @Test + fun testEquals_multiplicationOps_differentByValue_areNotEqual() { + val comparable1 = parseNumericExpression("2 * 3").toComparableOperation() + val comparable2 = parseNumericExpression("2 * 4").toComparableOperation() + + assertThat(comparable1).isNotEqualTo(comparable2) + } + + @Test + fun testEquals_multiplicationOps_sameOnlyByEvaluation_areNotEqual() { + val comparable1 = parseNumericExpression("2 * 3 * 4").toComparableOperation() + val comparable2 = parseNumericExpression("2 * 2 * 2 * 3").toComparableOperation() + + // While the two expressions are numerically equivalent, they aren't comparable since there are + // extra terms in one (more than trivial rearranging is required to determine that they're + // equal). + assertThat(comparable1).isNotEqualTo(comparable2) + } + + @Test + fun testEquals_subtractionOps_same_areEqual() { + val comparable1 = parseNumericExpression("1 - 2").toComparableOperation() + val comparable2 = parseNumericExpression("1 - 2").toComparableOperation() + + assertThat(comparable1).isEqualTo(comparable2) + } + + @Test + fun testEquals_subtractionOps_differentByOrder_areNotEqual() { + val comparable1 = parseNumericExpression("1 - 2").toComparableOperation() + val comparable2 = parseNumericExpression("2 - 1").toComparableOperation() + + // Subtraction is not commutative. + assertThat(comparable1).isNotEqualTo(comparable2) + } + + @Test + fun testEquals_subtractionOps_differentByAssociativity_areNotEqual() { + val comparable1 = parseNumericExpression("1 - (2 - 3)").toComparableOperation() + val comparable2 = parseNumericExpression("(1 - 2) - 3").toComparableOperation() + + // Subtraction is not associative. + assertThat(comparable1).isNotEqualTo(comparable2) + } + + @Test + fun testEquals_subtractionOps_sameOnlyByEvaluation_areNotEqual() { + val comparable1 = parseNumericExpression("1 - 2 - 3").toComparableOperation() + val comparable2 = parseNumericExpression("1 - 2 - 2 - 1").toComparableOperation() + + // While the two expressions are numerically equivalent, they aren't comparable since there are + // extra terms in one (more than trivial rearranging is required to determine that they're + // equal). + assertThat(comparable1).isNotEqualTo(comparable2) + } + + @Test + fun testEquals_divisionOps_same_areEqual() { + val comparable1 = parseNumericExpression("2 / 3").toComparableOperation() + val comparable2 = parseNumericExpression("2 / 3").toComparableOperation() + + assertThat(comparable1).isEqualTo(comparable2) + } + + @Test + fun testEquals_divisionOps_differentByOrder_areNotEqual() { + val comparable1 = parseNumericExpression("2 / 3").toComparableOperation() + val comparable2 = parseNumericExpression("3 / 2").toComparableOperation() + + // Division is not commutative. + assertThat(comparable1).isNotEqualTo(comparable2) + } + + @Test + fun testEquals_divisionOps_differentByAssociativity_areNotEqual() { + val comparable1 = parseNumericExpression("2 / (3 / 4)").toComparableOperation() + val comparable2 = parseNumericExpression("(2 / 3) / 4").toComparableOperation() + + // Division is not associative. + assertThat(comparable1).isNotEqualTo(comparable2) + } + + @Test + fun testEquals_divisionOps_sameOnlyByEvaluation_areNotEqual() { + val comparable1 = parseNumericExpression("2 / 3 / 4").toComparableOperation() + val comparable2 = parseNumericExpression("2 / 3 / 2 / 2").toComparableOperation() + + // While the two expressions are numerically equivalent, they aren't comparable since there are + // extra terms in one (more than trivial rearranging is required to determine that they're + // equal). + assertThat(comparable1).isNotEqualTo(comparable2) + } + + @Test + fun testEquals_exponentiationOps_same_areEqual() { + val comparable1 = parseNumericExpression("2 ^ 3").toComparableOperation() + val comparable2 = parseNumericExpression("2 ^ 3").toComparableOperation() + + assertThat(comparable1).isEqualTo(comparable2) + } + + @Test + fun testEquals_exponentiationOps_differentByOrder_areNotEqual() { + val comparable1 = parseNumericExpression("2 ^ 3").toComparableOperation() + val comparable2 = parseNumericExpression("3 ^ 2").toComparableOperation() + + // Exponentiation is not commutative. + assertThat(comparable1).isNotEqualTo(comparable2) + } + + @Test + fun testEquals_exponentiationOps_differentByAssociativity_areNotEqual() { + // Disable optional errors to allow nested exponentiation. + val comparable1 = + parseNumericExpression( + "2 ^ (3 ^ 4)", errorCheckingMode = REQUIRED_ONLY + ).toComparableOperation() + val comparable2 = + parseNumericExpression( + "(2 ^ 3) ^ 4", errorCheckingMode = REQUIRED_ONLY + ).toComparableOperation() + + // Exponentiation is not associative. + assertThat(comparable1).isNotEqualTo(comparable2) + } + + @Test + fun testEquals_exponentiationOps_differentByValue_areNotEqual() { + val comparable1 = parseNumericExpression("2 ^ 3").toComparableOperation() + val comparable2 = parseNumericExpression("2 ^ 4").toComparableOperation() + + assertThat(comparable1).isNotEqualTo(comparable2) + } + + @Test + fun testEquals_exponentiationOps_sameOnlyByEvaluation_areNotEqual() { + // Disable optional errors to allow nested exponentiation. + val comparable1 = parseNumericExpression("2 ^ 4").toComparableOperation() + val comparable2 = + parseNumericExpression("2 ^ 2 ^ 2", errorCheckingMode = REQUIRED_ONLY).toComparableOperation() + + // While the two expressions are numerically equivalent, they aren't comparable since there are + // extra terms in one (more than trivial rearranging is required to determine that they're + // equal). + assertThat(comparable1).isNotEqualTo(comparable2) + } + + @Test + fun testEquals_squareRootOps_same_areEqual() { + val comparable1 = parseNumericExpression("sqrt(2)").toComparableOperation() + val comparable2 = parseNumericExpression("sqrt(2)").toComparableOperation() + + assertThat(comparable1).isEqualTo(comparable2) + } + + @Test + fun testEquals_squareRootOps_differentByValue_areNotEqual() { + val comparable1 = parseNumericExpression("sqrt(2)").toComparableOperation() + val comparable2 = parseNumericExpression("sqrt(3)").toComparableOperation() + + assertThat(comparable1).isNotEqualTo(comparable2) + } + + @Test + fun testEquals_squareRootOps_sameOnlyByEvaluation_areNotEqual() { + val comparable1 = parseNumericExpression("sqrt(2)").toComparableOperation() + val comparable2 = parseNumericExpression("sqrt(1 + 1)").toComparableOperation() + + // While the two expressions are numerically equivalent, they aren't comparable since there are + // extra terms in one (more than trivial rearranging is required to determine that they're + // equal). + assertThat(comparable1).isNotEqualTo(comparable2) + } + + @Test + fun testEquals_additionsAndSubtractions_differentByOrder_areEqual() { + val comparable1 = parseNumericExpression("1+2-3").toComparableOperation() + val comparable2 = parseNumericExpression("-3+2+1").toComparableOperation() + + assertThat(comparable1).isEqualTo(comparable2) + } + + @Test + fun testEquals_multiplicationsAndDivisions_differentByOrder_areEqual() { + val comparable1 = parseNumericExpression("2*3/4*7").toComparableOperation() + val comparable2 = parseNumericExpression("7*2*3/4").toComparableOperation() + + assertThat(comparable1).isEqualTo(comparable2) + } + + @Test + fun testEquals_allAccumulationOperations_differentByOrder_areEqual() { + val comparable1 = parseNumericExpression("1+2*3/4*7-8+3").toComparableOperation() + val comparable2 = parseNumericExpression("-8+3+7*3/4*2+1").toComparableOperation() + + assertThat(comparable1).isEqualTo(comparable2) + } + + @Test + fun testEquals_allOperations_differentByOrder_areEqual() { + val comparable1 = parseNumericExpression("sqrt(1+2*3)*2^3/7-(2-2*3)").toComparableOperation() + val comparable2 = parseNumericExpression("2^3*sqrt(3*2+1)/7-(-3*2+2)").toComparableOperation() + + assertThat(comparable1).isEqualTo(comparable2) + } + + @Test + fun testEquals_allOperations_oneNestedDifferentByValue_areNotEqual() { + val comparable1 = parseNumericExpression("sqrt(1+2*3)*2^3/7-(2-2*3)").toComparableOperation() + val comparable2 = parseNumericExpression("sqrt(1+2*3)*2^3/7-(2-4*3)").toComparableOperation() + + // Just one different term leads to the entire comparison failing. + assertThat(comparable1).isNotEqualTo(comparable2) + } + + @Test + fun testEquals_twoOps_allOperations_oneMissingTerm_areNotEqual() { + val comparable1 = parseNumericExpression("sqrt(1+2*3)*2^3/7-(2-2*3)").toComparableOperation() + val comparable2 = parseNumericExpression("sqrt(1+2*3)*2^3/7-(2-2)").toComparableOperation() + + // Just one missing term leads to the entire comparison failing. + assertThat(comparable1).isNotEqualTo(comparable2) + } @Test fun test1() { @@ -1900,7 +3098,7 @@ class ExpressionToComparableOperationConverterTest { @Test fun test11() { // TODO: do something with this - val exp = parseNumericExpression("2^3^4", REQUIRED_ONLY) + val exp = parseNumericExpression("2^3^4", errorCheckingMode = REQUIRED_ONLY) assertThat(exp.toComparableOperation()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() @@ -2702,7 +3900,7 @@ class ExpressionToComparableOperationConverterTest { @Test fun test31() { // TODO: do something with this - val exp = parseAlgebraicExpression("x^3^4", REQUIRED_ONLY) + val exp = parseAlgebraicExpression("x^3^4", errorCheckingMode = REQUIRED_ONLY) assertThat(exp.toComparableOperation()).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() hasInvertedPropertyThat().isFalse() @@ -3348,10 +4546,12 @@ class ExpressionToComparableOperationConverterTest { } private fun parseAlgebraicExpression( - expression: String, errorCheckingMode: ErrorCheckingMode = ALL_ERRORS + expression: String, + allowedVariables: List = listOf("x", "y", "z"), + errorCheckingMode: ErrorCheckingMode = ALL_ERRORS ): MathExpression { return MathExpressionParser.parseAlgebraicExpression( - expression, allowedVariables = listOf("x", "y", "z"), errorCheckingMode + expression, allowedVariables, errorCheckingMode ).retrieveExpectedSuccessfulResult() } From e6c09d85c095cbe0f44611f25787f5051b8556b8 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 28 Jan 2022 18:57:17 -0800 Subject: [PATCH 085/162] Simplify operation sorting comparators. --- .../org/oppia/android/util/math/BUILD.bazel | 3 + .../android/util/math/ComparatorExtensions.kt | 71 ++++------- ...xpressionToComparableOperationConverter.kt | 120 ++++++++---------- 3 files changed, 82 insertions(+), 112 deletions(-) diff --git a/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel b/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel index d74e1683647..f203e85521d 100644 --- a/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel @@ -91,6 +91,9 @@ kt_android_library( srcs = [ "ComparatorExtensions.kt", ], + deps = [ + "//third_party:com_google_protobuf_protobuf-javalite", + ], ) kt_android_library( diff --git a/utility/src/main/java/org/oppia/android/util/math/ComparatorExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/ComparatorExtensions.kt index 284ec69fe69..46ab552cfa2 100644 --- a/utility/src/main/java/org/oppia/android/util/math/ComparatorExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/ComparatorExtensions.kt @@ -1,57 +1,32 @@ package org.oppia.android.util.math -import java.util.SortedSet +import com.google.protobuf.MessageLite -fun comparingDeferred( - keySelector: (T) -> U, - comparatorSelector: () -> Comparator -): Comparator { - // Store as captured val for memoization. - val comparator by lazy { comparatorSelector() } - return Comparator.comparing(keySelector) { o1, o2 -> - comparator.compare(o1, o2) +fun Comparator.compareIterables(first: Iterable, second: Iterable): Int { + // Reference: https://stackoverflow.com/a/30107086. + val firstIter = first.sortedWith(this).iterator() + val secondIter = second.sortedWith(this).iterator() + while (firstIter.hasNext() && secondIter.hasNext()) { + val comparison = this.compare(firstIter.next(), secondIter.next()) + if (comparison != 0) return comparison // Found a different item. } -} - -fun > Comparator.thenComparingReversed( - keySelector: (T) -> U -): Comparator = thenComparing(Comparator.comparing(keySelector).reversed()) -fun > Comparator.thenSelectAmong( - enumSelector: (T) -> E, - vararg comparators: Pair> -): Comparator { - val comparatorMap = comparators.toMap() - return thenComparing( - Comparator { o1, o2 -> - val enum1 = enumSelector(o1) - val enum2 = enumSelector(o2) - check(enum1 == enum2) { - "Expected objects to have the same enum values: $o1 ($enum1), $o2 ($enum2)" - } - val comparator = - checkNotNull(comparatorMap[enum1]) { "No comparator for matched enum: $enum1" } - return@Comparator comparator.compare(o1, o2) - } - ) + // Everything is equal up to here, see if the lists are different length. + return when { + firstIter.hasNext() -> 1 // The first list is longer, therefore "greater." + secondIter.hasNext() -> -1 // Ditto, but for the second list. + else -> 0 // Otherwise, they're the same length with all equal items (and are thus equal). + } } -fun Comparator.toSetComparator(): Comparator> { - val itemComparator = this - return Comparator { first, second -> - // Reference: https://stackoverflow.com/a/30107086. - val firstIter = first.iterator() - val secondIter = second.iterator() - while (firstIter.hasNext() && secondIter.hasNext()) { - val comparison = itemComparator.compare(firstIter.next(), secondIter.next()) - if (comparison != 0) return@Comparator comparison // Found a different item. - } - - // Everything is equal up to here, see if the lists are different length. - return@Comparator when { - firstIter.hasNext() -> 1 // The first list is longer, therefore "greater." - secondIter.hasNext() -> -1 // Ditto, but for the second list. - else -> 0 // Otherwise, they're the same length with all equal items (and are thus equal). - } +fun Comparator.compareProtos(left: T, right: T): Int { + val defaultValue = left.defaultInstanceForType + val leftIsDefault = left == defaultValue + val rightIsDefault = right == defaultValue + return when { + leftIsDefault && rightIsDefault -> 0 // Both are default, therefore equal. + leftIsDefault -> 1 // right > left since it's initialized. + rightIsDefault -> 1 // left > right since it's initialized. + else -> compare(left, right) // Both are initialized; perform a deep-comparison. } } diff --git a/utility/src/main/java/org/oppia/android/util/math/ExpressionToComparableOperationConverter.kt b/utility/src/main/java/org/oppia/android/util/math/ExpressionToComparableOperationConverter.kt index 5e9cc00b21f..4f1bc904aba 100644 --- a/utility/src/main/java/org/oppia/android/util/math/ExpressionToComparableOperationConverter.kt +++ b/utility/src/main/java/org/oppia/android/util/math/ExpressionToComparableOperationConverter.kt @@ -4,11 +4,8 @@ import org.oppia.android.app.model.ComparableOperation import org.oppia.android.app.model.ComparableOperation.CommutativeAccumulation import org.oppia.android.app.model.ComparableOperation.CommutativeAccumulation.AccumulationType.PRODUCT import org.oppia.android.app.model.ComparableOperation.CommutativeAccumulation.AccumulationType.SUMMATION -import org.oppia.android.app.model.ComparableOperation.ComparisonTypeCase.CONSTANT_TERM -import org.oppia.android.app.model.ComparableOperation.ComparisonTypeCase.NON_COMMUTATIVE_OPERATION -import org.oppia.android.app.model.ComparableOperation.ComparisonTypeCase.VARIABLE_TERM import org.oppia.android.app.model.ComparableOperation.NonCommutativeOperation -import org.oppia.android.app.model.ComparableOperation.NonCommutativeOperation.OperationTypeCase +import org.oppia.android.app.model.ComparableOperation.NonCommutativeOperation.BinaryOperation import org.oppia.android.app.model.MathBinaryOperation.Operator as BinaryOperator import org.oppia.android.app.model.MathBinaryOperation.Operator.ADD import org.oppia.android.app.model.MathBinaryOperation.Operator.DIVIDE @@ -28,65 +25,10 @@ import org.oppia.android.app.model.MathFunctionCall.FunctionType.SQUARE_ROOT import org.oppia.android.app.model.MathUnaryOperation.Operator as UnaryOperator import org.oppia.android.app.model.MathUnaryOperation.Operator.NEGATE import org.oppia.android.app.model.MathUnaryOperation.Operator.POSITIVE -import org.oppia.android.util.math.ExpressionToComparableOperationConverter.Companion.invertNegation -import org.oppia.android.util.math.ExpressionToComparableOperationConverter.Companion.toComparableOperation class ExpressionToComparableOperationConverter private constructor() { companion object { - // TODO: consider eliminating the comparator extensions. Probably should verify full test suite - // & the old tests before deleting the old tests. - - private val COMPARABLE_OPERATION_COMPARATOR: Comparator by lazy { - // Some of the comparators must be deferred since they indirectly reference this comparator - // (which isn't valid until it's fully assembled). - Comparator.comparing(ComparableOperation::getComparisonTypeCase) - .thenComparing(ComparableOperation::getIsNegated) - .thenComparing(ComparableOperation::getIsInverted) - .thenSelectAmong( - ComparableOperation::getComparisonTypeCase, - ComparableOperation.ComparisonTypeCase.COMMUTATIVE_ACCUMULATION to comparingDeferred( - ComparableOperation::getCommutativeAccumulation - ) { COMMUTATIVE_ACCUMULATION_COMPARATOR }, - NON_COMMUTATIVE_OPERATION to comparingDeferred( - ComparableOperation::getNonCommutativeOperation - ) { NON_COMMUTATIVE_OPERATION_COMPARATOR }, - CONSTANT_TERM to Comparator.comparing( - ComparableOperation::getConstantTerm, REAL_COMPARATOR - ), - VARIABLE_TERM to Comparator.comparing(ComparableOperation::getVariableTerm) - ) - } - - private val COMMUTATIVE_ACCUMULATION_COMPARATOR: Comparator by lazy { - Comparator.comparing(CommutativeAccumulation::getAccumulationType) - .thenComparing( - { accumulation -> - accumulation.combinedOperationsList.toSortedSet(COMPARABLE_OPERATION_COMPARATOR) - }, - COMPARABLE_OPERATION_COMPARATOR.toSetComparator() - ) - } - - private val NON_COMMUTATIVE_BINARY_OPERATION_COMPARATOR by lazy { - Comparator.comparing( - NonCommutativeOperation.BinaryOperation::getLeftOperand, COMPARABLE_OPERATION_COMPARATOR - ).thenComparing( - NonCommutativeOperation.BinaryOperation::getRightOperand, COMPARABLE_OPERATION_COMPARATOR - ) - } - - private val NON_COMMUTATIVE_OPERATION_COMPARATOR: Comparator by lazy { - Comparator.comparing(NonCommutativeOperation::getOperationTypeCase) - .thenSelectAmong( - NonCommutativeOperation::getOperationTypeCase, - OperationTypeCase.EXPONENTIATION to Comparator.comparing( - NonCommutativeOperation::getExponentiation, NON_COMMUTATIVE_BINARY_OPERATION_COMPARATOR - ), - OperationTypeCase.SQUARE_ROOT to Comparator.comparing( - NonCommutativeOperation::getSquareRoot, COMPARABLE_OPERATION_COMPARATOR - ), - ) - } + private val COMPARABLE_OPERATION_COMPARATOR by lazy { createComparableOperationComparator() } fun MathExpression.toComparableOperation(): ComparableOperation { return when (expressionTypeCase) { @@ -244,20 +186,20 @@ class ExpressionToComparableOperationConverter private constructor() { // Replace the list operations with a sorted list of operations. Note that the inner elements // are already sorted since this is called during operation creation time (so nested // operations would have already been sorted). - val operationsList = combinedOperationsList.toMutableList() + val operationsList = combinedOperationsList.sortedWith(COMPARABLE_OPERATION_COMPARATOR) clearCombinedOperations() - addAllCombinedOperations(operationsList.sortedWith(COMPARABLE_OPERATION_COMPARATOR)) + addAllCombinedOperations(operationsList) } private fun MathExpression.toNonCommutativeOperation( setOperation: NonCommutativeOperation.Builder.( - NonCommutativeOperation.BinaryOperation + BinaryOperation ) -> NonCommutativeOperation.Builder ): ComparableOperation { return ComparableOperation.newBuilder().apply { nonCommutativeOperation = NonCommutativeOperation.newBuilder().apply { setOperation( - NonCommutativeOperation.BinaryOperation.newBuilder().apply { + BinaryOperation.newBuilder().apply { leftOperand = binaryOperation.leftOperand.toComparableOperation() rightOperand = binaryOperation.rightOperand.toComparableOperation() }.build() @@ -274,5 +216,55 @@ class ExpressionToComparableOperationConverter private constructor() { private fun ComparableOperation.invertInverted(): ComparableOperation = toBuilder().apply { isInverted = !isInverted }.build() + + private fun createComparableOperationComparator(): Comparator { + // Note that this & constituent comparators is designed to also verify undefined fields (such + // as all the possibilities of a oneof versus just one) for simpler syntax. Computationally, + // it shouldn't make a large difference since default protos are generally cached for proto + // lite, and compareProtos short-circuits for default protos. Further, a comparator is created + // for each staged of the execution since, unfortunately, there's no easy way to circularly + // reference cached fields. + return compareBy(ComparableOperation::getComparisonTypeCase) + .thenBy(ComparableOperation::getIsNegated) + .thenBy(ComparableOperation::getIsInverted) + .thenComparator { a, b -> + createCommutativeAccumulationComparator() + .compareProtos(a.commutativeAccumulation, b.commutativeAccumulation) + }.thenComparator { a, b -> + createNonCommutativeOperationComparator() + .compareProtos(a.nonCommutativeOperation, b.nonCommutativeOperation) + }.thenComparator { a, b -> + REAL_COMPARATOR.compareProtos(a.constantTerm, b.constantTerm) + } + .thenBy(ComparableOperation::getVariableTerm) + } + + private fun createCommutativeAccumulationComparator(): Comparator { + return compareBy(CommutativeAccumulation::getAccumulationType) + .thenComparator { a, b -> + createComparableOperationComparator().compareIterables( + a.combinedOperationsList, b.combinedOperationsList + ) + } + } + + private fun createNonCommutativeOperationComparator(): Comparator { + return compareBy(NonCommutativeOperation::getOperationTypeCase) + .thenComparator { a, b -> + createBinaryOperationComparator().compareProtos(a.exponentiation, b.exponentiation) + }.thenComparator { a, b -> + createComparableOperationComparator().compareProtos(a.squareRoot, b.squareRoot) + } + } + + private fun createBinaryOperationComparator(): Comparator { + // Start with a trivial comparator to start the chain for nicer syntax. + return compareBy(BinaryOperation::hasLeftOperand) + .thenComparator { a, b -> + createComparableOperationComparator().compareProtos(a.leftOperand, b.leftOperand) + }.thenComparator { a, b -> + createComparableOperationComparator().compareProtos(a.rightOperand, b.rightOperand) + } + } } } From bebc100bcc7f01d29bcb15944e7b77d267e8bfe8 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 28 Jan 2022 18:58:14 -0800 Subject: [PATCH 086/162] Remove old tests. --- ...ssionToComparableOperationConverterTest.kt | 1802 ----------------- 1 file changed, 1802 deletions(-) diff --git a/utility/src/test/java/org/oppia/android/util/math/ExpressionToComparableOperationConverterTest.kt b/utility/src/test/java/org/oppia/android/util/math/ExpressionToComparableOperationConverterTest.kt index 5d4dc8495fe..925ac470db8 100644 --- a/utility/src/test/java/org/oppia/android/util/math/ExpressionToComparableOperationConverterTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/ExpressionToComparableOperationConverterTest.kt @@ -2734,1808 +2734,6 @@ class ExpressionToComparableOperationConverterTest { assertThat(comparable1).isNotEqualTo(comparable2) } - @Test - fun test1() { - // TODO: do something with this - val exp = parseNumericExpression("1") - assertThat(exp.toComparableOperation()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - } - - @Test - fun test2() { - // TODO: do something with this - val exp = parseNumericExpression("-1") - assertThat(exp.toComparableOperation()).hasStructureThatMatches { - hasNegatedPropertyThat().isTrue() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - } - - @Test - fun test3() { - // TODO: do something with this - val exp = parseNumericExpression("1+3+4") - assertThat(exp.toComparableOperation()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(SUMMATION) { - hasOperandCountThat().isEqualTo(3) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - } - - @Test - fun test4() { - // TODO: do something with this - val exp = parseNumericExpression("-1-2-3") - assertThat(exp.toComparableOperation()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(SUMMATION) { - hasOperandCountThat().isEqualTo(3) - index(0) { - hasNegatedPropertyThat().isTrue() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - index(1) { - hasNegatedPropertyThat().isTrue() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - index(2) { - hasNegatedPropertyThat().isTrue() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - } - - @Test - fun test5() { - // TODO: do something with this - val exp = parseNumericExpression("1+2-3") - assertThat(exp.toComparableOperation()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(SUMMATION) { - hasOperandCountThat().isEqualTo(3) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - index(2) { - hasNegatedPropertyThat().isTrue() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - } - - @Test - fun test6() { - // TODO: do something with this - val exp = parseNumericExpression("2*3*4") - assertThat(exp.toComparableOperation()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(3) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - } - - @Test - fun test7() { - // TODO: do something with this - val exp = parseNumericExpression("1-2*3") - assertThat(exp.toComparableOperation()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(SUMMATION) { - hasOperandCountThat().isEqualTo(2) - index(0) { - hasNegatedPropertyThat().isTrue() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(2) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - } - } - } - - @Test - fun test8() { - // TODO: do something with this - val exp = parseNumericExpression("2*3-4") - assertThat(exp.toComparableOperation()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(SUMMATION) { - hasOperandCountThat().isEqualTo(2) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(2) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - index(1) { - hasNegatedPropertyThat().isTrue() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - } - - @Test - fun test9() { - // TODO: do something with this - val exp = parseNumericExpression("1+2*3-4+8*7*6-9") - assertThat(exp.toComparableOperation()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(SUMMATION) { - hasOperandCountThat().isEqualTo(5) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(2) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(3) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(6) - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(7) - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(8) - } - } - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - index(3) { - hasNegatedPropertyThat().isTrue() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - index(4) { - hasNegatedPropertyThat().isTrue() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(9) - } - } - } - } - } - - @Test - fun test10() { - // TODO: do something with this - val exp = parseNumericExpression("2/3/4") - assertThat(exp.toComparableOperation()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(3) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isTrue() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isTrue() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - } - - @Test - fun test11() { - // TODO: do something with this - val exp = parseNumericExpression("2^3^4", errorCheckingMode = REQUIRED_ONLY) - assertThat(exp.toComparableOperation()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - nonCommutativeOperation { - exponentiation { - leftOperand { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - nonCommutativeOperation { - exponentiation { - leftOperand { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - } - } - } - } - } - - @Test - fun test12() { - // TODO: do something with this - val exp = parseNumericExpression("1+2/3+3") - assertThat(exp.toComparableOperation()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(SUMMATION) { - hasOperandCountThat().isEqualTo(3) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(2) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isTrue() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - } - - @Test - fun test13() { - // TODO: do something with this - val exp = parseNumericExpression("1+(2/3)+3") - assertThat(exp.toComparableOperation()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(SUMMATION) { - hasOperandCountThat().isEqualTo(3) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(2) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isTrue() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - } - - @Test - fun test14() { - // TODO: do something with this - val exp = parseNumericExpression("1+2^3+3") - assertThat(exp.toComparableOperation()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(SUMMATION) { - hasOperandCountThat().isEqualTo(3) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - nonCommutativeOperation { - exponentiation { - leftOperand { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - } - - @Test - fun test15() { - // TODO: do something with this - val exp = parseNumericExpression("1+(2^3)+3") - assertThat(exp.toComparableOperation()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(SUMMATION) { - hasOperandCountThat().isEqualTo(3) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - nonCommutativeOperation { - exponentiation { - leftOperand { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - rightOperand { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - } - - @Test - fun test16() { - // TODO: do something with this - val exp = parseNumericExpression("2*3/4*7") - assertThat(exp.toComparableOperation()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(4) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(7) - } - } - index(3) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isTrue() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - } - - @Test - fun test17() { - // TODO: do something with this - val exp = parseNumericExpression("2*(3/4)*7") - assertThat(exp.toComparableOperation()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(4) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(7) - } - } - index(3) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isTrue() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - } - - @Test - fun test18() { - // TODO: do something with this - val exp = parseNumericExpression("-3*sqrt(2)") - assertThat(exp.toComparableOperation()).hasStructureThatMatches { - hasNegatedPropertyThat().isTrue() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(2) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - nonCommutativeOperation { - squareRootWithArgument { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - } - - @Test - fun test19() { - // TODO: do something with this - val exp = parseNumericExpression("1+(2+(3+(4+5)))") - assertThat(exp.toComparableOperation()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(SUMMATION) { - hasOperandCountThat().isEqualTo(5) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - index(3) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - index(4) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(5) - } - } - } - } - } - - @Test - fun test20() { - // TODO: do something with this - val exp = parseNumericExpression("2*(3*(4*(5*6)))") - assertThat(exp.toComparableOperation()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(5) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - index(3) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(5) - } - } - index(4) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(6) - } - } - } - } - } - - @Test - fun test21() { - // TODO: do something with this - val exp = parseAlgebraicExpression("x") - assertThat(exp.toComparableOperation()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("x") - } - } - } - - @Test - fun test22() { - // TODO: do something with this - val exp = parseAlgebraicExpression("-x") - assertThat(exp.toComparableOperation()).hasStructureThatMatches { - hasNegatedPropertyThat().isTrue() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("x") - } - } - } - - @Test - fun test23() { - // TODO: do something with this - val exp = parseAlgebraicExpression("1+x+y") - assertThat(exp.toComparableOperation()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(SUMMATION) { - hasOperandCountThat().isEqualTo(3) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("x") - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("y") - } - } - } - } - } - - @Test - fun test24() { - // TODO: do something with this - val exp = parseAlgebraicExpression("-1-x-y") - assertThat(exp.toComparableOperation()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(SUMMATION) { - hasOperandCountThat().isEqualTo(3) - index(0) { - hasNegatedPropertyThat().isTrue() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - index(1) { - hasNegatedPropertyThat().isTrue() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("x") - } - } - index(2) { - hasNegatedPropertyThat().isTrue() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("y") - } - } - } - } - } - - @Test - fun test25() { - // TODO: do something with this - val exp = parseAlgebraicExpression("1+x-y") - assertThat(exp.toComparableOperation()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(SUMMATION) { - hasOperandCountThat().isEqualTo(3) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("x") - } - } - index(2) { - hasNegatedPropertyThat().isTrue() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("y") - } - } - } - } - } - - @Test - fun test26() { - // TODO: do something with this - val exp = parseAlgebraicExpression("2xy") - assertThat(exp.toComparableOperation()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(3) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("x") - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("y") - } - } - } - } - } - - @Test - fun test27() { - // TODO: do something with this - val exp = parseAlgebraicExpression("1-xy") - assertThat(exp.toComparableOperation()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(SUMMATION) { - hasOperandCountThat().isEqualTo(2) - index(0) { - hasNegatedPropertyThat().isTrue() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(2) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("x") - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("y") - } - } - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - } - } - } - - @Test - fun test28() { - // TODO: do something with this - val exp = parseAlgebraicExpression("xy-4") - assertThat(exp.toComparableOperation()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(SUMMATION) { - hasOperandCountThat().isEqualTo(2) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(2) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("x") - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("y") - } - } - } - } - index(1) { - hasNegatedPropertyThat().isTrue() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - } - - @Test - fun test29() { - // TODO: do something with this - val exp = parseAlgebraicExpression("1+xy-4+yz-9") - assertThat(exp.toComparableOperation()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(SUMMATION) { - hasOperandCountThat().isEqualTo(5) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(2) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("x") - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("y") - } - } - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(2) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("y") - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("z") - } - } - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - index(3) { - hasNegatedPropertyThat().isTrue() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - index(4) { - hasNegatedPropertyThat().isTrue() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(9) - } - } - } - } - } - - @Test - fun test30() { - // TODO: do something with this - val exp = parseAlgebraicExpression("2/x/y") - assertThat(exp.toComparableOperation()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(3) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isTrue() - variableTerm { - withNameThat().isEqualTo("x") - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isTrue() - variableTerm { - withNameThat().isEqualTo("y") - } - } - } - } - } - - @Test - fun test31() { - // TODO: do something with this - val exp = parseAlgebraicExpression("x^3^4", errorCheckingMode = REQUIRED_ONLY) - assertThat(exp.toComparableOperation()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - nonCommutativeOperation { - exponentiation { - leftOperand { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("x") - } - } - rightOperand { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - nonCommutativeOperation { - exponentiation { - leftOperand { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - rightOperand { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - } - } - } - } - } - } - } - - @Test - fun test32() { - // TODO: do something with this - val exp = parseAlgebraicExpression("1+x/y+z") - assertThat(exp.toComparableOperation()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(SUMMATION) { - hasOperandCountThat().isEqualTo(3) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(2) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("x") - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isTrue() - variableTerm { - withNameThat().isEqualTo("y") - } - } - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("z") - } - } - } - } - } - - @Test - fun test33() { - // TODO: do something with this - val exp = parseAlgebraicExpression("1+(x/y)+z") - assertThat(exp.toComparableOperation()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(SUMMATION) { - hasOperandCountThat().isEqualTo(3) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(2) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("x") - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isTrue() - variableTerm { - withNameThat().isEqualTo("y") - } - } - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("z") - } - } - } - } - } - - @Test - fun test34() { - // TODO: do something with this - val exp = parseAlgebraicExpression("1+x^3+y") - assertThat(exp.toComparableOperation()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(SUMMATION) { - hasOperandCountThat().isEqualTo(3) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - nonCommutativeOperation { - exponentiation { - leftOperand { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("x") - } - } - rightOperand { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("y") - } - } - } - } - } - - @Test - fun test35() { - // TODO: do something with this - val exp = parseAlgebraicExpression("1+(x^3)+y") - assertThat(exp.toComparableOperation()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(SUMMATION) { - hasOperandCountThat().isEqualTo(3) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - nonCommutativeOperation { - exponentiation { - leftOperand { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("x") - } - } - rightOperand { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - } - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("y") - } - } - } - } - } - - @Test - fun test36() { - // TODO: do something with this - val exp = parseAlgebraicExpression("2*x/y*z") - assertThat(exp.toComparableOperation()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(4) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("x") - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("z") - } - } - index(3) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isTrue() - variableTerm { - withNameThat().isEqualTo("y") - } - } - } - } - } - - @Test - fun test37() { - // TODO: do something with this - val exp = parseAlgebraicExpression("2*(x/y)*z") - assertThat(exp.toComparableOperation()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(4) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("x") - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("z") - } - } - index(3) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isTrue() - variableTerm { - withNameThat().isEqualTo("y") - } - } - } - } - } - - @Test - fun test38() { - // TODO: do something with this - val exp = parseAlgebraicExpression("-2*sqrt(x)") - assertThat(exp.toComparableOperation()).hasStructureThatMatches { - hasNegatedPropertyThat().isTrue() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(2) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - nonCommutativeOperation { - squareRootWithArgument { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("x") - } - } - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - } - } - } - - @Test - fun test39() { - // TODO: do something with this - val exp = parseAlgebraicExpression("1+(x+(3+(z+y)))") - assertThat(exp.toComparableOperation()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(SUMMATION) { - hasOperandCountThat().isEqualTo(5) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(1) - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(3) - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("x") - } - } - index(3) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("y") - } - } - index(4) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("z") - } - } - } - } - } - - @Test - fun test40() { - // TODO: do something with this - val exp = parseAlgebraicExpression("2*(x*(4*(zy)))") - assertThat(exp.toComparableOperation()).hasStructureThatMatches { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - commutativeAccumulationWithType(PRODUCT) { - hasOperandCountThat().isEqualTo(5) - index(0) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(2) - } - } - index(1) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - constantTerm { - withValueThat().isIntegerThat().isEqualTo(4) - } - } - index(2) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("x") - } - } - index(3) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("y") - } - } - index(4) { - hasNegatedPropertyThat().isFalse() - hasInvertedPropertyThat().isFalse() - variableTerm { - withNameThat().isEqualTo("z") - } - } - } - } - } - - // TODO: Equality tests: - @Test - fun test41() { - // TODO: do something with this - val firstList = createComparableOperationListFromNumericExpression("(1+2)+3") - val secondList = createComparableOperationListFromNumericExpression("1+(2+3)") - assertThat(firstList).isEqualTo(secondList) - } - - @Test - fun test42() { - // TODO: do something with this - val firstList = createComparableOperationListFromNumericExpression("1+2+3") - val secondList = createComparableOperationListFromNumericExpression("3+2+1") - assertThat(firstList).isEqualTo(secondList) - } - - @Test - fun test43() { - // TODO: do something with this - val firstList = createComparableOperationListFromNumericExpression("1-2-3") - val secondList = createComparableOperationListFromNumericExpression("-3 + -2 + 1") - assertThat(firstList).isEqualTo(secondList) - } - - @Test - fun test44() { - // TODO: do something with this - val firstList = createComparableOperationListFromNumericExpression("1-2-3") - val secondList = createComparableOperationListFromNumericExpression("-3-2+1") - assertThat(firstList).isEqualTo(secondList) - } - - @Test - fun test45() { - // TODO: do something with this - val firstList = createComparableOperationListFromNumericExpression("1-2-3") - val secondList = createComparableOperationListFromNumericExpression("-3-2+1") - assertThat(firstList).isEqualTo(secondList) - } - - @Test - fun test46() { - // TODO: do something with this - val firstList = createComparableOperationListFromNumericExpression("1-2-3") - val secondList = createComparableOperationListFromNumericExpression("3-2-1") - assertThat(firstList).isNotEqualTo(secondList) - } - - @Test - fun test47() { - // TODO: do something with this - val firstList = createComparableOperationListFromNumericExpression("2*3*4") - val secondList = createComparableOperationListFromNumericExpression("4*3*2") - assertThat(firstList).isEqualTo(secondList) - } - - @Test - fun test48() { - // TODO: do something with this - val firstList = createComparableOperationListFromNumericExpression("2*(3/4)") - val secondList = createComparableOperationListFromNumericExpression("3/4*2") - assertThat(firstList).isEqualTo(secondList) - } - - @Test - fun test49() { - // TODO: do something with this - val firstList = createComparableOperationListFromNumericExpression("2*3/4") - val secondList = createComparableOperationListFromNumericExpression("3/4*2") - assertThat(firstList).isEqualTo(secondList) - } - - @Test - fun test50() { - // TODO: do something with this - val firstList = createComparableOperationListFromNumericExpression("2*3/4") - val secondList = createComparableOperationListFromNumericExpression("2*3*4") - assertThat(firstList).isNotEqualTo(secondList) - } - - @Test - fun test51() { - // TODO: do something with this - val firstList = createComparableOperationListFromNumericExpression("2*3/4") - val secondList = createComparableOperationListFromNumericExpression("2*4/3") - assertThat(firstList).isNotEqualTo(secondList) - } - - @Test - fun test52() { - // TODO: do something with this - val firstList = createComparableOperationListFromNumericExpression("2*3/4*7") - val secondList = createComparableOperationListFromNumericExpression("3/4*7*2") - assertThat(firstList).isEqualTo(secondList) - } - - @Test - fun test53() { - // TODO: do something with this - val firstList = createComparableOperationListFromNumericExpression("2*3/4*7") - val secondList = createComparableOperationListFromNumericExpression("7*(3*2/4)") - assertThat(firstList).isEqualTo(secondList) - } - - @Test - fun test54() { - // TODO: do something with this - val firstList = createComparableOperationListFromNumericExpression("2*3/4*7") - val secondList = createComparableOperationListFromNumericExpression("7*3*2/4") - assertThat(firstList).isEqualTo(secondList) - } - - @Test - fun test55() { - // TODO: do something with this - val firstList = createComparableOperationListFromNumericExpression("-2*3") - val secondList = createComparableOperationListFromNumericExpression("3*-2") - assertThat(firstList).isEqualTo(secondList) - } - - @Test - fun test56() { - // TODO: do something with this - val firstList = createComparableOperationListFromNumericExpression("2^3") - val secondList = createComparableOperationListFromNumericExpression("3^2") - assertThat(firstList).isNotEqualTo(secondList) - } - - @Test - fun test57() { - // TODO: do something with this - val firstList = createComparableOperationListFromNumericExpression("-(1+2)") - val secondList = createComparableOperationListFromNumericExpression("-1+2") - assertThat(firstList).isNotEqualTo(secondList) - } - - @Test - fun test58() { - // TODO: do something with this - val firstList = createComparableOperationListFromNumericExpression("-(1+2)") - val secondList = createComparableOperationListFromNumericExpression("-1-2") - assertThat(firstList).isNotEqualTo(secondList) - } - - @Test - fun test59() { - // TODO: do something with this - val firstList = createComparableOperationListFromAlgebraicExpression("x(x+1)") - val secondList = createComparableOperationListFromAlgebraicExpression("(1+x)x") - assertThat(firstList).isEqualTo(secondList) - } - - @Test - fun test60() { - // TODO: do something with this - val firstList = createComparableOperationListFromAlgebraicExpression("x(x+1)") - val secondList = createComparableOperationListFromAlgebraicExpression("x^2+x") - assertThat(firstList).isNotEqualTo(secondList) - } - - @Test - fun test61() { - // TODO: do something with this - val firstList = createComparableOperationListFromAlgebraicExpression("x^2*sqrt(x)") - val secondList = createComparableOperationListFromAlgebraicExpression("x") - assertThat(firstList).isNotEqualTo(secondList) - } - - @Test - fun test62() { - // TODO: do something with this - val firstList = createComparableOperationListFromAlgebraicExpression("xyz") - val secondList = createComparableOperationListFromAlgebraicExpression("zyx") - assertThat(firstList).isEqualTo(secondList) - } - - @Test - fun test63() { - // TODO: do something with this - val firstList = createComparableOperationListFromAlgebraicExpression("1+xy-2") - val secondList = createComparableOperationListFromAlgebraicExpression("-2+1+yx") - assertThat(firstList).isEqualTo(secondList) - } - - private fun createComparableOperationListFromNumericExpression(expression: String) = - parseNumericExpression(expression).toComparableOperation() - - private fun createComparableOperationListFromAlgebraicExpression(expression: String) = - parseAlgebraicExpression(expression).toComparableOperation() - private companion object { private fun parseNumericExpression( expression: String, errorCheckingMode: ErrorCheckingMode = ALL_ERRORS From 3cecc6cc35efc5d2e7c88c3cf4d497eeb16ad8cb Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 28 Jan 2022 20:31:39 -0800 Subject: [PATCH 087/162] Add remaining missing tests. --- .../android/util/math/ComparatorExtensions.kt | 2 +- .../util/math/MathExpressionExtensions.kt | 2 +- .../oppia/android/util/math/RealExtensions.kt | 1 - .../org/oppia/android/util/math/BUILD.bazel | 1 + .../util/math/ComparatorExtensionsTest.kt | 269 +++++++++++++++++- .../util/math/MathExpressionExtensionsTest.kt | 25 +- .../android/util/math/RealExtensionsTest.kt | 207 +++++++++++++- 7 files changed, 499 insertions(+), 8 deletions(-) diff --git a/utility/src/main/java/org/oppia/android/util/math/ComparatorExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/ComparatorExtensions.kt index 46ab552cfa2..99b66e11243 100644 --- a/utility/src/main/java/org/oppia/android/util/math/ComparatorExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/ComparatorExtensions.kt @@ -25,7 +25,7 @@ fun Comparator.compareProtos(left: T, right: T): Int { val rightIsDefault = right == defaultValue return when { leftIsDefault && rightIsDefault -> 0 // Both are default, therefore equal. - leftIsDefault -> 1 // right > left since it's initialized. + leftIsDefault -> -1 // right > left since it's initialized. rightIsDefault -> 1 // left > right since it's initialized. else -> compare(left, right) // Both are initialized; perform a deep-comparison. } diff --git a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt index 07b3d949fee..109490e5abe 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt @@ -31,4 +31,4 @@ fun MathEquation.toRawLatex(divAsFraction: Boolean): String = convertToLatex(div */ fun MathExpression.evaluateAsNumericExpression(): Real? = evaluate() -fun MathExpression.toComparableOperation(): ComparableOperation = toComparableOperation() +fun MathExpression.toComparable(): ComparableOperation = toComparableOperation() diff --git a/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt index ef0e21a6bcd..0006df1e5d4 100644 --- a/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt @@ -9,7 +9,6 @@ import org.oppia.android.app.model.Real.RealTypeCase.REALTYPE_NOT_SET import kotlin.math.absoluteValue import kotlin.math.pow -// TODO: add tests. val REAL_COMPARATOR: Comparator by lazy { Comparator.comparing(Real::toDouble) } /** diff --git a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel index a6fe0270cea..8a1f784aef3 100644 --- a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel @@ -49,6 +49,7 @@ oppia_android_test( test_class = "org.oppia.android.util.math.ComparatorExtensionsTest", test_manifest = "//utility:test_manifest", deps = [ + "//model/src/main/proto:test_models", "//third_party:androidx_test_ext_junit", "//third_party:com_google_truth_truth", "//third_party:junit_junit", diff --git a/utility/src/test/java/org/oppia/android/util/math/ComparatorExtensionsTest.kt b/utility/src/test/java/org/oppia/android/util/math/ComparatorExtensionsTest.kt index bb27e759bb2..91c8f352e4a 100644 --- a/utility/src/test/java/org/oppia/android/util/math/ComparatorExtensionsTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/ComparatorExtensionsTest.kt @@ -1,8 +1,10 @@ package org.oppia.android.util.math import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat import org.junit.Test import org.junit.runner.RunWith +import org.oppia.android.app.model.TestMessage import org.robolectric.annotation.LooperMode /** Tests for [Comparator] extensions. */ @@ -11,10 +13,271 @@ import org.robolectric.annotation.LooperMode @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) class ComparatorExtensionsTest { - // TODO: finish tests + private companion object { + private val TEST_MESSAGE_0 = TestMessage.newBuilder().apply { intValue = 0 }.build() + private val TEST_MESSAGE_1 = TestMessage.newBuilder().apply { intValue = 1 }.build() + private val TEST_MESSAGE_2 = TestMessage.newBuilder().apply { intValue = 2 }.build() + } + + private val stringComparator: Comparator by lazy { + Comparator { o1, o2 -> o1.compareTo(o2) } + } + private val protoComparator: Comparator by lazy { + compareBy(TestMessage::getIntValue) + } + + @Test + fun testCompareIterables_emptyList_emptyList_returnsZero() { + val leftList = listOf() + val rightList = listOf() + + val compareResult = stringComparator.compareIterables(leftList, rightList) + + assertThat(compareResult).isEqualTo(0) + } + + @Test + fun testCompareIterables_singletonList_emptyList_returnsOne() { + val leftList = listOf("1") + val rightList = listOf() + + val compareResult = stringComparator.compareIterables(leftList, rightList) + + assertThat(compareResult).isEqualTo(1) + } @Test - fun test() { - throw Exception() + fun testCompareIterables_emptyList_singletonList_returnsNegativeOne() { + val leftList = listOf() + val rightList = listOf("1") + + val compareResult = stringComparator.compareIterables(leftList, rightList) + + assertThat(compareResult).isEqualTo(-1) + } + + @Test + fun testCompareIterables_singletonList_singletonList_sameElems_returnsZero() { + val leftList = listOf("1") + val rightList = listOf("1") + + val compareResult = stringComparator.compareIterables(leftList, rightList) + + assertThat(compareResult).isEqualTo(0) + } + + @Test + fun testCompareIterables_twoItemList_singletonList_commonElem_returnsOne() { + val leftList = listOf("1", "2") + val rightList = listOf("1") + + val compareResult = stringComparator.compareIterables(leftList, rightList) + + // The first list is larger, therefore "greater". + assertThat(compareResult).isEqualTo(1) + } + + @Test + fun testCompareIterables_singletonList_twoItemList_commonElem_returnsNegativeOne() { + val leftList = listOf("1") + val rightList = listOf("1", "2") + + val compareResult = stringComparator.compareIterables(leftList, rightList) + + assertThat(compareResult).isEqualTo(-1) + } + + @Test + fun testCompareIterables_equalSizeLists_sameItems_sameOrder_returnsZero() { + val leftList = listOf("1", "2") + val rightList = listOf("1", "2") + + val compareResult = stringComparator.compareIterables(leftList, rightList) + + assertThat(compareResult).isEqualTo(0) + } + + @Test + fun testCompareIterables_equalSizeLists_sameItems_differentOrder_returnsZero() { + val leftList = listOf("1", "2") + val rightList = listOf("2", "1") + + val compareResult = stringComparator.compareIterables(leftList, rightList) + + // Order shouldn't matter. + assertThat(compareResult).isEqualTo(0) + } + + @Test + fun testCompareIterables_list223_list123_returnsOne() { + val leftList = listOf("2", "2", "3") + val rightList = listOf("1", "2", "3") + + val compareResult = stringComparator.compareIterables(leftList, rightList) + + // The first element is larger in the left list, so it's "greater". + assertThat(compareResult).isEqualTo(1) + } + + @Test + fun testCompareIterables_list123_list223_returnsNegativeOne() { + val leftList = listOf("1", "2", "3") + val rightList = listOf("2", "2", "3") + + val compareResult = stringComparator.compareIterables(leftList, rightList) + + assertThat(compareResult).isEqualTo(-1) + } + + @Test + fun testCompareIterables_list123_list11_returnsOne() { + val leftList = listOf("1", "2", "3") + val rightList = listOf("1", "1") + + val compareResult = stringComparator.compareIterables(leftList, rightList) + + // The second item is bigger in the first list, so it's "greater". + assertThat(compareResult).isEqualTo(1) + } + + @Test + fun testCompareIterables_list123_list13_returnsNegativeOne() { + val leftList = listOf("1", "2", "3") + val rightList = listOf("1", "3") + + val compareResult = stringComparator.compareIterables(leftList, rightList) + + // The second item is bigger in the second list, so the first one is "lesser". + assertThat(compareResult).isEqualTo(-1) + } + + @Test + fun testCompareIterables_list223_list1_returnsOne() { + val leftList = listOf("1", "2", "3") + val rightList = listOf("1") + + val compareResult = stringComparator.compareIterables(leftList, rightList) + + assertThat(compareResult).isEqualTo(1) + } + + @Test + fun testCompareIterables_list123_list2_returnsNegativeOne() { + val leftList = listOf("1", "2", "3") + val rightList = listOf("2") + + val compareResult = stringComparator.compareIterables(leftList, rightList) + + assertThat(compareResult).isEqualTo(-1) + } + + @Test + fun testCompareIterables_list22_list2_returnsOne() { + val leftList = listOf("2", "2") + val rightList = listOf("2") + + val compareResult = stringComparator.compareIterables(leftList, rightList) + + // The first list has an extra element. + assertThat(compareResult).isEqualTo(1) + } + + @Test + fun testCompareIterables_list2_list22_returnsNegativeOne() { + val leftList = listOf("2") + val rightList = listOf("2", "2") + + val compareResult = stringComparator.compareIterables(leftList, rightList) + + // The second list has an extra element. + assertThat(compareResult).isEqualTo(-1) + } + + @Test + fun testCompareIterables_list22_list22_returnsZero() { + val leftList = listOf("2", "2") + val rightList = listOf("2", "2") + + val compareResult = stringComparator.compareIterables(leftList, rightList) + + assertThat(compareResult).isEqualTo(0) + } + + @Test + fun testCompareProtos_defaultAndDefault_returnsZero() { + val leftProto = TestMessage.newBuilder().build() + val rightProto = TestMessage.newBuilder().build() + + val compareResult = protoComparator.compareProtos(leftProto, rightProto) + + // Two default instances are equal. + assertThat(compareResult).isEqualTo(0) + } + + @Test + fun testCompareProtos_nonDefaultZeroAndDefault_returnsZero() { + val leftProto = TEST_MESSAGE_0 + val rightProto = TestMessage.newBuilder().build() + + val compareResult = protoComparator.compareProtos(leftProto, rightProto) + + // Even though the left proto is defined, the value of 0 for its int field makes it the same as + // a default (per proto3 spec). + assertThat(compareResult).isEqualTo(0) + } + + @Test + fun testCompareProtos_nonDefaultAndDefault_returnsOne() { + val leftProto = TEST_MESSAGE_1 + val rightProto = TestMessage.newBuilder().build() + + val compareResult = protoComparator.compareProtos(leftProto, rightProto) + + // The left proto is actually defined. + assertThat(compareResult).isEqualTo(1) + } + + @Test + fun testCompareProtos_defaultAndNonDefault_returnsNegativeOne() { + val leftProto = TestMessage.newBuilder().build() + val rightProto = TEST_MESSAGE_1 + + val compareResult = protoComparator.compareProtos(leftProto, rightProto) + + // The right proto is actually defined. + assertThat(compareResult).isEqualTo(-1) + } + + @Test + fun testCompareProtos_twoNonDefaults_sameProtoValues_returnsZero() { + val leftProto = TEST_MESSAGE_1 + val rightProto = TEST_MESSAGE_1 + + val compareResult = protoComparator.compareProtos(leftProto, rightProto) + + // The protos are equal per protoComparator. + assertThat(compareResult).isEqualTo(0) + } + + @Test + fun testCompareProtos_twoNonDefaults_leftIsLarger_returnsOne() { + val leftProto = TEST_MESSAGE_2 + val rightProto = TEST_MESSAGE_1 + + val compareResult = protoComparator.compareProtos(leftProto, rightProto) + + // The left proto is larger per protoComparator. + assertThat(compareResult).isEqualTo(1) + } + + @Test + fun testCompareProtos_twoNonDefaults_leftIsSmaller_returnsNegativeOne() { + val leftProto = TEST_MESSAGE_1 + val rightProto = TEST_MESSAGE_2 + + val compareResult = protoComparator.compareProtos(leftProto, rightProto) + + // The right proto is larger per protoComparator. + assertThat(compareResult).isEqualTo(-1) } } diff --git a/utility/src/test/java/org/oppia/android/util/math/MathExpressionExtensionsTest.kt b/utility/src/test/java/org/oppia/android/util/math/MathExpressionExtensionsTest.kt index 0a81df14c54..586a7253c9d 100644 --- a/utility/src/test/java/org/oppia/android/util/math/MathExpressionExtensionsTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/MathExpressionExtensionsTest.kt @@ -19,7 +19,8 @@ import org.robolectric.annotation.LooperMode * Note that this suite only verifies that the extensions work at a high-level. More specific * verifications for operations like LaTeX conversion and expression evaluation are part of more * targeted test suites such as [ExpressionToLatexConverterTest] and - * [NumericExpressionEvaluatorTest]. + * [NumericExpressionEvaluatorTest]. For comparable operations, see + * [ExpressionToComparableOperationConverterTest]. */ // FunctionName: test names are conventionally named with underscores. // SameParameterValue: tests should have specific context included/excluded for readability. @@ -72,6 +73,28 @@ class MathExpressionExtensionsTest { assertThat(result).isIrrationalThat().isWithin(1e-5).of(322194.700361352) } + @Test + fun testToComparableOperation_twoAlgebraicExpressions_differentOrders_returnsEqualOperations() { + val expression1 = parseAlgebraicExpression("x+2/x-(-7*8-9)+sqrt((x+2)+1)+3xy^2") + val expression2 = parseAlgebraicExpression("sqrt(x+(1+2))+2/x+x-(-9+8*-7-3y^2x)") + + val operation1 = expression1.toComparable() + val operation2 = expression2.toComparable() + + assertThat(operation1).isEqualTo(operation2) + } + + @Test + fun testToComparableOperation_twoAlgebraicExpressions_differentValue_returnsUnequalOperations() { + val expression1 = parseAlgebraicExpression("x+2/x-(-7*8-9)+sqrt((x+2)+1)+3xy^2") + val expression2 = parseAlgebraicExpression("sqrt(x+(1+3))+2/x+x-(-9+8*-7-3y^2x)") + + val operation1 = expression1.toComparable() + val operation2 = expression2.toComparable() + + assertThat(operation1).isNotEqualTo(operation2) + } + private companion object { private fun parseNumericExpression(expression: String): MathExpression { return parseNumericExpression(expression, ALL_ERRORS).retrieveExpectedSuccessfulResult() diff --git a/utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt b/utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt index 9760b175ff8..43234d63feb 100644 --- a/utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt @@ -13,7 +13,15 @@ import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.RunParameter import org.oppia.android.testing.math.RealSubject.Companion.assertThat import org.robolectric.annotation.LooperMode -/** Tests for [Real] extensions. */ +/** + * Tests for [Real] extensions. + * + * Note that this suite makes special use of parameterized tests to significantly reduce the length + * of the suite, even partially at the expensive of good testing practices (since many of the + * parameterized tests are actually verifying multiple behaviors). Given the generally trivial + * nature of these behaviors, this trade-off is considered acceptable. That being said, this pattern + * should only be replicated elsewhere in the codebase after thorough consideration. + */ // FunctionName: test names are conventionally named with underscores. @Suppress("FunctionName") @RunWith(OppiaParameterizedTestRunner::class) @@ -1774,6 +1782,203 @@ class RealExtensionsTest { assertThat(result).isIrrationalThat().isWithin(1e-5).of(1.772004515) } + /* + * Tests for REAL_COMPARATOR. + * + * Note that these specifically don't try to compare against negative doubles since the comparison + * logic is a bit unexpected (see https://stackoverflow.com/a/45544483). + */ + + @Test + @RunParameterized( + Iteration("0==0", "lhsInt=0", "rhsInt=0", "expInt=0"), + Iteration("-2<0", "lhsInt=-2", "rhsInt=0", "expInt=-1"), + Iteration("-5<-2", "lhsInt=-5", "rhsInt=-2", "expInt=-1"), + Iteration("-2>-5", "lhsInt=-2", "rhsInt=-5", "expInt=1"), + Iteration("2>0", "lhsInt=2", "rhsInt=0", "expInt=1"), + Iteration("5>2", "lhsInt=5", "rhsInt=2", "expInt=1"), + Iteration("2<5", "lhsInt=2", "rhsInt=5", "expInt=-1"), + Iteration("-2<5", "lhsInt=-2", "rhsInt=5", "expInt=-1"), + Iteration("5>-2", "lhsInt=5", "rhsInt=-2", "expInt=1") + ) + fun testComparator_intAndInt_returnsCorrectComparisonInt() { + val lhsValue = createIntegerReal(lhsInt) + val rhsValue = createIntegerReal(rhsInt) + + val comparison = REAL_COMPARATOR.compare(lhsValue, rhsValue) + + assertThat(comparison).isEqualTo(expInt) + } + + @Test + @RunParameterized( + Iteration("0==0", "lhsInt=0", "rhsFrac=0", "expInt=0"), + Iteration("-2<0", "lhsInt=-2", "rhsFrac=0", "expInt=-1"), + Iteration("-5<-2", "lhsInt=-5", "rhsFrac=-2", "expInt=-1"), + Iteration("-5<-1/2", "lhsInt=-5", "rhsFrac=-1/2", "expInt=-1"), + Iteration("-2>-5", "lhsInt=-2", "rhsFrac=-5", "expInt=1"), + Iteration("-1>-3/2", "lhsInt=-1", "rhsFrac=-3/2", "expInt=1"), + Iteration("2>0", "lhsInt=2", "rhsFrac=0", "expInt=1"), + Iteration("5>2", "lhsInt=5", "rhsFrac=2", "expInt=1"), + Iteration("2<5", "lhsInt=2", "rhsFrac=5", "expInt=-1"), + Iteration("2<7/2", "lhsInt=2", "rhsFrac=7/2", "expInt=-1"), + Iteration("5>3/2", "lhsInt=5", "rhsFrac=3/2", "expInt=1"), + Iteration("-2<5", "lhsInt=-2", "rhsFrac=5", "expInt=-1"), + Iteration("-2<3/2", "lhsInt=-2", "rhsFrac=3/2", "expInt=-1"), + Iteration("5>-2", "lhsInt=5", "rhsFrac=-2", "expInt=1"), + Iteration("5>-3/2", "lhsInt=5", "rhsFrac=-3/2", "expInt=1") + ) + fun testComparator_intAndFraction_returnsCorrectComparisonInt() { + val lhsValue = createIntegerReal(lhsInt) + val rhsValue = createRationalReal(rhsFrac) + + val comparison = REAL_COMPARATOR.compare(lhsValue, rhsValue) + + assertThat(comparison).isEqualTo(expInt) + } + + @Test + @RunParameterized( + Iteration("0==0", "lhsInt=0", "rhsDouble=0.0", "expInt=0"), + Iteration("-2<0", "lhsInt=-2", "rhsDouble=0.0", "expInt=-1"), + Iteration("-5<-3.14", "lhsInt=-5", "rhsDouble=-3.14", "expInt=-1"), + Iteration("-2>-6.28", "lhsInt=-2", "rhsDouble=-6.28", "expInt=1"), + Iteration("2>0", "lhsInt=2", "rhsDouble=0.0", "expInt=1"), + Iteration("5>3.14", "lhsInt=5", "rhsDouble=3.14", "expInt=1"), + Iteration("2<6.28", "lhsInt=2", "rhsDouble=6.28", "expInt=-1"), + Iteration("-2<3.14", "lhsInt=-2", "rhsDouble=3.14", "expInt=-1"), + Iteration("2>-3.14", "lhsInt=2", "rhsDouble=-3.14", "expInt=1") + ) + fun testComparator_intAndDouble_returnsCorrectComparisonInt() { + val lhsValue = createIntegerReal(lhsInt) + val rhsValue = createIrrationalReal(rhsDouble) + + val comparison = REAL_COMPARATOR.compare(lhsValue, rhsValue) + + assertThat(comparison).isEqualTo(expInt) + } + + @Test + @RunParameterized( + Iteration("0==0", "lhsFrac=0", "rhsInt=0", "expInt=0"), + Iteration("-3/2<0", "lhsFrac=-3/2", "rhsInt=0", "expInt=-1"), + Iteration("-7/2<-3", "lhsFrac=-7/2", "rhsInt=-3", "expInt=-1"), + Iteration("-3/2>-5", "lhsFrac=-3/2", "rhsInt=-5", "expInt=1"), + Iteration("3/2>0", "lhsFrac=3/2", "rhsInt=0", "expInt=1"), + Iteration("7/2>3", "lhsFrac=7/2", "rhsInt=3", "expInt=1"), + Iteration("3/2<5", "lhsFrac=3/2", "rhsInt=5", "expInt=-1"), + Iteration("-3/2<3", "lhsFrac=-3/2", "rhsInt=3", "expInt=-1"), + Iteration("3/2>-3", "lhsFrac=3/2", "rhsInt=-3", "expInt=1") + ) + fun testComparator_fractionAndInt_returnsCorrectComparisonInt() { + val lhsValue = createRationalReal(lhsFrac) + val rhsValue = createIntegerReal(rhsInt) + + val comparison = REAL_COMPARATOR.compare(lhsValue, rhsValue) + + assertThat(comparison).isEqualTo(expInt) + } + + @Test + @RunParameterized( + Iteration("0==0", "lhsFrac=0", "rhsFrac=0", "expInt=0"), + Iteration("-3/2<0", "lhsFrac=-3/2", "rhsFrac=0", "expInt=-1"), + Iteration("-7/2<-3/2", "lhsFrac=-7/2", "rhsFrac=-3/2", "expInt=-1"), + Iteration("3/2>0", "lhsFrac=3/2", "rhsFrac=0", "expInt=1"), + Iteration("7/2>3/2", "lhsFrac=7/2", "rhsFrac=3/2", "expInt=1"), + Iteration("3/2<7/2", "lhsFrac=3/2", "rhsFrac=7/2", "expInt=-1"), + Iteration("-3/2<3/2", "lhsFrac=-3/2", "rhsFrac=3/2", "expInt=-1"), + Iteration("3/2>-3/2", "lhsFrac=3/2", "rhsFrac=-3/2", "expInt=1") + ) + fun testComparator_fractionAndFraction_returnsCorrectComparisonInt() { + val lhsValue = createRationalReal(lhsFrac) + val rhsValue = createRationalReal(rhsFrac) + + val comparison = REAL_COMPARATOR.compare(lhsValue, rhsValue) + + assertThat(comparison).isEqualTo(expInt) + } + + @Test + @RunParameterized( + Iteration("0==0", "lhsFrac=0", "rhsDouble=0.0", "expInt=0"), + Iteration("-3/2<0", "lhsFrac=-3/2", "rhsDouble=0.0", "expInt=-1"), + Iteration("-7/2<-3.14", "lhsFrac=-7/2", "rhsDouble=-3.14", "expInt=-1"), + Iteration("3/2>0", "lhsFrac=3/2", "rhsDouble=0.0", "expInt=1"), + Iteration("7/2>3.14", "lhsFrac=7/2", "rhsDouble=3.14", "expInt=1"), + Iteration("3/2<3.14", "lhsFrac=3/2", "rhsDouble=3.14", "expInt=-1"), + Iteration("-3/2<3.14", "lhsFrac=-3/2", "rhsDouble=3.14", "expInt=-1"), + Iteration("3/2>-3.14", "lhsFrac=3/2", "rhsDouble=-3.14", "expInt=1") + ) + fun testComparator_fractionAndDouble_returnsCorrectComparisonInt() { + val lhsValue = createRationalReal(lhsFrac) + val rhsValue = createIrrationalReal(rhsDouble) + + val comparison = REAL_COMPARATOR.compare(lhsValue, rhsValue) + + assertThat(comparison).isEqualTo(expInt) + } + + @Test + @RunParameterized( + Iteration("0==0", "lhsDouble=0.0", "rhsInt=0", "expInt=0"), + Iteration("-3.14<0", "lhsDouble=-3.14", "rhsInt=0", "expInt=-1"), + Iteration("-6.28<-4", "lhsDouble=-6.28", "rhsInt=-4", "expInt=-1"), + Iteration("3.14>0", "lhsDouble=3.14", "rhsInt=0", "expInt=1"), + Iteration("6.28>4", "lhsDouble=6.28", "rhsInt=4", "expInt=1"), + Iteration("3.14<4", "lhsDouble=3.14", "rhsInt=4", "expInt=-1"), + Iteration("-3.14<4", "lhsDouble=-3.14", "rhsInt=4", "expInt=-1"), + Iteration("3.14>-4", "lhsDouble=3.14", "rhsInt=-4", "expInt=1") + ) + fun testComparator_doubleAndInt_returnsCorrectComparisonInt() { + val lhsValue = createIrrationalReal(lhsDouble) + val rhsValue = createIntegerReal(rhsInt) + + val comparison = REAL_COMPARATOR.compare(lhsValue, rhsValue) + + assertThat(comparison).isEqualTo(expInt) + } + + @Test + @RunParameterized( + Iteration("0==0", "lhsDouble=0.0", "rhsFrac=0", "expInt=0"), + Iteration("-3.14<0", "lhsDouble=-3.14", "rhsFrac=0", "expInt=-1"), + Iteration("-6.28<-7/2", "lhsDouble=-6.28", "rhsFrac=-7/2", "expInt=-1"), + Iteration("3.14>0", "lhsDouble=3.14", "rhsFrac=0", "expInt=1"), + Iteration("6.28>7/2", "lhsDouble=6.28", "rhsFrac=7/2", "expInt=1"), + Iteration("3.14<7/2", "lhsDouble=3.14", "rhsFrac=7/2", "expInt=-1"), + Iteration("-3.14<7/2", "lhsDouble=-3.14", "rhsFrac=7/2", "expInt=-1"), + Iteration("3.14>-7/2", "lhsDouble=3.14", "rhsFrac=-7/2", "expInt=1") + ) + fun testComparator_doubleAndFraction_returnsCorrectComparisonInt() { + val lhsValue = createIrrationalReal(lhsDouble) + val rhsValue = createRationalReal(rhsFrac) + + val comparison = REAL_COMPARATOR.compare(lhsValue, rhsValue) + + assertThat(comparison).isEqualTo(expInt) + } + + @Test + @RunParameterized( + Iteration("0==0", "lhsDouble=0.0", "rhsDouble=0.0", "expInt=0"), + Iteration("-3.14<0", "lhsDouble=-3.14", "rhsDouble=0.0", "expInt=-1"), + Iteration("-6.28<-3.14", "lhsDouble=-6.28", "rhsDouble=-3.14", "expInt=-1"), + Iteration("3.14>0", "lhsDouble=3.14", "rhsDouble=0.0", "expInt=1"), + Iteration("6.28>3.14", "lhsDouble=6.28", "rhsDouble=3.14", "expInt=1"), + Iteration("3.14<6.28", "lhsDouble=3.14", "rhsDouble=6.28", "expInt=-1"), + Iteration("-3.14<6.28", "lhsDouble=-3.14", "rhsDouble=6.28", "expInt=-1"), + Iteration("3.14>-6.28", "lhsDouble=3.14", "rhsDouble=-6.28", "expInt=1") + ) + fun testComparator_doubleAndDouble_returnsCorrectComparisonInt() { + val lhsValue = createIrrationalReal(lhsDouble) + val rhsValue = createIrrationalReal(rhsDouble) + + val comparison = REAL_COMPARATOR.compare(lhsValue, rhsValue) + + assertThat(comparison).isEqualTo(expInt) + } + private fun createRationalReal(rawFractionExpression: String) = createRationalReal(fractionParser.parseFractionFromString(rawFractionExpression)) } From 573fee9deada59fd7d13db2a004b7562642f40f0 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 28 Jan 2022 20:45:31 -0800 Subject: [PATCH 088/162] KDocs & test exemption. --- .../file_content_validation_checks.textproto | 1 + .../android/util/math/ComparatorExtensions.kt | 20 ++ ...xpressionToComparableOperationConverter.kt | 40 ++- .../util/math/MathExpressionExtensions.kt | 9 +- .../oppia/android/util/math/RealExtensions.kt | 5 + .../org/oppia/android/util/math/BUILD.bazel | 4 +- .../util/math/ComparatorExtensionsTest.kt | 3 +- ...ssionToComparableOperationConverterTest.kt | 268 +++++++++--------- .../util/math/MathExpressionExtensionsTest.kt | 8 +- 9 files changed, 205 insertions(+), 153 deletions(-) diff --git a/scripts/assets/file_content_validation_checks.textproto b/scripts/assets/file_content_validation_checks.textproto index 8b8fe90e4cd..bcae6e30b40 100644 --- a/scripts/assets/file_content_validation_checks.textproto +++ b/scripts/assets/file_content_validation_checks.textproto @@ -287,6 +287,7 @@ file_content_checks { prohibited_content_regex: "OppiaParameterizedTestRunner" failure_message: "To use OppiaParameterizedTestRunner, please add an exemption to file_content_validation_checks.textproto and add an explanation for your use case in your PR description. Note that parameterized tests should only be used in special circumstances where a single behavior can be tested across multiple inputs, or for especially large test suites that can be trivially reduced." exempted_file_name: "scripts/src/javatests/org/oppia/android/scripts/regex/RegexPatternValidationCheckTest.kt" + exempted_file_name: "utility/src/test/java/org/oppia/android/util/math/ExpressionToComparableOperationConverterTest.kt" exempted_file_name: "utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt" exempted_file_name: "utility/src/test/java/org/oppia/android/util/math/MathTokenizerTest.kt" exempted_file_name: "utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt" diff --git a/utility/src/main/java/org/oppia/android/util/math/ComparatorExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/ComparatorExtensions.kt index 99b66e11243..4e4355d0ec0 100644 --- a/utility/src/main/java/org/oppia/android/util/math/ComparatorExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/ComparatorExtensions.kt @@ -2,6 +2,17 @@ package org.oppia.android.util.math import com.google.protobuf.MessageLite +/** + * Compares two [Iterable]s based on an item [Comparator] and returns the result. + * + * The two [Iterable]s are iterated in an order determined by [Comparator], and then compared + * element-by-element. If any element is different, the difference of that item becomes the overall + * difference. If the lists are different sizes (but otherwise match up to the boundary of one + * list), then the longer list will be considered "greater". + * + * This means that two [Iterable]s are only equal if they have the same number of elements, and that + * all of their items are equal per this [Comparator], including duplicates. + */ fun Comparator.compareIterables(first: Iterable, second: Iterable): Int { // Reference: https://stackoverflow.com/a/30107086. val firstIter = first.sortedWith(this).iterator() @@ -19,6 +30,15 @@ fun Comparator.compareIterables(first: Iterable, second: Iterable): } } +/** + * Compares two protos of type [T] ([left] and [right]) using this [Comparator] and returns the + * result. + * + * This adds behavior above the standard ``compare`` function by short-circuiting if either proto is + * equal to the default instance (in which case "defined" is always considered larger, and this + * [Comparator] isn't used). This short-circuiting behavior can be useful when comparing recursively + * infinite proto structures to avoid stack overflows.. + */ fun Comparator.compareProtos(left: T, right: T): Int { val defaultValue = left.defaultInstanceForType val leftIsDefault = left == defaultValue diff --git a/utility/src/main/java/org/oppia/android/util/math/ExpressionToComparableOperationConverter.kt b/utility/src/main/java/org/oppia/android/util/math/ExpressionToComparableOperationConverter.kt index 4f1bc904aba..5983f09e27d 100644 --- a/utility/src/main/java/org/oppia/android/util/math/ExpressionToComparableOperationConverter.kt +++ b/utility/src/main/java/org/oppia/android/util/math/ExpressionToComparableOperationConverter.kt @@ -26,11 +26,31 @@ import org.oppia.android.app.model.MathUnaryOperation.Operator as UnaryOperator import org.oppia.android.app.model.MathUnaryOperation.Operator.NEGATE import org.oppia.android.app.model.MathUnaryOperation.Operator.POSITIVE +/** + * Converter from [MathExpression] to [ComparableOperation]. + * + * See the separate proto details for context, and [convertToComparableOperation] for the actual conversion + * function. + */ class ExpressionToComparableOperationConverter private constructor() { companion object { private val COMPARABLE_OPERATION_COMPARATOR by lazy { createComparableOperationComparator() } - fun MathExpression.toComparableOperation(): ComparableOperation { + /** + * Returns a new [ComparableOperation] representing this [MathExpression]. + * + * Comparable operations are representations of math expressions that are deterministically + * arranged to ensure two expressions that only differ due to associativity or commutativity are + * still equal. This is done by combining neighboring arithmetic operations into accumulations, + * and still retaining the structures for non-commutative operations. The order of all + * operations is well-defined and deterministic. Further, how elements retain inverted or + * negated properties is also deterministic (and designed to minimize negative values). + * + * The tests for this method provide very thorough and broad examples of different cases that + * this function supports. In particular, the equality tests are useful to see what sorts of + * expressions can be considered the same per [ComparableOperation]. + */ + fun MathExpression.convertToComparableOperation(): ComparableOperation { return when (expressionTypeCase) { CONSTANT -> ComparableOperation.newBuilder().apply { constantTerm = constant @@ -49,21 +69,21 @@ class ExpressionToComparableOperationConverter private constructor() { ComparableOperation.getDefaultInstance() } UNARY_OPERATION -> when (unaryOperation.operator) { - NEGATE -> unaryOperation.operand.toComparableOperation().invertNegation() - POSITIVE -> unaryOperation.operand.toComparableOperation() + NEGATE -> unaryOperation.operand.convertToComparableOperation().invertNegation() + POSITIVE -> unaryOperation.operand.convertToComparableOperation() UnaryOperator.OPERATOR_UNSPECIFIED, UnaryOperator.UNRECOGNIZED, null -> ComparableOperation.getDefaultInstance() } FUNCTION_CALL -> when (functionCall.functionType) { SQUARE_ROOT -> ComparableOperation.newBuilder().apply { nonCommutativeOperation = NonCommutativeOperation.newBuilder().apply { - squareRoot = functionCall.argument.toComparableOperation() + squareRoot = functionCall.argument.convertToComparableOperation() }.build() }.build() FunctionType.FUNCTION_UNSPECIFIED, FunctionType.UNRECOGNIZED, null -> ComparableOperation.getDefaultInstance() } - GROUP -> group.toComparableOperation() + GROUP -> group.convertToComparableOperation() EXPRESSIONTYPE_NOT_SET, null -> ComparableOperation.getDefaultInstance() } } @@ -119,8 +139,8 @@ class ExpressionToComparableOperationConverter private constructor() { addOperationToSum(expression.unaryOperation.operand, forceNegative) // Skip groups so that nested operations can be properly combined. expression.expressionTypeCase == GROUP -> addOperationToSum(expression.group, forceNegative) - forceNegative -> addCombinedOperations(expression.toComparableOperation().invertNegation()) - else -> addCombinedOperations(expression.toComparableOperation()) + forceNegative -> addCombinedOperations(expression.convertToComparableOperation().invertNegation()) + else -> addCombinedOperations(expression.convertToComparableOperation()) } } @@ -169,7 +189,7 @@ class ExpressionToComparableOperationConverter private constructor() { expression.expressionTypeCase == GROUP -> addOperationToProduct(expression.group, forceInverse, invertNegation) else -> { - val operationExpression = expression.toComparableOperation() + val operationExpression = expression.convertToComparableOperation() val potentiallyInvertedExpression = if (invertNegation) { operationExpression.invertNegation() } else operationExpression @@ -200,8 +220,8 @@ class ExpressionToComparableOperationConverter private constructor() { nonCommutativeOperation = NonCommutativeOperation.newBuilder().apply { setOperation( BinaryOperation.newBuilder().apply { - leftOperand = binaryOperation.leftOperand.toComparableOperation() - rightOperand = binaryOperation.rightOperand.toComparableOperation() + leftOperand = binaryOperation.leftOperand.convertToComparableOperation() + rightOperand = binaryOperation.rightOperand.convertToComparableOperation() }.build() ) }.build() diff --git a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt index 109490e5abe..6b89e83acdb 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt @@ -4,7 +4,7 @@ import org.oppia.android.app.model.ComparableOperation import org.oppia.android.app.model.MathEquation import org.oppia.android.app.model.MathExpression import org.oppia.android.app.model.Real -import org.oppia.android.util.math.ExpressionToComparableOperationConverter.Companion.toComparableOperation +import org.oppia.android.util.math.ExpressionToComparableOperationConverter.Companion.convertToComparableOperation import org.oppia.android.util.math.ExpressionToLatexConverter.Companion.convertToLatex import org.oppia.android.util.math.NumericExpressionEvaluator.Companion.evaluate @@ -31,4 +31,9 @@ fun MathEquation.toRawLatex(divAsFraction: Boolean): String = convertToLatex(div */ fun MathExpression.evaluateAsNumericExpression(): Real? = evaluate() -fun MathExpression.toComparable(): ComparableOperation = toComparableOperation() +/** + * Returns the [ComparableOperation] representation of this [MathExpression]. + * + * See [convertToComparableOperation] for details. + */ +fun MathExpression.toComparableOperation(): ComparableOperation = convertToComparableOperation() diff --git a/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt index 0006df1e5d4..69f4ab19aa2 100644 --- a/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt @@ -9,6 +9,11 @@ import org.oppia.android.app.model.Real.RealTypeCase.REALTYPE_NOT_SET import kotlin.math.absoluteValue import kotlin.math.pow +/** + * [Comparator] for [Real]s that ensures two reals can be compared even if they are different types. + * + * Note that no reliance should be placed on how negative zeros for doubles and fractions behave. + */ val REAL_COMPARATOR: Comparator by lazy { Comparator.comparing(Real::toDouble) } /** diff --git a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel index 8a1f784aef3..9c4a4ef43bb 100644 --- a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel @@ -67,12 +67,12 @@ oppia_android_test( test_manifest = "//utility:test_manifest", deps = [ "//model/src/main/proto:math_java_proto_lite", + "//testing/src/main/java/org/oppia/android/testing/junit:oppia_parameterized_test_runner", + "//testing/src/main/java/org/oppia/android/testing/junit:parameterized_robolectric_test_runner", "//testing/src/main/java/org/oppia/android/testing/math:comparable_operation_subject", "//third_party:com_google_truth_truth", "//third_party:junit_junit", "//third_party:org_robolectric_robolectric", - "//testing/src/main/java/org/oppia/android/testing/junit:oppia_parameterized_test_runner", - "//testing/src/main/java/org/oppia/android/testing/junit:parameterized_robolectric_test_runner", "//third_party:robolectric_android-all", "//utility/src/main/java/org/oppia/android/util/math:extensions", "//utility/src/main/java/org/oppia/android/util/math:math_expression_parser", diff --git a/utility/src/test/java/org/oppia/android/util/math/ComparatorExtensionsTest.kt b/utility/src/test/java/org/oppia/android/util/math/ComparatorExtensionsTest.kt index 91c8f352e4a..5c44c17968e 100644 --- a/utility/src/test/java/org/oppia/android/util/math/ComparatorExtensionsTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/ComparatorExtensionsTest.kt @@ -178,7 +178,8 @@ class ComparatorExtensionsTest { val compareResult = stringComparator.compareIterables(leftList, rightList) - // The first list has an extra element. + // The first list has an extra element. This also verifies that duplicates are correctly + // considered during comparison. assertThat(compareResult).isEqualTo(1) } diff --git a/utility/src/test/java/org/oppia/android/util/math/ExpressionToComparableOperationConverterTest.kt b/utility/src/test/java/org/oppia/android/util/math/ExpressionToComparableOperationConverterTest.kt index 925ac470db8..1452a9c70bd 100644 --- a/utility/src/test/java/org/oppia/android/util/math/ExpressionToComparableOperationConverterTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/ExpressionToComparableOperationConverterTest.kt @@ -2,7 +2,7 @@ package org.oppia.android.util.math import com.google.common.truth.Truth.assertThat import org.junit.Test -import org.oppia.android.util.math.ExpressionToComparableOperationConverter.Companion.toComparableOperation +import org.oppia.android.util.math.ExpressionToComparableOperationConverter.Companion.convertToComparableOperation import org.junit.runner.RunWith import org.oppia.android.app.model.ComparableOperation.CommutativeAccumulation.AccumulationType.PRODUCT import org.oppia.android.app.model.ComparableOperation.CommutativeAccumulation.AccumulationType.SUMMATION @@ -38,7 +38,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_integerConstantExpression_returnsConstantOperation() { val expression = parseNumericExpression("2") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() assertThat(comparable).hasStructureThatMatches { constantTerm { @@ -51,7 +51,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_decimalConstantExpression_returnsConstantOperation() { val expression = parseNumericExpression("3.14") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() assertThat(comparable).hasStructureThatMatches { constantTerm { @@ -64,7 +64,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_variableExpression_returnsVariableOperation() { val expression = parseAlgebraicExpression("x") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() assertThat(comparable).hasStructureThatMatches { variableTerm { @@ -77,7 +77,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_addition_returnsSummation() { val expression = parseNumericExpression("1+2") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() assertThat(comparable).hasStructureThatMatches { commutativeAccumulationWithType(SUMMATION) { @@ -102,7 +102,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_addition_sameValues_returnsSummationWithBoth() { val expression = parseNumericExpression("1+1") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() assertThat(comparable).hasStructureThatMatches { commutativeAccumulationWithType(SUMMATION) { @@ -127,7 +127,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_subtraction_returnsSummationOfNegative() { val expression = parseNumericExpression("1-2") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() assertThat(comparable).hasStructureThatMatches { commutativeAccumulationWithType(SUMMATION) { @@ -152,7 +152,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_multiplication_returnsProduct() { val expression = parseNumericExpression("2*3") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() assertThat(comparable).hasStructureThatMatches { commutativeAccumulationWithType(PRODUCT) { @@ -176,7 +176,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_division_returnsProductOfInverted() { val expression = parseNumericExpression("2/3") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() assertThat(comparable).hasStructureThatMatches { commutativeAccumulationWithType(PRODUCT) { @@ -200,7 +200,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_exponentiation_returnsNonCommutativeOperation() { val expression = parseNumericExpression("2^3") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() assertThat(comparable).hasStructureThatMatches { nonCommutativeOperation { @@ -224,7 +224,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_squareRoot_returnsNonCommutativeOperation() { val expression = parseNumericExpression("sqrt(2)") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() assertThat(comparable).hasStructureThatMatches { nonCommutativeOperation { @@ -241,7 +241,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_variableTerm_returnsNonNegativeOperation() { val expression = parseAlgebraicExpression("x") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() assertThat(comparable).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() @@ -252,7 +252,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_negatedVariable_returnsNegativeVariableOperation() { val expression = parseAlgebraicExpression("-x") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() assertThat(comparable).hasStructureThatMatches { hasNegatedPropertyThat().isTrue() @@ -266,7 +266,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_positiveVariable_returnsVariableOperation() { val expression = parseAlgebraicExpression("+x", errorCheckingMode = REQUIRED_ONLY) - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() assertThat(comparable).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() @@ -280,7 +280,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_positiveOfNegativeVariable_returnsNegativeVariableOperation() { val expression = parseAlgebraicExpression("+-x", errorCheckingMode = REQUIRED_ONLY) - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() assertThat(comparable).hasStructureThatMatches { hasNegatedPropertyThat().isTrue() @@ -295,7 +295,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_subtractionOfNegative_returnsSummationWithPositives() { val expression = parseNumericExpression("1--2") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() // Verify that the subtraction & negation cancel out each other. assertThat(comparable).hasStructureThatMatches { @@ -322,7 +322,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_negativePlusPositive_returnsSummationWithFirstTermNegative() { val expression = parseNumericExpression("-2+1") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() // Verify that the negative only applies to the 2, not to the whole expression. assertThat(comparable).hasStructureThatMatches { @@ -349,7 +349,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_multipleAdditions_returnsCombinedSummation() { val expression = parseNumericExpression("1+2+3") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() assertThat(comparable).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() @@ -381,7 +381,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_multipleSubtractions_returnsCombinedSummation() { val expression = parseNumericExpression("1-2-3") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() assertThat(comparable).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() @@ -413,7 +413,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_additionsAndSubtractions_returnsCombinedSummation() { val expression = parseNumericExpression("1+2-3-4+5") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() assertThat(comparable).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() @@ -457,7 +457,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_additionsWithNestedAdds_returnsCompletelyCombinedSummation() { val expression = parseNumericExpression("1+((2+(3+4)+5)+6)") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() assertThat(comparable).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() @@ -507,7 +507,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_subtractsWithNesting_returnsSummationWithDistributedNegation() { val expression = parseNumericExpression("1-(2+3-4)") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() // Both the 2 & 3 are negative since the subtraction distributes, and the 4 becomes positive. assertThat(comparable).hasStructureThatMatches { @@ -546,7 +546,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_subtractsWithNestedSubs_returnsCompletelyCombinedSummation() { val expression = parseNumericExpression("1-((2-(3-4)-5)-6)") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() // Some of these are positive because of distribution. assertThat(comparable).hasStructureThatMatches { @@ -598,7 +598,7 @@ class ExpressionToComparableOperationConverterTest { val expression = parseNumericExpression("1++(2-3)+-(4+5--(2+3-1))", errorCheckingMode = REQUIRED_ONLY) - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() // This also verifies that negation distributes in the same way as subtraction. assertThat(comparable).hasStructureThatMatches { @@ -661,7 +661,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_multipleMultiplications_returnsCombinedProduct() { val expression = parseNumericExpression("2*3*4") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() assertThat(comparable).hasStructureThatMatches { hasInvertedPropertyThat().isFalse() @@ -693,7 +693,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_multipleDivisions_returnsCombinedProduct() { val expression = parseNumericExpression("2/3/4") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() assertThat(comparable).hasStructureThatMatches { hasInvertedPropertyThat().isFalse() @@ -725,7 +725,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_multiplicationsAndDivisions_returnsCombinedProduct() { val expression = parseNumericExpression("2*3/4/5*6") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() assertThat(comparable).hasStructureThatMatches { hasInvertedPropertyThat().isFalse() @@ -769,7 +769,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_multiplicationsWithNestedMults_returnsCompletelyCombinedProduct() { val expression = parseNumericExpression("2*((3*(4*5)*6)*7)") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() assertThat(comparable).hasStructureThatMatches { hasInvertedPropertyThat().isFalse() @@ -819,7 +819,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_dividesWithNesting_returnsProductWithDistributedInversion() { val expression = parseNumericExpression("2/(3*4/5)") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() // Both the 3 & 5 become inverted, and the 5 becomes regular multiplication due to the division // distribution. @@ -860,7 +860,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_divisionsWithNestedDivs_returnsCompletelyCombinedProduct() { val expression = parseNumericExpression("2/((3/(4/5)/6)/7)") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() // Some of these are non-inverted because of distribution. assertThat(comparable).hasStructureThatMatches { @@ -911,7 +911,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_multiplicationsAndDivisionsWithNested_returnsCombinedProduct() { val expression = parseNumericExpression("1*(2/3)/(4*5*(2*3/1))") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() assertThat(comparable).hasStructureThatMatches { hasInvertedPropertyThat().isFalse() @@ -973,7 +973,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_multiplicationWithNoNegatives_returnsPositiveProduct() { val expression = parseNumericExpression("2*3") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() assertThat(comparable).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() @@ -996,7 +996,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_multiplicationWithOneNegative_returnsNegativeProduct() { val expression = parseNumericExpression("2*-3") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() // The entire accumulation is considered negative. assertThat(comparable).hasStructureThatMatches { @@ -1026,7 +1026,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_multiplicationWithTwoNegatives_returnsPositiveProduct() { val expression = parseNumericExpression("-2*-3") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() // The two negatives cancel out. This also verifies that negation can pipe up to top-level // negation. @@ -1057,7 +1057,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_multiplicationWithThreeNegatives_returnsNegativeProduct() { val expression = parseNumericExpression("-2*-3*-4") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() // 3 negative operands results in the overall product being negative. assertThat(comparable).hasStructureThatMatches { @@ -1094,7 +1094,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_combinedMultDivWithNested_evenNegatives_returnsPositiveProduct() { val expression = parseNumericExpression("-2*-3/-(4/-(3*2))") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() // There are four negatives, so the overall expression is positive. assertThat(comparable).hasStructureThatMatches { @@ -1108,7 +1108,7 @@ class ExpressionToComparableOperationConverterTest { val expression = parseNumericExpression("-2*-3/-(4/-(3*2*+(-3*7)))", errorCheckingMode = REQUIRED_ONLY) - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() // There are five negatives, so the overall expression is negative. Note that this is also // verifying that the negation properly distributes with the group. @@ -1127,7 +1127,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_additionAndExp_returnsSummationWithNonCommutative() { val expression = parseNumericExpression("1+2^3") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() assertThat(comparable).hasStructureThatMatches { commutativeAccumulationWithType(SUMMATION) { @@ -1161,7 +1161,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_additionAndSquareRoot_returnsSummationWithNonCommutative() { val expression = parseNumericExpression("1+sqrt(2)") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() assertThat(comparable).hasStructureThatMatches { commutativeAccumulationWithType(SUMMATION) { @@ -1188,7 +1188,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_additionWithinExp_returnsSummationWithinNonCommutative() { val expression = parseNumericExpression("2^(1+3)") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() assertThat(comparable).hasStructureThatMatches { nonCommutativeOperation { @@ -1222,7 +1222,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_additionWithinSquareRoot_returnsSummationWithinNonCommutative() { val expression = parseNumericExpression("sqrt(1+3)") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() assertThat(comparable).hasStructureThatMatches { nonCommutativeOperation { @@ -1249,7 +1249,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_multiplicationAndExp_returnsProductWithNonCommutative() { val expression = parseNumericExpression("2*3^4") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() assertThat(comparable).hasStructureThatMatches { commutativeAccumulationWithType(PRODUCT) { @@ -1283,7 +1283,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_multiplicationAndSquareRoot_returnsProductWithNonCommutative() { val expression = parseNumericExpression("2*sqrt(3)") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() assertThat(comparable).hasStructureThatMatches { commutativeAccumulationWithType(PRODUCT) { @@ -1310,7 +1310,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_multiplicationWithinExp_returnsProductWithinNonCommutative() { val expression = parseNumericExpression("2^(3*4)") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() assertThat(comparable).hasStructureThatMatches { nonCommutativeOperation { @@ -1344,7 +1344,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_multiplicationWithinSquareRoot_returnsProductWithinNonCommutative() { val expression = parseNumericExpression("sqrt(2*3)") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() assertThat(comparable).hasStructureThatMatches { nonCommutativeOperation { @@ -1371,7 +1371,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_additionAndMultiplication_returnsSummationOfProduct() { val expression = parseNumericExpression("2*3+1") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() assertThat(comparable).hasStructureThatMatches { commutativeAccumulationWithType(SUMMATION) { @@ -1404,7 +1404,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_multiplicationAndGroupedAddition_returnsProductOfSummation() { val expression = parseNumericExpression("2*(3+1)") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() assertThat(comparable).hasStructureThatMatches { commutativeAccumulationWithType(PRODUCT) { @@ -1454,7 +1454,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_additionAndNonCommutativeOp_samePrecedence_returnsOpWithSummationFirst() { val expression = parseNumericExpression("$op1 * $op2") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() // Verify that the summation is still first since it's higher priority during sorting. assertThat(comparable).hasStructureThatMatches { @@ -1480,7 +1480,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_constantAndNonCommutativeOp_samePrecedence_returnsOpWithNonCommutativeFirst() { val expression = parseNumericExpression("$op1 + $op2") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() // Verify that the non-commutative operation is first since it's higher priority during sorting. assertThat(comparable).hasStructureThatMatches { @@ -1504,7 +1504,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_constantAndVariable_samePrecedence_returnsOpWithConstantFirst() { val expression = parseAlgebraicExpression("$op1 * $op2") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() // Verify that the constant is first since it's higher priority during sorting. assertThat(comparable).hasStructureThatMatches { @@ -1528,7 +1528,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_positiveAndNegativeVariables_returnsOpWithNegatedLast() { val expression = parseAlgebraicExpression("$op1 + $op2") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() // Verify that the positive term is first since it's higher priority during sorting. assertThat(comparable).hasStructureThatMatches { @@ -1554,7 +1554,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_invertedAndNonInvertedVariables_returnsOpWithInvertedLast() { val expression = parseAlgebraicExpression("$op1 * $op2") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() // Verify that the non-inverted term is first since it's higher priority during sorting. assertThat(comparable).hasStructureThatMatches { @@ -1590,7 +1590,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_twoAdditionsInProduct_smallerSumIsFirst() { val expression = parseNumericExpression("($op1)*($op2)") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() // Summations are deterministically sorted regardless of how the original expression structures // them. @@ -1635,7 +1635,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_twoMultiplicationsInSum_smallerProductIsFirst() { val expression = parseNumericExpression("($op1)+($op2)") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() // Products are deterministically sorted regardless of how the original expression structures // them. @@ -1676,7 +1676,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_expAndSqrt_samePrecedence_returnsOpWithExpThenSqrt() { val expression = parseNumericExpression("$op1+$op2") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() // Verify that the exponentiation is first since it's higher priority during sorting. assertThat(comparable).hasStructureThatMatches { @@ -1719,7 +1719,7 @@ class ExpressionToComparableOperationConverterTest { val expression = parseAlgebraicExpression("($op1)+($op2)", errorCheckingMode = REQUIRED_ONLY) - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() // Verify that the exponentiations are ordered based on the left-hand operand's size. assertThat(comparable).hasStructureThatMatches { @@ -1778,7 +1778,7 @@ class ExpressionToComparableOperationConverterTest { errorCheckingMode = REQUIRED_ONLY ) - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() // Verify that the exponentiations are ordered based on the left-hand operand's lexicographical // ordering. @@ -1819,7 +1819,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_addTwoSqrts_leftConst_rightConst_returnsOpWithSqrtsByArgSize() { val expression = parseNumericExpression("sqrt($op1)+sqrt($op2)") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() // The square roots should be ordered based on their argument sorting. assertThat(comparable).hasStructureThatMatches { @@ -1855,7 +1855,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_addTwoSqrts_leftVar_rightVar_returnsOpWithSqrtsByVariableOrder() { val expression = parseAlgebraicExpression("sqrt($op1)+sqrt($op2)") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() // The square roots should be ordered based on their argument lexicographical sorting. assertThat(comparable).hasStructureThatMatches { @@ -1891,7 +1891,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_addTwoSqrts_oneConst_oneVar_returnsOpWithSqrtsByConstFirst() { val expression = parseAlgebraicExpression("sqrt($op1)+sqrt($op2)") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() // Constant-before-variable ordering also affects peer square root orders. assertThat(comparable).hasStructureThatMatches { @@ -1929,7 +1929,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_addTwoConstants_leftInteger_rightInteger_returnsOpSortedByValues() { val expression = parseNumericExpression("$op1 + $op2") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() // The order of the summation should be based on the constants' values. assertThat(comparable).hasStructureThatMatches { @@ -1957,7 +1957,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_addTwoConstants_leftDouble_rightDouble_returnsOpSortedByValues() { val expression = parseNumericExpression("$op1 + $op2") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() // The order of the summation should be based on the constants' values. assertThat(comparable).hasStructureThatMatches { @@ -1985,7 +1985,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_addTwoConstants_smallInt_largeDouble_returnsOpWithIntFirst() { val expression = parseNumericExpression("$op1 + $op2") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() // The order of the summation should be based on the constants' values. assertThat(comparable).hasStructureThatMatches { @@ -2013,7 +2013,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_addTwoConstants_largeInt_smallDouble_returnsOpWithDoubleFirst() { val expression = parseNumericExpression("$op1 + $op2") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() // The order of the summation should be based on the constants' values. assertThat(comparable).hasStructureThatMatches { @@ -2041,7 +2041,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_addVarAndIntConstant_returnsOpWithConstantFirst() { val expression = parseAlgebraicExpression("$op1 + $op2") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() // Constants are always ordered before variables. assertThat(comparable).hasStructureThatMatches { @@ -2069,7 +2069,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_addVarAndDoubleConstant_returnsOpWithConstantFirst() { val expression = parseAlgebraicExpression("$op1 + $op2") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() // The order of the summation should be based on the constants' values. assertThat(comparable).hasStructureThatMatches { @@ -2093,7 +2093,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_addTwoVariables_leftX_rightX_returnsOpBothXs() { val expression = parseAlgebraicExpression("x + x") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() assertThat(comparable).hasStructureThatMatches { commutativeAccumulationWithType(SUMMATION) { @@ -2120,7 +2120,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_addTwoVariables_oneX_oneY_returnsOpWithXThenY() { val expression = parseAlgebraicExpression("$op1 + $op2") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() // Variables are sorted lexicographically. assertThat(comparable).hasStructureThatMatches { @@ -2144,7 +2144,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_addMultipleVars_returnsOpWithThemInOrder() { val expression = parseAlgebraicExpression("x + z + x + y + x") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() // Variables are sorted lexicographically. assertThat(comparable).hasStructureThatMatches { @@ -2185,7 +2185,7 @@ class ExpressionToComparableOperationConverterTest { fun testConvert_allOperations_withNestedGroups_returnsCorrectlyStructuredAndOrderedOperation() { val expression = parseAlgebraicExpression("√(1+2*3)+-2^3*4/7-(2yx+x^(2+1)*(17/3))/-(x+(y+1.2))") - val comparable = expression.toComparableOperation() + val comparable = expression.convertToComparableOperation() assertThat(comparable).hasStructureThatMatches { hasNegatedPropertyThat().isFalse() @@ -2445,40 +2445,40 @@ class ExpressionToComparableOperationConverterTest { @Test fun testEquals_additionOps_differentByCommutativity_areEqual() { - val comparable1 = parseNumericExpression("1 + 2").toComparableOperation() - val comparable2 = parseNumericExpression("2 + 1").toComparableOperation() + val comparable1 = parseNumericExpression("1 + 2").convertToComparableOperation() + val comparable2 = parseNumericExpression("2 + 1").convertToComparableOperation() assertThat(comparable1).isEqualTo(comparable2) } @Test fun testEquals_additionOps_differentByAssociativity_areEqual() { - val comparable1 = parseNumericExpression("1 + (2 + 3)").toComparableOperation() - val comparable2 = parseNumericExpression("(1 + 2) + 3").toComparableOperation() + val comparable1 = parseNumericExpression("1 + (2 + 3)").convertToComparableOperation() + val comparable2 = parseNumericExpression("(1 + 2) + 3").convertToComparableOperation() assertThat(comparable1).isEqualTo(comparable2) } @Test fun testEquals_additionOps_differentByAssociativityAndCommutativity_areEqual() { - val comparable1 = parseNumericExpression("1 + (2 + 3)").toComparableOperation() - val comparable2 = parseNumericExpression("(2 + 1) + 3").toComparableOperation() + val comparable1 = parseNumericExpression("1 + (2 + 3)").convertToComparableOperation() + val comparable2 = parseNumericExpression("(2 + 1) + 3").convertToComparableOperation() assertThat(comparable1).isEqualTo(comparable2) } @Test fun testEquals_additionOps_differentByValue_areNotEqual() { - val comparable1 = parseNumericExpression("1 + 2").toComparableOperation() - val comparable2 = parseNumericExpression("1 + 3").toComparableOperation() + val comparable1 = parseNumericExpression("1 + 2").convertToComparableOperation() + val comparable2 = parseNumericExpression("1 + 3").convertToComparableOperation() assertThat(comparable1).isNotEqualTo(comparable2) } @Test fun testEquals_additionOps_sameOnlyByEvaluation_areNotEqual() { - val comparable1 = parseNumericExpression("1 + 2 + 2 + 1").toComparableOperation() - val comparable2 = parseNumericExpression("1 + 2 + 3").toComparableOperation() + val comparable1 = parseNumericExpression("1 + 2 + 2 + 1").convertToComparableOperation() + val comparable2 = parseNumericExpression("1 + 2 + 3").convertToComparableOperation() // While the two expressions are numerically equivalent, they aren't comparable since there are // extra terms in one (more than trivial rearranging is required to determine that they're @@ -2488,40 +2488,40 @@ class ExpressionToComparableOperationConverterTest { @Test fun testEquals_multiplicationOps_differentByCommutativity_areEqual() { - val comparable1 = parseNumericExpression("2 * 3").toComparableOperation() - val comparable2 = parseNumericExpression("3 * 2").toComparableOperation() + val comparable1 = parseNumericExpression("2 * 3").convertToComparableOperation() + val comparable2 = parseNumericExpression("3 * 2").convertToComparableOperation() assertThat(comparable1).isEqualTo(comparable2) } @Test fun testEquals_multiplicationOps_differentByAssociativity_areEqual() { - val comparable1 = parseNumericExpression("2 * (3 * 4)").toComparableOperation() - val comparable2 = parseNumericExpression("(2 * 3) * 4").toComparableOperation() + val comparable1 = parseNumericExpression("2 * (3 * 4)").convertToComparableOperation() + val comparable2 = parseNumericExpression("(2 * 3) * 4").convertToComparableOperation() assertThat(comparable1).isEqualTo(comparable2) } @Test fun testEquals_multiplicationOps_differentByAssociativityAndCommutativity_areEqual() { - val comparable1 = parseNumericExpression("2 * (3 * 4)").toComparableOperation() - val comparable2 = parseNumericExpression("(3 * 2) * 4").toComparableOperation() + val comparable1 = parseNumericExpression("2 * (3 * 4)").convertToComparableOperation() + val comparable2 = parseNumericExpression("(3 * 2) * 4").convertToComparableOperation() assertThat(comparable1).isEqualTo(comparable2) } @Test fun testEquals_multiplicationOps_differentByValue_areNotEqual() { - val comparable1 = parseNumericExpression("2 * 3").toComparableOperation() - val comparable2 = parseNumericExpression("2 * 4").toComparableOperation() + val comparable1 = parseNumericExpression("2 * 3").convertToComparableOperation() + val comparable2 = parseNumericExpression("2 * 4").convertToComparableOperation() assertThat(comparable1).isNotEqualTo(comparable2) } @Test fun testEquals_multiplicationOps_sameOnlyByEvaluation_areNotEqual() { - val comparable1 = parseNumericExpression("2 * 3 * 4").toComparableOperation() - val comparable2 = parseNumericExpression("2 * 2 * 2 * 3").toComparableOperation() + val comparable1 = parseNumericExpression("2 * 3 * 4").convertToComparableOperation() + val comparable2 = parseNumericExpression("2 * 2 * 2 * 3").convertToComparableOperation() // While the two expressions are numerically equivalent, they aren't comparable since there are // extra terms in one (more than trivial rearranging is required to determine that they're @@ -2531,16 +2531,16 @@ class ExpressionToComparableOperationConverterTest { @Test fun testEquals_subtractionOps_same_areEqual() { - val comparable1 = parseNumericExpression("1 - 2").toComparableOperation() - val comparable2 = parseNumericExpression("1 - 2").toComparableOperation() + val comparable1 = parseNumericExpression("1 - 2").convertToComparableOperation() + val comparable2 = parseNumericExpression("1 - 2").convertToComparableOperation() assertThat(comparable1).isEqualTo(comparable2) } @Test fun testEquals_subtractionOps_differentByOrder_areNotEqual() { - val comparable1 = parseNumericExpression("1 - 2").toComparableOperation() - val comparable2 = parseNumericExpression("2 - 1").toComparableOperation() + val comparable1 = parseNumericExpression("1 - 2").convertToComparableOperation() + val comparable2 = parseNumericExpression("2 - 1").convertToComparableOperation() // Subtraction is not commutative. assertThat(comparable1).isNotEqualTo(comparable2) @@ -2548,8 +2548,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun testEquals_subtractionOps_differentByAssociativity_areNotEqual() { - val comparable1 = parseNumericExpression("1 - (2 - 3)").toComparableOperation() - val comparable2 = parseNumericExpression("(1 - 2) - 3").toComparableOperation() + val comparable1 = parseNumericExpression("1 - (2 - 3)").convertToComparableOperation() + val comparable2 = parseNumericExpression("(1 - 2) - 3").convertToComparableOperation() // Subtraction is not associative. assertThat(comparable1).isNotEqualTo(comparable2) @@ -2557,8 +2557,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun testEquals_subtractionOps_sameOnlyByEvaluation_areNotEqual() { - val comparable1 = parseNumericExpression("1 - 2 - 3").toComparableOperation() - val comparable2 = parseNumericExpression("1 - 2 - 2 - 1").toComparableOperation() + val comparable1 = parseNumericExpression("1 - 2 - 3").convertToComparableOperation() + val comparable2 = parseNumericExpression("1 - 2 - 2 - 1").convertToComparableOperation() // While the two expressions are numerically equivalent, they aren't comparable since there are // extra terms in one (more than trivial rearranging is required to determine that they're @@ -2568,16 +2568,16 @@ class ExpressionToComparableOperationConverterTest { @Test fun testEquals_divisionOps_same_areEqual() { - val comparable1 = parseNumericExpression("2 / 3").toComparableOperation() - val comparable2 = parseNumericExpression("2 / 3").toComparableOperation() + val comparable1 = parseNumericExpression("2 / 3").convertToComparableOperation() + val comparable2 = parseNumericExpression("2 / 3").convertToComparableOperation() assertThat(comparable1).isEqualTo(comparable2) } @Test fun testEquals_divisionOps_differentByOrder_areNotEqual() { - val comparable1 = parseNumericExpression("2 / 3").toComparableOperation() - val comparable2 = parseNumericExpression("3 / 2").toComparableOperation() + val comparable1 = parseNumericExpression("2 / 3").convertToComparableOperation() + val comparable2 = parseNumericExpression("3 / 2").convertToComparableOperation() // Division is not commutative. assertThat(comparable1).isNotEqualTo(comparable2) @@ -2585,8 +2585,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun testEquals_divisionOps_differentByAssociativity_areNotEqual() { - val comparable1 = parseNumericExpression("2 / (3 / 4)").toComparableOperation() - val comparable2 = parseNumericExpression("(2 / 3) / 4").toComparableOperation() + val comparable1 = parseNumericExpression("2 / (3 / 4)").convertToComparableOperation() + val comparable2 = parseNumericExpression("(2 / 3) / 4").convertToComparableOperation() // Division is not associative. assertThat(comparable1).isNotEqualTo(comparable2) @@ -2594,8 +2594,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun testEquals_divisionOps_sameOnlyByEvaluation_areNotEqual() { - val comparable1 = parseNumericExpression("2 / 3 / 4").toComparableOperation() - val comparable2 = parseNumericExpression("2 / 3 / 2 / 2").toComparableOperation() + val comparable1 = parseNumericExpression("2 / 3 / 4").convertToComparableOperation() + val comparable2 = parseNumericExpression("2 / 3 / 2 / 2").convertToComparableOperation() // While the two expressions are numerically equivalent, they aren't comparable since there are // extra terms in one (more than trivial rearranging is required to determine that they're @@ -2605,16 +2605,16 @@ class ExpressionToComparableOperationConverterTest { @Test fun testEquals_exponentiationOps_same_areEqual() { - val comparable1 = parseNumericExpression("2 ^ 3").toComparableOperation() - val comparable2 = parseNumericExpression("2 ^ 3").toComparableOperation() + val comparable1 = parseNumericExpression("2 ^ 3").convertToComparableOperation() + val comparable2 = parseNumericExpression("2 ^ 3").convertToComparableOperation() assertThat(comparable1).isEqualTo(comparable2) } @Test fun testEquals_exponentiationOps_differentByOrder_areNotEqual() { - val comparable1 = parseNumericExpression("2 ^ 3").toComparableOperation() - val comparable2 = parseNumericExpression("3 ^ 2").toComparableOperation() + val comparable1 = parseNumericExpression("2 ^ 3").convertToComparableOperation() + val comparable2 = parseNumericExpression("3 ^ 2").convertToComparableOperation() // Exponentiation is not commutative. assertThat(comparable1).isNotEqualTo(comparable2) @@ -2626,11 +2626,11 @@ class ExpressionToComparableOperationConverterTest { val comparable1 = parseNumericExpression( "2 ^ (3 ^ 4)", errorCheckingMode = REQUIRED_ONLY - ).toComparableOperation() + ).convertToComparableOperation() val comparable2 = parseNumericExpression( "(2 ^ 3) ^ 4", errorCheckingMode = REQUIRED_ONLY - ).toComparableOperation() + ).convertToComparableOperation() // Exponentiation is not associative. assertThat(comparable1).isNotEqualTo(comparable2) @@ -2638,8 +2638,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun testEquals_exponentiationOps_differentByValue_areNotEqual() { - val comparable1 = parseNumericExpression("2 ^ 3").toComparableOperation() - val comparable2 = parseNumericExpression("2 ^ 4").toComparableOperation() + val comparable1 = parseNumericExpression("2 ^ 3").convertToComparableOperation() + val comparable2 = parseNumericExpression("2 ^ 4").convertToComparableOperation() assertThat(comparable1).isNotEqualTo(comparable2) } @@ -2647,9 +2647,9 @@ class ExpressionToComparableOperationConverterTest { @Test fun testEquals_exponentiationOps_sameOnlyByEvaluation_areNotEqual() { // Disable optional errors to allow nested exponentiation. - val comparable1 = parseNumericExpression("2 ^ 4").toComparableOperation() + val comparable1 = parseNumericExpression("2 ^ 4").convertToComparableOperation() val comparable2 = - parseNumericExpression("2 ^ 2 ^ 2", errorCheckingMode = REQUIRED_ONLY).toComparableOperation() + parseNumericExpression("2 ^ 2 ^ 2", errorCheckingMode = REQUIRED_ONLY).convertToComparableOperation() // While the two expressions are numerically equivalent, they aren't comparable since there are // extra terms in one (more than trivial rearranging is required to determine that they're @@ -2659,24 +2659,24 @@ class ExpressionToComparableOperationConverterTest { @Test fun testEquals_squareRootOps_same_areEqual() { - val comparable1 = parseNumericExpression("sqrt(2)").toComparableOperation() - val comparable2 = parseNumericExpression("sqrt(2)").toComparableOperation() + val comparable1 = parseNumericExpression("sqrt(2)").convertToComparableOperation() + val comparable2 = parseNumericExpression("sqrt(2)").convertToComparableOperation() assertThat(comparable1).isEqualTo(comparable2) } @Test fun testEquals_squareRootOps_differentByValue_areNotEqual() { - val comparable1 = parseNumericExpression("sqrt(2)").toComparableOperation() - val comparable2 = parseNumericExpression("sqrt(3)").toComparableOperation() + val comparable1 = parseNumericExpression("sqrt(2)").convertToComparableOperation() + val comparable2 = parseNumericExpression("sqrt(3)").convertToComparableOperation() assertThat(comparable1).isNotEqualTo(comparable2) } @Test fun testEquals_squareRootOps_sameOnlyByEvaluation_areNotEqual() { - val comparable1 = parseNumericExpression("sqrt(2)").toComparableOperation() - val comparable2 = parseNumericExpression("sqrt(1 + 1)").toComparableOperation() + val comparable1 = parseNumericExpression("sqrt(2)").convertToComparableOperation() + val comparable2 = parseNumericExpression("sqrt(1 + 1)").convertToComparableOperation() // While the two expressions are numerically equivalent, they aren't comparable since there are // extra terms in one (more than trivial rearranging is required to determine that they're @@ -2686,40 +2686,40 @@ class ExpressionToComparableOperationConverterTest { @Test fun testEquals_additionsAndSubtractions_differentByOrder_areEqual() { - val comparable1 = parseNumericExpression("1+2-3").toComparableOperation() - val comparable2 = parseNumericExpression("-3+2+1").toComparableOperation() + val comparable1 = parseNumericExpression("1+2-3").convertToComparableOperation() + val comparable2 = parseNumericExpression("-3+2+1").convertToComparableOperation() assertThat(comparable1).isEqualTo(comparable2) } @Test fun testEquals_multiplicationsAndDivisions_differentByOrder_areEqual() { - val comparable1 = parseNumericExpression("2*3/4*7").toComparableOperation() - val comparable2 = parseNumericExpression("7*2*3/4").toComparableOperation() + val comparable1 = parseNumericExpression("2*3/4*7").convertToComparableOperation() + val comparable2 = parseNumericExpression("7*2*3/4").convertToComparableOperation() assertThat(comparable1).isEqualTo(comparable2) } @Test fun testEquals_allAccumulationOperations_differentByOrder_areEqual() { - val comparable1 = parseNumericExpression("1+2*3/4*7-8+3").toComparableOperation() - val comparable2 = parseNumericExpression("-8+3+7*3/4*2+1").toComparableOperation() + val comparable1 = parseNumericExpression("1+2*3/4*7-8+3").convertToComparableOperation() + val comparable2 = parseNumericExpression("-8+3+7*3/4*2+1").convertToComparableOperation() assertThat(comparable1).isEqualTo(comparable2) } @Test fun testEquals_allOperations_differentByOrder_areEqual() { - val comparable1 = parseNumericExpression("sqrt(1+2*3)*2^3/7-(2-2*3)").toComparableOperation() - val comparable2 = parseNumericExpression("2^3*sqrt(3*2+1)/7-(-3*2+2)").toComparableOperation() + val comparable1 = parseNumericExpression("sqrt(1+2*3)*2^3/7-(2-2*3)").convertToComparableOperation() + val comparable2 = parseNumericExpression("2^3*sqrt(3*2+1)/7-(-3*2+2)").convertToComparableOperation() assertThat(comparable1).isEqualTo(comparable2) } @Test fun testEquals_allOperations_oneNestedDifferentByValue_areNotEqual() { - val comparable1 = parseNumericExpression("sqrt(1+2*3)*2^3/7-(2-2*3)").toComparableOperation() - val comparable2 = parseNumericExpression("sqrt(1+2*3)*2^3/7-(2-4*3)").toComparableOperation() + val comparable1 = parseNumericExpression("sqrt(1+2*3)*2^3/7-(2-2*3)").convertToComparableOperation() + val comparable2 = parseNumericExpression("sqrt(1+2*3)*2^3/7-(2-4*3)").convertToComparableOperation() // Just one different term leads to the entire comparison failing. assertThat(comparable1).isNotEqualTo(comparable2) @@ -2727,8 +2727,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun testEquals_twoOps_allOperations_oneMissingTerm_areNotEqual() { - val comparable1 = parseNumericExpression("sqrt(1+2*3)*2^3/7-(2-2*3)").toComparableOperation() - val comparable2 = parseNumericExpression("sqrt(1+2*3)*2^3/7-(2-2)").toComparableOperation() + val comparable1 = parseNumericExpression("sqrt(1+2*3)*2^3/7-(2-2*3)").convertToComparableOperation() + val comparable2 = parseNumericExpression("sqrt(1+2*3)*2^3/7-(2-2)").convertToComparableOperation() // Just one missing term leads to the entire comparison failing. assertThat(comparable1).isNotEqualTo(comparable2) diff --git a/utility/src/test/java/org/oppia/android/util/math/MathExpressionExtensionsTest.kt b/utility/src/test/java/org/oppia/android/util/math/MathExpressionExtensionsTest.kt index 586a7253c9d..85a734a7ac4 100644 --- a/utility/src/test/java/org/oppia/android/util/math/MathExpressionExtensionsTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/MathExpressionExtensionsTest.kt @@ -78,8 +78,8 @@ class MathExpressionExtensionsTest { val expression1 = parseAlgebraicExpression("x+2/x-(-7*8-9)+sqrt((x+2)+1)+3xy^2") val expression2 = parseAlgebraicExpression("sqrt(x+(1+2))+2/x+x-(-9+8*-7-3y^2x)") - val operation1 = expression1.toComparable() - val operation2 = expression2.toComparable() + val operation1 = expression1.toComparableOperation() + val operation2 = expression2.toComparableOperation() assertThat(operation1).isEqualTo(operation2) } @@ -89,8 +89,8 @@ class MathExpressionExtensionsTest { val expression1 = parseAlgebraicExpression("x+2/x-(-7*8-9)+sqrt((x+2)+1)+3xy^2") val expression2 = parseAlgebraicExpression("sqrt(x+(1+3))+2/x+x-(-9+8*-7-3y^2x)") - val operation1 = expression1.toComparable() - val operation2 = expression2.toComparable() + val operation1 = expression1.toComparableOperation() + val operation2 = expression2.toComparableOperation() assertThat(operation1).isNotEqualTo(operation2) } From 1970f347efe4ed65ff5f7ef820a5585d2d28f91d Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 28 Jan 2022 20:51:52 -0800 Subject: [PATCH 089/162] Renames & lint fixes. --- .../android/util/math/ComparatorExtensions.kt | 2 +- ...xpressionToComparableOperationConverter.kt | 11 +- ...ssionToComparableOperationConverterTest.kt | 149 +++++++++--------- 3 files changed, 84 insertions(+), 78 deletions(-) diff --git a/utility/src/main/java/org/oppia/android/util/math/ComparatorExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/ComparatorExtensions.kt index 4e4355d0ec0..e853a03bfb9 100644 --- a/utility/src/main/java/org/oppia/android/util/math/ComparatorExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/ComparatorExtensions.kt @@ -39,7 +39,7 @@ fun Comparator.compareIterables(first: Iterable, second: Iterable): * [Comparator] isn't used). This short-circuiting behavior can be useful when comparing recursively * infinite proto structures to avoid stack overflows.. */ -fun Comparator.compareProtos(left: T, right: T): Int { +fun Comparator.compareProtos(left: T, right: T): Int { val defaultValue = left.defaultInstanceForType val leftIsDefault = left == defaultValue val rightIsDefault = right == defaultValue diff --git a/utility/src/main/java/org/oppia/android/util/math/ExpressionToComparableOperationConverter.kt b/utility/src/main/java/org/oppia/android/util/math/ExpressionToComparableOperationConverter.kt index 5983f09e27d..429ffe85828 100644 --- a/utility/src/main/java/org/oppia/android/util/math/ExpressionToComparableOperationConverter.kt +++ b/utility/src/main/java/org/oppia/android/util/math/ExpressionToComparableOperationConverter.kt @@ -6,7 +6,6 @@ import org.oppia.android.app.model.ComparableOperation.CommutativeAccumulation.A import org.oppia.android.app.model.ComparableOperation.CommutativeAccumulation.AccumulationType.SUMMATION import org.oppia.android.app.model.ComparableOperation.NonCommutativeOperation import org.oppia.android.app.model.ComparableOperation.NonCommutativeOperation.BinaryOperation -import org.oppia.android.app.model.MathBinaryOperation.Operator as BinaryOperator import org.oppia.android.app.model.MathBinaryOperation.Operator.ADD import org.oppia.android.app.model.MathBinaryOperation.Operator.DIVIDE import org.oppia.android.app.model.MathBinaryOperation.Operator.EXPONENTIATE @@ -22,15 +21,16 @@ import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.UNARY_OPERA import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.VARIABLE import org.oppia.android.app.model.MathFunctionCall.FunctionType import org.oppia.android.app.model.MathFunctionCall.FunctionType.SQUARE_ROOT -import org.oppia.android.app.model.MathUnaryOperation.Operator as UnaryOperator import org.oppia.android.app.model.MathUnaryOperation.Operator.NEGATE import org.oppia.android.app.model.MathUnaryOperation.Operator.POSITIVE +import org.oppia.android.app.model.MathBinaryOperation.Operator as BinaryOperator +import org.oppia.android.app.model.MathUnaryOperation.Operator as UnaryOperator /** * Converter from [MathExpression] to [ComparableOperation]. * - * See the separate proto details for context, and [convertToComparableOperation] for the actual conversion - * function. + * See the separate proto details for context, and [convertToComparableOperation] for the actual + * conversion function. */ class ExpressionToComparableOperationConverter private constructor() { companion object { @@ -139,7 +139,8 @@ class ExpressionToComparableOperationConverter private constructor() { addOperationToSum(expression.unaryOperation.operand, forceNegative) // Skip groups so that nested operations can be properly combined. expression.expressionTypeCase == GROUP -> addOperationToSum(expression.group, forceNegative) - forceNegative -> addCombinedOperations(expression.convertToComparableOperation().invertNegation()) + forceNegative -> + addCombinedOperations(expression.convertToComparableOperation().invertNegation()) else -> addCombinedOperations(expression.convertToComparableOperation()) } } diff --git a/utility/src/test/java/org/oppia/android/util/math/ExpressionToComparableOperationConverterTest.kt b/utility/src/test/java/org/oppia/android/util/math/ExpressionToComparableOperationConverterTest.kt index 1452a9c70bd..4af4b5afaa9 100644 --- a/utility/src/test/java/org/oppia/android/util/math/ExpressionToComparableOperationConverterTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/ExpressionToComparableOperationConverterTest.kt @@ -2,8 +2,8 @@ package org.oppia.android.util.math import com.google.common.truth.Truth.assertThat import org.junit.Test -import org.oppia.android.util.math.ExpressionToComparableOperationConverter.Companion.convertToComparableOperation import org.junit.runner.RunWith +import org.oppia.android.app.model.ComparableOperation import org.oppia.android.app.model.ComparableOperation.CommutativeAccumulation.AccumulationType.PRODUCT import org.oppia.android.app.model.ComparableOperation.CommutativeAccumulation.AccumulationType.SUMMATION import org.oppia.android.app.model.MathExpression @@ -12,6 +12,7 @@ import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Iteration import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Parameter import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.RunParameterized import org.oppia.android.testing.math.ComparableOperationSubject.Companion.assertThat +import org.oppia.android.util.math.ExpressionToComparableOperationConverter.Companion.convertToComparableOperation import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode.ALL_ERRORS import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode.REQUIRED_ONLY @@ -1665,7 +1666,7 @@ class ExpressionToComparableOperationConverterTest { } } } - + /* Non-commutative sorting */ @Test @@ -2439,46 +2440,46 @@ class ExpressionToComparableOperationConverterTest { * reliable equivalence checking (and may instead require threshold checking for approximated * equivalence). * - * Further, these checks are using vanilla equivalence checking since they rely on the opreations + * Further, these checks are using vanilla equivalence checking since they rely on the operations * being properly sorted. */ @Test fun testEquals_additionOps_differentByCommutativity_areEqual() { - val comparable1 = parseNumericExpression("1 + 2").convertToComparableOperation() - val comparable2 = parseNumericExpression("2 + 1").convertToComparableOperation() + val comparable1 = parseNumericExpressionAsComparableOperation("1 + 2") + val comparable2 = parseNumericExpressionAsComparableOperation("2 + 1") assertThat(comparable1).isEqualTo(comparable2) } @Test fun testEquals_additionOps_differentByAssociativity_areEqual() { - val comparable1 = parseNumericExpression("1 + (2 + 3)").convertToComparableOperation() - val comparable2 = parseNumericExpression("(1 + 2) + 3").convertToComparableOperation() + val comparable1 = parseNumericExpressionAsComparableOperation("1 + (2 + 3)") + val comparable2 = parseNumericExpressionAsComparableOperation("(1 + 2) + 3") assertThat(comparable1).isEqualTo(comparable2) } @Test fun testEquals_additionOps_differentByAssociativityAndCommutativity_areEqual() { - val comparable1 = parseNumericExpression("1 + (2 + 3)").convertToComparableOperation() - val comparable2 = parseNumericExpression("(2 + 1) + 3").convertToComparableOperation() + val comparable1 = parseNumericExpressionAsComparableOperation("1 + (2 + 3)") + val comparable2 = parseNumericExpressionAsComparableOperation("(2 + 1) + 3") assertThat(comparable1).isEqualTo(comparable2) } @Test fun testEquals_additionOps_differentByValue_areNotEqual() { - val comparable1 = parseNumericExpression("1 + 2").convertToComparableOperation() - val comparable2 = parseNumericExpression("1 + 3").convertToComparableOperation() + val comparable1 = parseNumericExpressionAsComparableOperation("1 + 2") + val comparable2 = parseNumericExpressionAsComparableOperation("1 + 3") assertThat(comparable1).isNotEqualTo(comparable2) } @Test fun testEquals_additionOps_sameOnlyByEvaluation_areNotEqual() { - val comparable1 = parseNumericExpression("1 + 2 + 2 + 1").convertToComparableOperation() - val comparable2 = parseNumericExpression("1 + 2 + 3").convertToComparableOperation() + val comparable1 = parseNumericExpressionAsComparableOperation("1 + 2 + 2 + 1") + val comparable2 = parseNumericExpressionAsComparableOperation("1 + 2 + 3") // While the two expressions are numerically equivalent, they aren't comparable since there are // extra terms in one (more than trivial rearranging is required to determine that they're @@ -2488,40 +2489,40 @@ class ExpressionToComparableOperationConverterTest { @Test fun testEquals_multiplicationOps_differentByCommutativity_areEqual() { - val comparable1 = parseNumericExpression("2 * 3").convertToComparableOperation() - val comparable2 = parseNumericExpression("3 * 2").convertToComparableOperation() + val comparable1 = parseNumericExpressionAsComparableOperation("2 * 3") + val comparable2 = parseNumericExpressionAsComparableOperation("3 * 2") assertThat(comparable1).isEqualTo(comparable2) } @Test fun testEquals_multiplicationOps_differentByAssociativity_areEqual() { - val comparable1 = parseNumericExpression("2 * (3 * 4)").convertToComparableOperation() - val comparable2 = parseNumericExpression("(2 * 3) * 4").convertToComparableOperation() + val comparable1 = parseNumericExpressionAsComparableOperation("2 * (3 * 4)") + val comparable2 = parseNumericExpressionAsComparableOperation("(2 * 3) * 4") assertThat(comparable1).isEqualTo(comparable2) } @Test fun testEquals_multiplicationOps_differentByAssociativityAndCommutativity_areEqual() { - val comparable1 = parseNumericExpression("2 * (3 * 4)").convertToComparableOperation() - val comparable2 = parseNumericExpression("(3 * 2) * 4").convertToComparableOperation() + val comparable1 = parseNumericExpressionAsComparableOperation("2 * (3 * 4)") + val comparable2 = parseNumericExpressionAsComparableOperation("(3 * 2) * 4") assertThat(comparable1).isEqualTo(comparable2) } @Test fun testEquals_multiplicationOps_differentByValue_areNotEqual() { - val comparable1 = parseNumericExpression("2 * 3").convertToComparableOperation() - val comparable2 = parseNumericExpression("2 * 4").convertToComparableOperation() + val comparable1 = parseNumericExpressionAsComparableOperation("2 * 3") + val comparable2 = parseNumericExpressionAsComparableOperation("2 * 4") assertThat(comparable1).isNotEqualTo(comparable2) } @Test fun testEquals_multiplicationOps_sameOnlyByEvaluation_areNotEqual() { - val comparable1 = parseNumericExpression("2 * 3 * 4").convertToComparableOperation() - val comparable2 = parseNumericExpression("2 * 2 * 2 * 3").convertToComparableOperation() + val comparable1 = parseNumericExpressionAsComparableOperation("2 * 3 * 4") + val comparable2 = parseNumericExpressionAsComparableOperation("2 * 2 * 2 * 3") // While the two expressions are numerically equivalent, they aren't comparable since there are // extra terms in one (more than trivial rearranging is required to determine that they're @@ -2531,16 +2532,16 @@ class ExpressionToComparableOperationConverterTest { @Test fun testEquals_subtractionOps_same_areEqual() { - val comparable1 = parseNumericExpression("1 - 2").convertToComparableOperation() - val comparable2 = parseNumericExpression("1 - 2").convertToComparableOperation() + val comparable1 = parseNumericExpressionAsComparableOperation("1 - 2") + val comparable2 = parseNumericExpressionAsComparableOperation("1 - 2") assertThat(comparable1).isEqualTo(comparable2) } @Test fun testEquals_subtractionOps_differentByOrder_areNotEqual() { - val comparable1 = parseNumericExpression("1 - 2").convertToComparableOperation() - val comparable2 = parseNumericExpression("2 - 1").convertToComparableOperation() + val comparable1 = parseNumericExpressionAsComparableOperation("1 - 2") + val comparable2 = parseNumericExpressionAsComparableOperation("2 - 1") // Subtraction is not commutative. assertThat(comparable1).isNotEqualTo(comparable2) @@ -2548,8 +2549,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun testEquals_subtractionOps_differentByAssociativity_areNotEqual() { - val comparable1 = parseNumericExpression("1 - (2 - 3)").convertToComparableOperation() - val comparable2 = parseNumericExpression("(1 - 2) - 3").convertToComparableOperation() + val comparable1 = parseNumericExpressionAsComparableOperation("1 - (2 - 3)") + val comparable2 = parseNumericExpressionAsComparableOperation("(1 - 2) - 3") // Subtraction is not associative. assertThat(comparable1).isNotEqualTo(comparable2) @@ -2557,8 +2558,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun testEquals_subtractionOps_sameOnlyByEvaluation_areNotEqual() { - val comparable1 = parseNumericExpression("1 - 2 - 3").convertToComparableOperation() - val comparable2 = parseNumericExpression("1 - 2 - 2 - 1").convertToComparableOperation() + val comparable1 = parseNumericExpressionAsComparableOperation("1 - 2 - 3") + val comparable2 = parseNumericExpressionAsComparableOperation("1 - 2 - 2 - 1") // While the two expressions are numerically equivalent, they aren't comparable since there are // extra terms in one (more than trivial rearranging is required to determine that they're @@ -2568,16 +2569,16 @@ class ExpressionToComparableOperationConverterTest { @Test fun testEquals_divisionOps_same_areEqual() { - val comparable1 = parseNumericExpression("2 / 3").convertToComparableOperation() - val comparable2 = parseNumericExpression("2 / 3").convertToComparableOperation() + val comparable1 = parseNumericExpressionAsComparableOperation("2 / 3") + val comparable2 = parseNumericExpressionAsComparableOperation("2 / 3") assertThat(comparable1).isEqualTo(comparable2) } @Test fun testEquals_divisionOps_differentByOrder_areNotEqual() { - val comparable1 = parseNumericExpression("2 / 3").convertToComparableOperation() - val comparable2 = parseNumericExpression("3 / 2").convertToComparableOperation() + val comparable1 = parseNumericExpressionAsComparableOperation("2 / 3") + val comparable2 = parseNumericExpressionAsComparableOperation("3 / 2") // Division is not commutative. assertThat(comparable1).isNotEqualTo(comparable2) @@ -2585,8 +2586,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun testEquals_divisionOps_differentByAssociativity_areNotEqual() { - val comparable1 = parseNumericExpression("2 / (3 / 4)").convertToComparableOperation() - val comparable2 = parseNumericExpression("(2 / 3) / 4").convertToComparableOperation() + val comparable1 = parseNumericExpressionAsComparableOperation("2 / (3 / 4)") + val comparable2 = parseNumericExpressionAsComparableOperation("(2 / 3) / 4") // Division is not associative. assertThat(comparable1).isNotEqualTo(comparable2) @@ -2594,8 +2595,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun testEquals_divisionOps_sameOnlyByEvaluation_areNotEqual() { - val comparable1 = parseNumericExpression("2 / 3 / 4").convertToComparableOperation() - val comparable2 = parseNumericExpression("2 / 3 / 2 / 2").convertToComparableOperation() + val comparable1 = parseNumericExpressionAsComparableOperation("2 / 3 / 4") + val comparable2 = parseNumericExpressionAsComparableOperation("2 / 3 / 2 / 2") // While the two expressions are numerically equivalent, they aren't comparable since there are // extra terms in one (more than trivial rearranging is required to determine that they're @@ -2605,16 +2606,16 @@ class ExpressionToComparableOperationConverterTest { @Test fun testEquals_exponentiationOps_same_areEqual() { - val comparable1 = parseNumericExpression("2 ^ 3").convertToComparableOperation() - val comparable2 = parseNumericExpression("2 ^ 3").convertToComparableOperation() + val comparable1 = parseNumericExpressionAsComparableOperation("2 ^ 3") + val comparable2 = parseNumericExpressionAsComparableOperation("2 ^ 3") assertThat(comparable1).isEqualTo(comparable2) } @Test fun testEquals_exponentiationOps_differentByOrder_areNotEqual() { - val comparable1 = parseNumericExpression("2 ^ 3").convertToComparableOperation() - val comparable2 = parseNumericExpression("3 ^ 2").convertToComparableOperation() + val comparable1 = parseNumericExpressionAsComparableOperation("2 ^ 3") + val comparable2 = parseNumericExpressionAsComparableOperation("3 ^ 2") // Exponentiation is not commutative. assertThat(comparable1).isNotEqualTo(comparable2) @@ -2624,13 +2625,9 @@ class ExpressionToComparableOperationConverterTest { fun testEquals_exponentiationOps_differentByAssociativity_areNotEqual() { // Disable optional errors to allow nested exponentiation. val comparable1 = - parseNumericExpression( - "2 ^ (3 ^ 4)", errorCheckingMode = REQUIRED_ONLY - ).convertToComparableOperation() + parseNumericExpressionAsComparableOperation("2 ^ (3 ^ 4)", errorCheckingMode = REQUIRED_ONLY) val comparable2 = - parseNumericExpression( - "(2 ^ 3) ^ 4", errorCheckingMode = REQUIRED_ONLY - ).convertToComparableOperation() + parseNumericExpressionAsComparableOperation("(2 ^ 3) ^ 4", errorCheckingMode = REQUIRED_ONLY) // Exponentiation is not associative. assertThat(comparable1).isNotEqualTo(comparable2) @@ -2638,8 +2635,8 @@ class ExpressionToComparableOperationConverterTest { @Test fun testEquals_exponentiationOps_differentByValue_areNotEqual() { - val comparable1 = parseNumericExpression("2 ^ 3").convertToComparableOperation() - val comparable2 = parseNumericExpression("2 ^ 4").convertToComparableOperation() + val comparable1 = parseNumericExpressionAsComparableOperation("2 ^ 3") + val comparable2 = parseNumericExpressionAsComparableOperation("2 ^ 4") assertThat(comparable1).isNotEqualTo(comparable2) } @@ -2647,9 +2644,9 @@ class ExpressionToComparableOperationConverterTest { @Test fun testEquals_exponentiationOps_sameOnlyByEvaluation_areNotEqual() { // Disable optional errors to allow nested exponentiation. - val comparable1 = parseNumericExpression("2 ^ 4").convertToComparableOperation() + val comparable1 = parseNumericExpressionAsComparableOperation("2 ^ 4") val comparable2 = - parseNumericExpression("2 ^ 2 ^ 2", errorCheckingMode = REQUIRED_ONLY).convertToComparableOperation() + parseNumericExpressionAsComparableOperation("2 ^ 2 ^ 2", errorCheckingMode = REQUIRED_ONLY) // While the two expressions are numerically equivalent, they aren't comparable since there are // extra terms in one (more than trivial rearranging is required to determine that they're @@ -2659,24 +2656,24 @@ class ExpressionToComparableOperationConverterTest { @Test fun testEquals_squareRootOps_same_areEqual() { - val comparable1 = parseNumericExpression("sqrt(2)").convertToComparableOperation() - val comparable2 = parseNumericExpression("sqrt(2)").convertToComparableOperation() + val comparable1 = parseNumericExpressionAsComparableOperation("sqrt(2)") + val comparable2 = parseNumericExpressionAsComparableOperation("sqrt(2)") assertThat(comparable1).isEqualTo(comparable2) } @Test fun testEquals_squareRootOps_differentByValue_areNotEqual() { - val comparable1 = parseNumericExpression("sqrt(2)").convertToComparableOperation() - val comparable2 = parseNumericExpression("sqrt(3)").convertToComparableOperation() + val comparable1 = parseNumericExpressionAsComparableOperation("sqrt(2)") + val comparable2 = parseNumericExpressionAsComparableOperation("sqrt(3)") assertThat(comparable1).isNotEqualTo(comparable2) } @Test fun testEquals_squareRootOps_sameOnlyByEvaluation_areNotEqual() { - val comparable1 = parseNumericExpression("sqrt(2)").convertToComparableOperation() - val comparable2 = parseNumericExpression("sqrt(1 + 1)").convertToComparableOperation() + val comparable1 = parseNumericExpressionAsComparableOperation("sqrt(2)") + val comparable2 = parseNumericExpressionAsComparableOperation("sqrt(1 + 1)") // While the two expressions are numerically equivalent, they aren't comparable since there are // extra terms in one (more than trivial rearranging is required to determine that they're @@ -2686,40 +2683,40 @@ class ExpressionToComparableOperationConverterTest { @Test fun testEquals_additionsAndSubtractions_differentByOrder_areEqual() { - val comparable1 = parseNumericExpression("1+2-3").convertToComparableOperation() - val comparable2 = parseNumericExpression("-3+2+1").convertToComparableOperation() + val comparable1 = parseNumericExpressionAsComparableOperation("1+2-3") + val comparable2 = parseNumericExpressionAsComparableOperation("-3+2+1") assertThat(comparable1).isEqualTo(comparable2) } @Test fun testEquals_multiplicationsAndDivisions_differentByOrder_areEqual() { - val comparable1 = parseNumericExpression("2*3/4*7").convertToComparableOperation() - val comparable2 = parseNumericExpression("7*2*3/4").convertToComparableOperation() + val comparable1 = parseNumericExpressionAsComparableOperation("2*3/4*7") + val comparable2 = parseNumericExpressionAsComparableOperation("7*2*3/4") assertThat(comparable1).isEqualTo(comparable2) } @Test fun testEquals_allAccumulationOperations_differentByOrder_areEqual() { - val comparable1 = parseNumericExpression("1+2*3/4*7-8+3").convertToComparableOperation() - val comparable2 = parseNumericExpression("-8+3+7*3/4*2+1").convertToComparableOperation() + val comparable1 = parseNumericExpressionAsComparableOperation("1+2*3/4*7-8+3") + val comparable2 = parseNumericExpressionAsComparableOperation("-8+3+7*3/4*2+1") assertThat(comparable1).isEqualTo(comparable2) } @Test fun testEquals_allOperations_differentByOrder_areEqual() { - val comparable1 = parseNumericExpression("sqrt(1+2*3)*2^3/7-(2-2*3)").convertToComparableOperation() - val comparable2 = parseNumericExpression("2^3*sqrt(3*2+1)/7-(-3*2+2)").convertToComparableOperation() + val comparable1 = parseNumericExpressionAsComparableOperation("sqrt(1+2*3)*2^3/7-(2-2*3)") + val comparable2 = parseNumericExpressionAsComparableOperation("2^3*sqrt(3*2+1)/7-(-3*2+2)") assertThat(comparable1).isEqualTo(comparable2) } @Test fun testEquals_allOperations_oneNestedDifferentByValue_areNotEqual() { - val comparable1 = parseNumericExpression("sqrt(1+2*3)*2^3/7-(2-2*3)").convertToComparableOperation() - val comparable2 = parseNumericExpression("sqrt(1+2*3)*2^3/7-(2-4*3)").convertToComparableOperation() + val comparable1 = parseNumericExpressionAsComparableOperation("sqrt(1+2*3)*2^3/7-(2-2*3)") + val comparable2 = parseNumericExpressionAsComparableOperation("sqrt(1+2*3)*2^3/7-(2-4*3)") // Just one different term leads to the entire comparison failing. assertThat(comparable1).isNotEqualTo(comparable2) @@ -2727,16 +2724,24 @@ class ExpressionToComparableOperationConverterTest { @Test fun testEquals_twoOps_allOperations_oneMissingTerm_areNotEqual() { - val comparable1 = parseNumericExpression("sqrt(1+2*3)*2^3/7-(2-2*3)").convertToComparableOperation() - val comparable2 = parseNumericExpression("sqrt(1+2*3)*2^3/7-(2-2)").convertToComparableOperation() + val comparable1 = parseNumericExpressionAsComparableOperation("sqrt(1+2*3)*2^3/7-(2-2*3)") + val comparable2 = parseNumericExpressionAsComparableOperation("sqrt(1+2*3)*2^3/7-(2-2)") // Just one missing term leads to the entire comparison failing. assertThat(comparable1).isNotEqualTo(comparable2) } + private fun parseNumericExpressionAsComparableOperation( + expression: String, + errorCheckingMode: ErrorCheckingMode = ALL_ERRORS + ): ComparableOperation { + return parseNumericExpression(expression, errorCheckingMode).convertToComparableOperation() + } + private companion object { private fun parseNumericExpression( - expression: String, errorCheckingMode: ErrorCheckingMode = ALL_ERRORS + expression: String, + errorCheckingMode: ErrorCheckingMode = ALL_ERRORS ): MathExpression { return MathExpressionParser.parseNumericExpression( expression, errorCheckingMode From 45b6099465f12eb09a644c43a8d8887d1e6bb68f Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 28 Jan 2022 21:25:14 -0800 Subject: [PATCH 090/162] Post-merge fixes. --- .../android/util/math/PolynomialExtensions.kt | 26 +++++++++++++++++++ .../org/oppia/android/util/math/BUILD.bazel | 2 +- .../util/math/ExpressionToPolynomialTest.kt | 4 +-- 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt index 39ad485955d..534253c02c4 100644 --- a/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt @@ -393,3 +393,29 @@ private fun Real.maybeSimplifyRationalToInteger(): Real = when (realTypeCase) { Real.RealTypeCase.IRRATIONAL, Real.RealTypeCase.INTEGER, Real.RealTypeCase.REALTYPE_NOT_SET, null -> this } + +// TODO: figure out of this can be removed. +private fun > Comparator.thenComparingReversed( + keySelector: (T) -> U +): Comparator = thenComparing(Comparator.comparing(keySelector).reversed()) + +// TODO: figure out of this can be removed. +private fun Comparator.toSetComparator(): Comparator> { + val itemComparator = this + return Comparator { first, second -> + // Reference: https://stackoverflow.com/a/30107086. + val firstIter = first.iterator() + val secondIter = second.iterator() + while (firstIter.hasNext() && secondIter.hasNext()) { + val comparison = itemComparator.compare(firstIter.next(), secondIter.next()) + if (comparison != 0) return@Comparator comparison // Found a different item. + } + + // Everything is equal up to here, see if the lists are different length. + return@Comparator when { + firstIter.hasNext() -> 1 // The first list is longer, therefore "greater." + secondIter.hasNext() -> -1 // Ditto, but for the second list. + else -> 0 // Otherwise, they're the same length with all equal items (and are thus equal). + } + } +} diff --git a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel index 98cc029ec01..519cff70cf8 100644 --- a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel @@ -185,7 +185,7 @@ oppia_android_test( "//third_party:junit_junit", "//third_party:org_robolectric_robolectric", "//third_party:robolectric_android-all", - "//utility/src/main/java/org/oppia/android/util/math:parser", + "//utility/src/main/java/org/oppia/android/util/math:math_expression_parser", ], ) diff --git a/utility/src/test/java/org/oppia/android/util/math/ExpressionToPolynomialTest.kt b/utility/src/test/java/org/oppia/android/util/math/ExpressionToPolynomialTest.kt index f8163f88c3c..b66c801e1ba 100644 --- a/utility/src/test/java/org/oppia/android/util/math/ExpressionToPolynomialTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/ExpressionToPolynomialTest.kt @@ -779,8 +779,8 @@ class ExpressionToPolynomialTest { assertThat(poly44).term(1).apply { hasCoefficientThat().isRationalThat().apply { hasNegativePropertyThat().isTrue() - hasWholeNumberThat().isEqualTo(0) - hasNumeratorThat().isEqualTo(3) + hasWholeNumberThat().isEqualTo(1) + hasNumeratorThat().isEqualTo(1) hasDenominatorThat().isEqualTo(2) } hasVariableCountThat().isEqualTo(0) From 76a788774f6eeb441933b3f5858a9e58d2c97545 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Tue, 1 Feb 2022 18:57:46 -0800 Subject: [PATCH 091/162] Add tests. --- .../math/ExpressionToPolynomialConverter.kt | 14 +- .../util/math/MathExpressionExtensions.kt | 26 +- .../android/util/math/PolynomialExtensions.kt | 263 +- .../oppia/android/util/math/RealExtensions.kt | 52 +- .../org/oppia/android/util/math/BUILD.bazel | 40 +- .../ExpressionToPolynomialConverterTest.kt | 2325 +++++++++++++++ .../util/math/ExpressionToPolynomialTest.kt | 945 ------ .../util/math/MathExpressionExtensionsTest.kt | 42 +- .../util/math/PolynomialExtensionsTest.kt | 2653 ++++++++++++++++- .../android/util/math/RealExtensionsTest.kt | 402 ++- 10 files changed, 5514 insertions(+), 1248 deletions(-) create mode 100644 utility/src/test/java/org/oppia/android/util/math/ExpressionToPolynomialConverterTest.kt delete mode 100644 utility/src/test/java/org/oppia/android/util/math/ExpressionToPolynomialTest.kt diff --git a/utility/src/main/java/org/oppia/android/util/math/ExpressionToPolynomialConverter.kt b/utility/src/main/java/org/oppia/android/util/math/ExpressionToPolynomialConverter.kt index 15b9678626b..e64aa1baa63 100644 --- a/utility/src/main/java/org/oppia/android/util/math/ExpressionToPolynomialConverter.kt +++ b/utility/src/main/java/org/oppia/android/util/math/ExpressionToPolynomialConverter.kt @@ -23,11 +23,14 @@ import org.oppia.android.app.model.MathUnaryOperation.Operator.POSITIVE import org.oppia.android.app.model.Polynomial import org.oppia.android.app.model.MathBinaryOperation.Operator as BinaryOperator import org.oppia.android.app.model.MathUnaryOperation.Operator as UnaryOperator +import org.oppia.android.app.model.Real class ExpressionToPolynomialConverter private constructor() { companion object { + // TODO: document that this generally only relate to algebraic expressions. fun MathExpression.reduceToPolynomial(): Polynomial? = - replaceSquareRoots().reduceToPolynomialAux() + replaceSquareRoots() + .reduceToPolynomialAux() ?.removeUnnecessaryVariables() ?.simplifyRationals() ?.sort() @@ -59,6 +62,7 @@ class ExpressionToPolynomialConverter private constructor() { }.build() FUNCTION_UNSPECIFIED, FunctionType.UNRECOGNIZED, null -> this } + // This also eliminates groups from the expression. GROUP -> group.replaceSquareRoots() CONSTANT, VARIABLE, EXPRESSIONTYPE_NOT_SET, null -> this } @@ -83,7 +87,7 @@ class ExpressionToPolynomialConverter private constructor() { SUBTRACT -> leftPolynomial - rightPolynomial MULTIPLY -> leftPolynomial * rightPolynomial DIVIDE -> leftPolynomial / rightPolynomial - EXPONENTIATE -> leftPolynomial.pow(rightPolynomial) + EXPONENTIATE -> leftPolynomial pow rightPolynomial BinaryOperator.OPERATOR_UNSPECIFIED, BinaryOperator.UNRECOGNIZED, null -> null } } @@ -109,5 +113,11 @@ class ExpressionToPolynomialConverter private constructor() { }.build() ) } + + private fun createConstantPolynomial(constant: Real): Polynomial = + createSingleTermPolynomial(Polynomial.Term.newBuilder().setCoefficient(constant).build()) + + private fun createSingleTermPolynomial(term: Polynomial.Term): Polynomial = + Polynomial.newBuilder().apply { addTerm(term) }.build() } } diff --git a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt index 1c45ad9009c..65d40515f5f 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt @@ -47,28 +47,4 @@ fun MathExpression.evaluateAsNumericExpression(): Real? = evaluate() */ fun MathExpression.toComparableOperation(): ComparableOperation = convertToComparableOperation() -fun MathExpression.toPolynomial(): Polynomial? = stripGroups().reduceToPolynomial() - -// TODO: remove similar to comparable operations. -private fun MathExpression.stripGroups(): MathExpression { - return when (expressionTypeCase) { - BINARY_OPERATION -> toBuilder().apply { - binaryOperation = binaryOperation.toBuilder().apply { - leftOperand = binaryOperation.leftOperand.stripGroups() - rightOperand = binaryOperation.rightOperand.stripGroups() - }.build() - }.build() - UNARY_OPERATION -> toBuilder().apply { - unaryOperation = unaryOperation.toBuilder().apply { - operand = unaryOperation.operand.stripGroups() - }.build() - }.build() - FUNCTION_CALL -> toBuilder().apply { - functionCall = functionCall.toBuilder().apply { - argument = functionCall.argument.stripGroups() - }.build() - }.build() - GROUP -> group.stripGroups() - CONSTANT, VARIABLE, EXPRESSIONTYPE_NOT_SET, null -> this - } -} +fun MathExpression.toPolynomial(): Polynomial? = reduceToPolynomial() diff --git a/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt index 534253c02c4..e64f0aaabab 100644 --- a/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt @@ -7,6 +7,11 @@ import org.oppia.android.app.model.Polynomial.Term.Variable import org.oppia.android.app.model.Real import java.util.SortedSet +val ZERO_POLYNOMIAL: Polynomial = createConstantPolynomial(ZERO) + +val ONE_POLYNOMIAL: Polynomial = createConstantPolynomial(ONE) + +// TODO: Kotlin-ify. private val POLYNOMIAL_VARIABLE_COMPARATOR: Comparator by lazy { // Note that power is reversed because larger powers should actually be sorted ahead of smaller // powers for the same variable name (but variable name still takes precedence). This ensures @@ -27,17 +32,12 @@ private val POLYNOMIAL_TERM_COMPARATOR: Comparator by lazy { Comparator.comparing>( { term -> term.variableList.toSortedSet(POLYNOMIAL_VARIABLE_COMPARATOR) }, POLYNOMIAL_VARIABLE_COMPARATOR.reversed().toSetComparator() - ).reversed().thenComparing(Term::getCoefficient, REAL_COMPARATOR) + ).reversed().thenComparing(Term::getCoefficient, REAL_COMPARATOR.reversed()) } /** Returns whether this polynomial is a constant-only polynomial (contains no variables). */ fun Polynomial.isConstant(): Boolean = termCount == 1 && getTerm(0).variableCount == 0 -fun Polynomial.isSingleTerm(): Boolean = termList.size == 1 - -fun Polynomial.isApproximatelyZero(): Boolean = - termList.all { it.coefficient.isApproximatelyZero() } // Zero polynomials only have 0 coefs. - /** * Returns the first term coefficient from this polynomial. This corresponds to the whole value of * the polynomial iff isConstant() returns true, otherwise this value isn't useful. @@ -47,10 +47,6 @@ fun Polynomial.isApproximatelyZero(): Boolean = */ fun Polynomial.getConstant(): Real = getTerm(0).coefficient -// Return the highest power to represent the degree of the polynomial. Reference: -// https://www.varsitytutors.com/algebra_1-help/how-to-find-the-degree-of-a-polynomial. -fun Polynomial.getDegree(): Int = getLeadingTerm().highestDegree() - /** * Returns a human-readable, plaintext representation of this [Polynomial]. * @@ -67,57 +63,6 @@ fun Polynomial.toPlainText(): String { } } -private fun Term.toPlainText(): String { - val productValues = mutableListOf() - - // Include the coefficient if there is one (coefficients of 1 are ignored only if there are - // variables present). - productValues += when { - variableList.isEmpty() || !abs(coefficient).isApproximatelyEqualTo(1.0) -> when { - coefficient.isRational() && variableList.isNotEmpty() -> "(${coefficient.toPlainText()})" - else -> coefficient.toPlainText() - } - coefficient.isNegative() -> "-" - else -> "" - } - - // Include any present variables. - productValues += variableList.map(Variable::toPlainText) - - // Take the product of all relevant values of the term. - return productValues.joinToString(separator = "") -} - -private fun Variable.toPlainText(): String { - return if (power > 1) "$name^$power" else name -} - -fun Polynomial.combineLikeTerms(): Polynomial { - // The following algorithm is expected to grow in space by O(N*M) and in time by O(N*m*log(m)) - // where N is the total number of terms, M is the total number of variables, and m is the largest - // single count of variables among all terms (this is assuming constant-time insertion for the - // underlying hashtable). - val newTerms = termList.groupBy { - it.variableList.sortedWith(POLYNOMIAL_VARIABLE_COMPARATOR) - }.mapValues { (_, coefficientTerms) -> - coefficientTerms.map { it.coefficient } - }.mapNotNull { (variables, coefficients) -> - // Combine like terms by summing their coefficients. - val newCoefficient = coefficients.reduce(Real::plus) - return@mapNotNull if (!newCoefficient.isApproximatelyZero()) { - Term.newBuilder().apply { - coefficient = newCoefficient - - // Remove variables with zero powers (since they evaluate to '1'). - addAllVariable(variables.filter { variable -> variable.power != 0 }) - }.build() - } else null // Zero terms should be removed. - } - return Polynomial.newBuilder().apply { - addAllTerm(newTerms) - }.build().ensureAtLeastConstant() -} - fun Polynomial.removeUnnecessaryVariables(): Polynomial { return Polynomial.newBuilder().apply { addAllTerm( @@ -141,7 +86,17 @@ fun Polynomial.simplifyRationals(): Polynomial { } fun Polynomial.sort(): Polynomial = Polynomial.newBuilder().apply { - addAllTerm(this@sort.termList.sortedWith(POLYNOMIAL_TERM_COMPARATOR)) + // The double sorting here is less efficient, but it ensures both terms and variables are + // correctly kept sorted. Fortunately, most internal operations will keep variables sorted by + // default. + addAllTerm( + this@sort.termList.map { term -> + Term.newBuilder().apply { + coefficient = term.coefficient + addAllVariable(term.variableList.sortedWith(POLYNOMIAL_VARIABLE_COMPARATOR)) + }.build() + }.sortedWith(POLYNOMIAL_TERM_COMPARATOR) + ) }.build() operator fun Polynomial.unaryMinus(): Polynomial { @@ -157,7 +112,7 @@ operator fun Polynomial.plus(rhs: Polynomial): Polynomial { // common terms). return Polynomial.newBuilder().apply { addAllTerm(this@plus.termList + rhs.termList) - }.build().combineLikeTerms().removeUnnecessaryVariables() + }.build().combineLikeTerms().simplifyRationals().removeUnnecessaryVariables() } operator fun Polynomial.minus(rhs: Polynomial): Polynomial { @@ -172,8 +127,11 @@ operator fun Polynomial.times(rhs: Polynomial): Polynomial { } // Treat each multiplied term as a unique polynomial, then add them together (so that like terms - // can be properly combined). - return crossMultipliedTerms.map { createSingleTermPolynomial(it) }.reduce(Polynomial::plus) + // can be properly combined). Finally, ensure unnecessary variables are eliminated (especially for + // cases where no addition takes place, such as 0*x). + return crossMultipliedTerms.map { + createSingleTermPolynomial(it) + }.reduce(Polynomial::plus).simplifyRationals().removeUnnecessaryVariables() } operator fun Polynomial.div(rhs: Polynomial): Polynomial? { @@ -182,60 +140,90 @@ operator fun Polynomial.div(rhs: Polynomial): Polynomial? { return null // Dividing by zero is invalid and thus cannot yield a polynomial. } - var quotient = createConstantPolynomial(ZERO) + var quotient = ZERO_POLYNOMIAL var remainder = this - val leadingDivisorTerm = rhs.getLeadingTerm() + val leadingDivisorTerm = rhs.getLeadingTerm() ?: return null val divisorVariable = leadingDivisorTerm.highestDegreeVariable() val divisorVariableName = divisorVariable?.name val divisorDegree = leadingDivisorTerm.highestDegree() - while (!remainder.isApproximatelyZero() && remainder.getDegree() >= divisorDegree) { + while (!remainder.isApproximatelyZero() + && (remainder.getDegree() ?: return null) >= divisorDegree) { // Attempt to divide the leading terms (this may fail). Note that the leading term should always // be based on the divisor variable being used (otherwise subsequent division steps will be // inconsistent and potentially fail to resolve). - val newTerm = - remainder.getLeadingTerm(matchedVariable = divisorVariableName) / leadingDivisorTerm - ?: return null + val remainingLeadingTerm = remainder.getLeadingTerm(matchedVariable = divisorVariableName) + val newTerm = remainingLeadingTerm?.div(leadingDivisorTerm) ?: return null quotient += newTerm.toPolynomial() remainder -= newTerm.toPolynomial() * rhs } - return when { - remainder.isApproximatelyZero() -> quotient // Exact division (i.e. with no remainder). - remainder.isConstant() && rhs.isConstant() -> { - // Remainder is a constant term. - val remainingTerm = remainder.getConstant() / rhs.getConstant() - quotient + createConstantPolynomial(remainingTerm) - } - else -> null // Remainder is a polynomial, so the division failed. - } + // Either the division was exact, or the remainder is a polynomial (i.e. a failed division). + return quotient.takeIf { remainder.isApproximatelyZero() } } -fun Polynomial.pow(exp: Polynomial): Polynomial? { +infix fun Polynomial.pow(exp: Polynomial): Polynomial? { // Polynomial exponentiation is only supported if the right side is a constant polynomial, // otherwise the result cannot be a polynomial (though could still be compared to another // expression by utilizing sampling techniques). - return if (exp.isConstant()) pow(exp.getConstant()) else null + return if (exp.isConstant()) { + pow(exp.getConstant())?.simplifyRationals()?.removeUnnecessaryVariables() + } else null } -fun createConstantPolynomial(constant: Real): Polynomial = +private fun createConstantPolynomial(constant: Real): Polynomial = createSingleTermPolynomial(Term.newBuilder().setCoefficient(constant).build()) -fun createSingleTermPolynomial(term: Term): Polynomial = +private fun createSingleTermPolynomial(term: Term): Polynomial = Polynomial.newBuilder().apply { addTerm(term) }.build() -private fun Polynomial.pow(exp: Int): Polynomial { - // Anything raised to the power of 0 is 1. - if (exp == 0) return createConstantPolynomial(ONE) - if (exp == 1) return this - var newValue = this - for (i in 1 until exp) newValue *= this - return newValue +private fun Term.toPlainText(): String { + val productValues = mutableListOf() + + // Include the coefficient if there is one (coefficients of 1 are ignored only if there are + // variables present). + productValues += when { + variableList.isEmpty() || !abs(coefficient).isApproximatelyEqualTo(1.0) -> when { + coefficient.isRational() && variableList.isNotEmpty() -> "(${coefficient.toPlainText()})" + else -> coefficient.toPlainText() + } + coefficient.isNegative() -> "-" + else -> "" + } + + // Include any present variables. + productValues += variableList.map(Variable::toPlainText) + + // Take the product of all relevant values of the term. + return productValues.joinToString(separator = "") } -private fun Polynomial.pow(rational: Fraction): Polynomial? { - // Polynomials with addition require factoring. - return if (isSingleTerm()) { - termList.first().pow(rational)?.toPolynomial() - } else null +private fun Variable.toPlainText(): String { + return if (power > 1) "$name^$power" else name +} + +private fun Polynomial.combineLikeTerms(): Polynomial { + // The following algorithm is expected to grow in space by O(N*M) and in time by O(N*m*log(m)) + // where N is the total number of terms, M is the total number of variables, and m is the largest + // single count of variables among all terms (this is assuming constant-time insertion for the + // underlying hashtable). + val newTerms = termList.groupBy { + it.variableList.sortedWith(POLYNOMIAL_VARIABLE_COMPARATOR) + }.mapValues { (_, coefficientTerms) -> + coefficientTerms.map { it.coefficient } + }.mapNotNull { (variables, coefficients) -> + // Combine like terms by summing their coefficients. + val newCoefficient = coefficients.reduce(Real::plus) + return@mapNotNull if (!newCoefficient.isApproximatelyZero()) { + Term.newBuilder().apply { + coefficient = newCoefficient + + // Remove variables with zero powers (since they evaluate to '1'). + addAllVariable(variables.filter { variable -> variable.power != 0 }) + }.build() + } else null // Zero terms should be removed. + } + return Polynomial.newBuilder().apply { + addAllTerm(newTerms) + }.build().ensureAtLeastConstant() } private fun Polynomial.pow(exp: Real): Polynomial? { @@ -243,7 +231,7 @@ private fun Polynomial.pow(exp: Real): Polynomial? { val positivePower = if (shouldBeInverted) -exp else exp val exponentiation = when { // Constant polynomials can be raised by any constant. - isConstant() -> createConstantPolynomial(getConstant().pow(positivePower)) + isConstant() -> (getConstant() pow positivePower)?.let { createConstantPolynomial(it) } // Polynomials can only be raised to positive integers (or zero). exp.isWholeNumber() -> exp.asWholeNumber()?.let { pow(it) } @@ -251,12 +239,12 @@ private fun Polynomial.pow(exp: Real): Polynomial? { // Polynomials can potentially be raised by a fractional power. exp.isRational() -> pow(exp.rational) - // All other cases require factoring will definitely not compute to polynomials (such as + // All other cases require factoring most likely will not compute to polynomials (such as // irrational exponents). else -> null } return if (shouldBeInverted) { - val onePolynomial = createConstantPolynomial(ONE) + val onePolynomial = ONE_POLYNOMIAL // Note that this division is guaranteed to fail if the exponentiation result is a polynomial. // Future implementations may leverage root-finding algorithms to factor for integer inverse // powers (such as square root, cubic root, etc.). Non-integer inverse powers will require @@ -265,6 +253,22 @@ private fun Polynomial.pow(exp: Real): Polynomial? { } else exponentiation } +private fun Polynomial.pow(rational: Fraction): Polynomial? { + // Polynomials with addition require factoring. + return if (isSingleTerm()) { + termList.first().pow(rational)?.toPolynomial() + } else null +} + +private fun Polynomial.pow(exp: Int): Polynomial { + // Anything raised to the power of 0 is 1. + if (exp == 0) return ONE_POLYNOMIAL + if (exp == 1) return this + var newValue = this + for (i in 1 until exp) newValue *= this + return newValue +} + private operator fun Term.times(rhs: Term): Term { // The coefficients are always multiplied. val combinedCoefficient = coefficient * rhs.coefficient @@ -324,14 +328,14 @@ private fun Term.pow(rational: Fraction): Term? { // term in question is not rootable to that degree. if (newVariablePowers.any { !it.isOnlyWholeNumber() }) return null + val newCoefficient = coefficient pow Real.newBuilder().apply { + this.rational = rational + }.build() ?: return null + return Term.newBuilder().apply { - coefficient = this@pow.coefficient.pow( - Real.newBuilder().apply { - this.rational = rational - }.build() - ) + coefficient = newCoefficient addAllVariable( - this@pow.variableList.zip(newVariablePowers).map { (variable, newPower) -> + (this@pow.variableList zip newVariablePowers).map { (variable, newPower) -> variable.toBuilder().apply { power = newPower.toWholeNumber() }.build() @@ -340,25 +344,30 @@ private fun Term.pow(rational: Fraction): Term? { }.build() } +/** + * Returns either this [Polynomial] or [ZERO_POLYNOMIAL] if this polynomial has no terms (i.e. the + * returned polynomial is always guaranteed to have at least one term). + */ private fun Polynomial.ensureAtLeastConstant(): Polynomial { - return if (termCount == 0) { - Polynomial.newBuilder().apply { - addTerm( - Term.newBuilder().apply { - coefficient = ZERO - }.build() - ) - }.build() - } else this + return if (termCount != 0) this else ZERO_POLYNOMIAL } -private fun Polynomial.getLeadingTerm(matchedVariable: String? = null): Term { +private fun Polynomial.isSingleTerm(): Boolean = termList.size == 1 + +private fun Polynomial.isApproximatelyZero(): Boolean = + termList.all { it.coefficient.isApproximatelyZero() } // Zero polynomials only have 0 coefs. + +// Return the highest power to represent the degree of the polynomial. Reference: +// https://www.varsitytutors.com/algebra_1-help/how-to-find-the-degree-of-a-polynomial. +private fun Polynomial.getDegree(): Int? = getLeadingTerm()?.highestDegree() + +private fun Polynomial.getLeadingTerm(matchedVariable: String? = null): Term? { // Return the leading term. Reference: https://undergroundmathematics.org/glossary/leading-term. return termList.filter { term -> matchedVariable?.let { variableName -> term.variableList.any { it.name == variableName } } ?: true - }.reduce { maxTerm, term -> + }.takeIf { it.isNotEmpty() }?.reduce { maxTerm, term -> val maxTermDegree = maxTerm.highestDegree() val termDegree = term.highestDegree() return@reduce if (termDegree > maxTermDegree) term else maxTerm @@ -383,11 +392,23 @@ private fun Map.toVariableList(): List { private fun Real.maybeSimplifyRationalToInteger(): Real = when (realTypeCase) { Real.RealTypeCase.RATIONAL -> { - if (rational.isOnlyWholeNumber()) { - Real.newBuilder().apply { - integer = this@maybeSimplifyRationalToInteger.rational.toWholeNumber() - }.build() - } else this + val improperRational = rational.toImproperForm() + when { + rational.isOnlyWholeNumber() -> { + Real.newBuilder().apply { + integer = this@maybeSimplifyRationalToInteger.rational.toWholeNumber() + }.build() + } + // Some fractions are effectively whole numbers. + improperRational.denominator == 1 -> { + Real.newBuilder().apply { + integer = if (improperRational.isNegative) { + -improperRational.numerator + } else improperRational.numerator + }.build() + } + else -> this + } } // Nothing to do in these cases. Real.RealTypeCase.IRRATIONAL, Real.RealTypeCase.INTEGER, Real.RealTypeCase.REALTYPE_NOT_SET, diff --git a/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt index 7f8cafe175e..25b520d5a3e 100644 --- a/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt @@ -92,7 +92,7 @@ fun Real.asWholeNumber(): Int? { RATIONAL -> if (rational.isOnlyWholeNumber()) rational.toWholeNumber() else null INTEGER -> integer IRRATIONAL -> null - REALTYPE_NOT_SET, null -> throw Exception("Invalid real: $this.") + REALTYPE_NOT_SET, null -> throw IllegalStateException("Invalid real: $this.") } } @@ -296,8 +296,7 @@ operator fun Real.div(rhs: Real): Real { * This function can fail in a few circumstances: * - One of the [Real]s is malformed or incomplete (such as a default instance). * - In cases where a root is being taken (i.e. when |[rhs]| < 1), if the root cannot be taken - * either an exception will be thrown or NaN will be returned (such as trying to take the even - * root of a negative value). + * either null or NaN will be returned (such as trying to take the even root of a negative value). * * Further, note that this function represents the real value root rather than the principal root, * so negative bases are allowed so long as the root being used is odd. For non-integerlike powers, @@ -326,7 +325,7 @@ operator fun Real.div(rhs: Real): Real { * (Note that the left column represents the left-hand side and the top row represents the * right-hand side of the operation). */ -infix fun Real.pow(rhs: Real): Real { +infix fun Real.pow(rhs: Real): Real? { // Powers can really only be effectively done via floats or whole-number only fractions. return when (realTypeCase) { RATIONAL -> { @@ -377,8 +376,7 @@ infix fun Real.pow(rhs: Real): Real { * Failure cases: * - An invalid [Real] is passed in (such as a default instance), resulting in an exception being * thrown. - * - A negative value is passed in (this will either result in an exception or a NaN being - * returned). + * - A negative value is passed in (this will either result in null or a NaN being returned). * * Similar to [Real.plus] & other operations, this function attempts to retain as much precision as * possible by first performing perfect roots before needing to perform a numerical approximation. @@ -394,7 +392,7 @@ infix fun Real.pow(rhs: Real): Real { * | irrational | irrational | irrational | * |------------------------------------------------| */ -fun sqrt(real: Real): Real { +fun sqrt(real: Real): Real? { return when (real.realTypeCase) { RATIONAL -> real.rational.root(base = 2, invert = false) IRRATIONAL -> createIrrationalReal(kotlin.math.sqrt(real.irrational)) @@ -439,7 +437,7 @@ private fun Int.pow(exp: Int): Real { } } -private fun Fraction.root(base: Int, invert: Boolean): Real { +private fun Fraction.root(base: Int, invert: Boolean): Real? { check(base > 0) { "Expected base of 1 or higher, not: $base" } val adjustedFraction = toImproperForm() @@ -448,24 +446,28 @@ private fun Fraction.root(base: Int, invert: Boolean): Real { val adjustedDenom = adjustedFraction.denominator val rootedNumerator = if (invert) root(adjustedDenom, base) else root(adjustedNum, base) val rootedDenominator = if (invert) root(adjustedNum, base) else root(adjustedDenom, base) - return if (rootedNumerator.isInteger() && rootedDenominator.isInteger()) { - Real.newBuilder().apply { - rational = Fraction.newBuilder().apply { - isNegative = rootedNumerator.isNegative() || rootedDenominator.isNegative() - numerator = rootedNumerator.integer.absoluteValue - denominator = rootedDenominator.integer.absoluteValue - }.build().toProperForm() - }.build() - } else { - // One or both of the components of the fraction can't be rooted, so compute an irrational - // version. - Real.newBuilder().apply { - irrational = rootedNumerator.toDouble() / rootedDenominator.toDouble() - }.build() + return when { + rootedNumerator == null || rootedDenominator == null -> null + rootedNumerator.isInteger() && rootedDenominator.isInteger() -> { + Real.newBuilder().apply { + rational = Fraction.newBuilder().apply { + isNegative = rootedNumerator.isNegative() || rootedDenominator.isNegative() + numerator = rootedNumerator.integer.absoluteValue + denominator = rootedDenominator.integer.absoluteValue + }.build().toProperForm() + }.build() + } + else -> { + // One or both of the components of the fraction can't be rooted, so compute an irrational + // version. + Real.newBuilder().apply { + irrational = rootedNumerator.toDouble() / rootedDenominator.toDouble() + }.build() + } } } -private fun root(int: Int, base: Int): Real { +private fun root(int: Int, base: Int): Real? { // First, check if the integer is a root. Base reference for possible methods: // https://www.researchgate.net/post/How-to-decide-if-a-given-number-will-have-integer-square-root-or-not. if (int == 0 && base == 0) { @@ -478,7 +480,7 @@ private fun root(int: Int, base: Int): Real { } check(base > 0) { "Expected base of 1 or higher, not: $base" } - check((int < 0 && base.isOdd()) || int >= 0) { "Radicand results in imaginary number: $int" } + if (int < 0 && !base.isOdd()) return null when { int == 0 -> { @@ -502,7 +504,7 @@ private fun root(int: Int, base: Int): Real { } val radicand = int.absoluteValue - var potentialRoot = base + var potentialRoot = 1 while (potentialRoot.pow(base).integer < radicand) { potentialRoot++ } diff --git a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel index 519cff70cf8..7a7aed1e686 100644 --- a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel @@ -98,6 +98,24 @@ oppia_android_test( ], ) +oppia_android_test( + name = "ExpressionToPolynomialConverterTest", + srcs = ["ExpressionToPolynomialConverterTest.kt"], + custom_package = "org.oppia.android.util.math", + test_class = "org.oppia.android.util.math.ExpressionToPolynomialConverterTest", + test_manifest = "//utility:test_manifest", + deps = [ + "//model/src/main/proto:math_java_proto_lite", + "//testing/src/main/java/org/oppia/android/testing/math:polynomial_subject", + "//third_party:androidx_test_ext_junit", + "//third_party:com_google_truth_truth", + "//third_party:junit_junit", + "//third_party:org_robolectric_robolectric", + "//third_party:robolectric_android-all", + "//utility/src/main/java/org/oppia/android/util/math:math_expression_parser", + ], +) + oppia_android_test( name = "FloatExtensionsTest", srcs = ["FloatExtensionsTest.kt"], @@ -160,6 +178,7 @@ oppia_android_test( test_manifest = "//utility:test_manifest", deps = [ "//model/src/main/proto:math_java_proto_lite", + "//testing/src/main/java/org/oppia/android/testing/math:polynomial_subject", "//testing/src/main/java/org/oppia/android/testing/math:real_subject", "//third_party:androidx_test_ext_junit", "//third_party:com_google_truth_truth", @@ -171,24 +190,6 @@ oppia_android_test( ], ) -oppia_android_test( - name = "ExpressionToPolynomialTest", - srcs = ["ExpressionToPolynomialTest.kt"], - custom_package = "org.oppia.android.util.math", - test_class = "org.oppia.android.util.math.ExpressionToPolynomialTest", - test_manifest = "//utility:test_manifest", - deps = [ - "//model/src/main/proto:math_java_proto_lite", - "//testing/src/main/java/org/oppia/android/testing/math:polynomial_subject", - "//third_party:androidx_test_ext_junit", - "//third_party:com_google_truth_truth", - "//third_party:junit_junit", - "//third_party:org_robolectric_robolectric", - "//third_party:robolectric_android-all", - "//utility/src/main/java/org/oppia/android/util/math:math_expression_parser", - ], -) - oppia_android_test( name = "MathExpressionParserTest", srcs = ["MathExpressionParserTest.kt"], @@ -293,8 +294,9 @@ oppia_android_test( test_manifest = "//utility:test_manifest", deps = [ "//model/src/main/proto:math_java_proto_lite", + "//testing/src/main/java/org/oppia/android/testing/junit:oppia_parameterized_test_runner", + "//testing/src/main/java/org/oppia/android/testing/junit:parameterized_robolectric_test_runner", "//testing/src/main/java/org/oppia/android/testing/math:polynomial_subject", - "//third_party:androidx_test_ext_junit", "//third_party:com_google_truth_truth", "//third_party:junit_junit", "//third_party:org_robolectric_robolectric", diff --git a/utility/src/test/java/org/oppia/android/util/math/ExpressionToPolynomialConverterTest.kt b/utility/src/test/java/org/oppia/android/util/math/ExpressionToPolynomialConverterTest.kt new file mode 100644 index 00000000000..84d4f434731 --- /dev/null +++ b/utility/src/test/java/org/oppia/android/util/math/ExpressionToPolynomialConverterTest.kt @@ -0,0 +1,2325 @@ +package org.oppia.android.util.math + +import com.google.common.truth.Truth.assertThat +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Test +import org.junit.runner.RunWith +import org.oppia.android.app.model.MathExpression +import org.oppia.android.testing.math.PolynomialSubject.Companion.assertThat +import org.oppia.android.util.math.ExpressionToPolynomialConverter.Companion.reduceToPolynomial +import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode +import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode.ALL_ERRORS +import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode.REQUIRED_ONLY +import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult +import org.robolectric.annotation.LooperMode + +/** + * Tests for [ExpressionToPolynomialConverter]. + * + * Note that this suite only tests with algebraic expressions since numeric expressions are never + * considered to be polynomials (despite numeric expression evaluation and the constant term of + * polynomials being expected to always result in the same value). + */ +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") +@RunWith(AndroidJUnit4::class) +@LooperMode(LooperMode.Mode.PAUSED) +class ExpressionToPolynomialConverterTest { + @Test + fun testReduce_integerConstantExpression_returnsConstantPolynomial() { + val expression = parseAlgebraicExpression("2") + + val polynomial = expression.reduceToPolynomial() + + assertThat(polynomial).isConstantThat().isIntegerThat().isEqualTo(2) + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("2") + } + + @Test + fun testReduce_decimalConstantExpression_returnsConstantPolynomial() { + val expression = parseAlgebraicExpression("3.14") + + val polynomial = expression.reduceToPolynomial() + + assertThat(polynomial).isConstantThat().isIrrationalThat().isWithin(1e-5).of(3.14) + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("3.14") + } + + @Test + fun testReduce_variableConstantExpression_returnsSingleTermPolynomial() { + val expression = parseAlgebraicExpression("x") + + val polynomial = expression.reduceToPolynomial() + + assertThat(polynomial).hasTermCountThat().isEqualTo(1) + assertThat(polynomial).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(1) + assertThat(polynomial).term(0).hasVariableCountThat().isEqualTo(1) + assertThat(polynomial).term(0).variable(0).hasNameThat().isEqualTo("x") + assertThat(polynomial).term(0).variable(0).hasPowerThat().isEqualTo(1) + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("x") + } + + @Test + fun testReduce_intTimesVariable_returnsPolynomialWithCoefficient() { + val expression = parseAlgebraicExpression("7*x") + + val polynomial = expression.reduceToPolynomial() + + assertThat(polynomial).hasTermCountThat().isEqualTo(1) + assertThat(polynomial).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(7) + assertThat(polynomial).term(0).hasVariableCountThat().isEqualTo(1) + assertThat(polynomial).term(0).variable(0).hasNameThat().isEqualTo("x") + assertThat(polynomial).term(0).variable(0).hasPowerThat().isEqualTo(1) + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("7x") + } + + @Test + fun testReduce_negativeDecimalTimesVariable_returnsPolynomialWithNegativeCoefficient() { + val expression = parseAlgebraicExpression("-3.14*x") + + val polynomial = expression.reduceToPolynomial() + + assertThat(polynomial).hasTermCountThat().isEqualTo(1) + assertThat(polynomial).term(0).hasCoefficientThat().isIrrationalThat().isWithin(1e-5).of(-3.14) + assertThat(polynomial).term(0).hasVariableCountThat().isEqualTo(1) + assertThat(polynomial).term(0).variable(0).hasNameThat().isEqualTo("x") + assertThat(polynomial).term(0).variable(0).hasPowerThat().isEqualTo(1) + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("-3.14x") + } + + @Test + fun testReduce_twoTimesXImplicitly_returnsPolynomialWithOneTermAndCoefficient() { + val expression = parseAlgebraicExpression("2x") + + val polynomial = expression.reduceToPolynomial() + + assertThat(polynomial).hasTermCountThat().isEqualTo(1) + assertThat(polynomial).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(2) + assertThat(polynomial).term(0).hasVariableCountThat().isEqualTo(1) + assertThat(polynomial).term(0).variable(0).hasNameThat().isEqualTo("x") + assertThat(polynomial).term(0).variable(0).hasPowerThat().isEqualTo(1) + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("2x") + } + + @Test + fun testReduce_zeroX_returnsZeroPolynomial() { + val expression = parseAlgebraicExpression("0x") + + val polynomial = expression.reduceToPolynomial() + + // 0x just becomes 0 (the 'x' is removed). + assertThat(polynomial).isConstantThat().isIntegerThat().isEqualTo(0) + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("0") + } + + @Test + fun testReduce_onePlusTwo_returnsConstantThreePolynomial() { + val expression = parseAlgebraicExpression("1+2") + + val polynomial = expression.reduceToPolynomial() + + // The '1+2' is reduced to a single '3' constant. + assertThat(polynomial).isConstantThat().isIntegerThat().isEqualTo(3) + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("3") + } + + @Test + fun testReduce_xPlusX_returnTwoXPolynomial() { + val expression = parseAlgebraicExpression("x+x") + + val polynomial = expression.reduceToPolynomial() + + // x+x is combined to 2x (like terms are combined). + assertThat(polynomial).hasTermCountThat().isEqualTo(1) + assertThat(polynomial).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(2) + assertThat(polynomial).term(0).hasVariableCountThat().isEqualTo(1) + assertThat(polynomial).term(0).variable(0).hasNameThat().isEqualTo("x") + assertThat(polynomial).term(0).variable(0).hasPowerThat().isEqualTo(1) + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("2x") + } + + @Test + fun testReduce_xPlusOne_returnsXPlusOnePolynomial() { + val expression = parseAlgebraicExpression("x+1") + + val polynomial = expression.reduceToPolynomial() + + // x+1 leads to a two-term polynomial. + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(0) + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("x + 1") + } + + @Test + fun testReduce_onePlusX_returnsXPlusOnePolynomial() { + val expression = parseAlgebraicExpression("1+x") + + val polynomial = expression.reduceToPolynomial() + + // 1+x leads to a two-term polynomial (with 'x' sorted first). + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(0) + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("x + 1") + } + + @Test + fun testReduce_xMinusOne_returnsXMinusOnePolynomial() { + val expression = parseAlgebraicExpression("x-1") + + val polynomial = expression.reduceToPolynomial() + + // x-1 leads to a two-term polynomial. + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-1) + hasVariableCountThat().isEqualTo(0) + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("x - 1") + } + + @Test + fun testReduce_oneMinusX_returnsNegativeXPlusOne() { + val expression = parseAlgebraicExpression("1-x") + + val polynomial = expression.reduceToPolynomial() + + // 1-x leads to a two-term polynomial (note that 'x' is listed first due to sort priority). + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(0) + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("-x + 1") + } + + @Test + fun testReduce_xPlusTwoX_returnsThreeXPolynomial() { + val expression = parseAlgebraicExpression("x+2x") + + val polynomial = expression.reduceToPolynomial() + + // x+2x combines to 3x (since like terms are combined). This also verifies that coefficients are + // correctly combined. + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(3) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("3x") + } + + @Test + fun testReduce_xYPlusYzMinusXzMinusYzPlusThreeXy_returnsFourXyMinusXzPolynomial() { + val expression = parseAlgebraicExpression("xy+yz-xz-yz+3xy") + + val polynomial = expression.reduceToPolynomial() + + // xy+yz-xz-yz+3xy combines to 4xy-xz (eliminated terms are removed). + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(4) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + variable(1).apply { + hasNameThat().isEqualTo("z") + hasPowerThat().isEqualTo(1) + } + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("4xy - xz") + } + + @Test + fun testReduce_xy_returnsXTimesYPolynomial() { + val expression = parseAlgebraicExpression("xy") + + val polynomial = expression.reduceToPolynomial() + + // xy is a single-term, two-variable polynomial. + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("xy") + } + + @Test + fun testReduce_four_timesXPlusTwo_returnsEightXPlusEightPolynomial() { + val expression = parseAlgebraicExpression("4*(x+2)") + + val polynomial = expression.reduceToPolynomial() + + // 4*(x+2) becomes 4x+8 (the constant distributes to each term's coefficient). + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(4) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(8) + hasVariableCountThat().isEqualTo(0) + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("4x + 8") + } + + @Test + fun testReduce_x_timesOnePlusX_returnsXSquaredPlusXPolynomial() { + val expression = parseAlgebraicExpression("x(1+x)") + + val polynomial = expression.reduceToPolynomial() + + // x(1+x) is expanded to x^2+x. + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("x^2 + x") + } + + @Test + fun testReduce_y_timesOnePlusX_returnsXyPlusYPolynomial() { + val expression = parseAlgebraicExpression("y(1+x)") + + val polynomial = expression.reduceToPolynomial() + + // y(1+x) is expanded to xy+y. + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("xy + y") + } + + @Test + fun testReduce_xPlusOne_timesXMinusOne_returnsXSquaredMinusOnePolynomial() { + val expression = parseAlgebraicExpression("(x+1)(x-1)") + + val polynomial = expression.reduceToPolynomial() + + // (x+1)(x-1) expands to x^2-1. + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-1) + hasVariableCountThat().isEqualTo(0) + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("x^2 - 1") + } + + @Test + fun testReduce_xMinusOne_timesXPlusOne_returnsXSquaredMinusOnePolynomial() { + val expression = parseAlgebraicExpression("(x-1)(x+1)") + + val polynomial = expression.reduceToPolynomial() + + // (x-1)(x+1) expands to x^2-1 (demonstrating multiplication commutativity). + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-1) + hasVariableCountThat().isEqualTo(0) + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("x^2 - 1") + } + + @Test + fun testReduce_xPlusOne_timesXPlusOne_returnsXSquaredPlusTwoXPlusOnePolynomial() { + val expression = parseAlgebraicExpression("(x+1)(x+1)") + + val polynomial = expression.reduceToPolynomial() + + // (x+1)(x+1) expands to x^2+2x+1 (binomial multiplication). + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(3) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(2) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + term(2).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(0) + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("x^2 + 2x + 1") + } + + @Test + fun testReduce_twoMinusX_timesThreeXPlusSeven_returnsMinusThreeXSqPlusXPlusFourteenPolynomial() { + val expression = parseAlgebraicExpression("(2-x)(3x+7)") + + val polynomial = expression.reduceToPolynomial() + + // (2-x)(3x+7) expands to -3x^2-x+14 (shows multiplication with x coefficients). + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(3) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-3) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + term(2).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(14) + hasVariableCountThat().isEqualTo(0) + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("-3x^2 - x + 14") + } + + @Test + fun testReduce_xRaisedToTwo_returnsXSquaredPolynomial() { + val expression = parseAlgebraicExpression("x^2") + + val polynomial = expression.reduceToPolynomial() + + // x^2 is treated as the variable 'x' with power 2. + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("x^2") + } + + @Test + fun testReduce_xSquaredPlusXPlusOne_returnsXSquaredPlusXPlusOnePolynomial() { + val expression = parseAlgebraicExpression("x^2+x+1") + + val polynomial = expression.reduceToPolynomial() + + // x^2+x+1 stays the same since no terms can be combined, eliminated, or reordered. + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(3) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + term(2).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(0) + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("x^2 + x + 1") + } + + @Test + fun testReduce_xSquaredPlusXPlusXY_returnsXSquaredPlusXyPlusXPolynomial() { + val expression = parseAlgebraicExpression("x^2+x+xy") + + val polynomial = expression.reduceToPolynomial() + + // x^2+x+xy is treated as the same polynomial, though 'xy' comes before 'x' per sorting rules. + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(3) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + term(2).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("x^2 + xy + x") + } + + @Test + fun testReduce_x_timesXSquared_returnsXCubedPolynomial() { + val expression = parseAlgebraicExpression("xx^2") + + val polynomial = expression.reduceToPolynomial() + + // xx^2 becomes x^3 since like terms are multiplied and simplified. + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(3) + } + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("x^3") + } + + @Test + fun testReduce_xSquared_plusXSquaredY_returnsXSquaredYPlusXSquared() { + val expression = parseAlgebraicExpression("x^2 + x^2y") + + val polynomial = expression.reduceToPolynomial() + + // x^2+x^2y becomes x^2y+x^2 (terms reordered, but nothing should be combined). + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("x^2y + x^2") + } + + @Test + fun testReduce_constant_division_returnsFractionalPolynomial() { + val expression = parseAlgebraicExpression("1/2") + + val polynomial = expression.reduceToPolynomial() + + // Division of constants is actually computed. + assertThat(polynomial).isConstantThat().isEqualTo(ONE_HALF) + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("1/2") + } + + @Test + fun testReduce_decimalConstant_division_returnsIrrationalPolynomial() { + val expression = parseAlgebraicExpression("3.14/2") + + val polynomial = expression.reduceToPolynomial() + + // Division of constants is actually computed. + assertThat(polynomial).isConstantThat().isIrrationalThat().isWithin(1e-5).of(1.57) + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("1.57") + } + + @Test + fun testReduce_x_dividedByZero_returnsNullPolynomial() { + // Dividing by zero is an optional error that needs to be disabled for this check. + val expression = parseAlgebraicExpression("x/0", errorCheckingMode = REQUIRED_ONLY) + + val polynomial = expression.reduceToPolynomial() + + // Cannot divide by zero. + assertThat(polynomial).isNotValidPolynomial() + } + + @Test + fun testReduce_x_dividedByOneMinusOne_returnsNullPolynomial() { + val expression = parseAlgebraicExpression("x/(1-1)") + + val polynomial = expression.reduceToPolynomial() + + // Cannot divide by zero, even in cases when the denominator needs to be evaluated. + assertThat(polynomial).isNotValidPolynomial() + } + + @Test + fun testReduce_x_dividedByXMinusX_returnsNullPolynomial() { + val expression = parseAlgebraicExpression("x/(x-x)") + + val polynomial = expression.reduceToPolynomial() + + // Another division by zero, but more complex. + assertThat(polynomial).isNotValidPolynomial() + } + + @Test + fun testReduce_two_dividedByZero_returnsNullPolynomial() { + // Dividing by zero is an optional error that needs to be disabled for this check. + val expression = parseAlgebraicExpression("2/0", errorCheckingMode = REQUIRED_ONLY) + + val polynomial = expression.reduceToPolynomial() + + // Division by zero is not allowed for purely constant polynomials, either. + assertThat(polynomial).isNotValidPolynomial() + } + + @Test + fun testReduce_x_dividedByTwo_returnsOneHalfXPolynomial() { + val expression = parseAlgebraicExpression("x/2") + + val polynomial = expression.reduceToPolynomial() + + // x/2 is treated as (1/2)x (that is, the variable 'x' with coefficient '1/2'). + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isEqualTo(ONE_HALF) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("(1/2)x") + } + + @Test + fun testReduce_one_dividedByX_returnsNullPolynomial() { + val expression = parseAlgebraicExpression("1/x") + + val polynomial = expression.reduceToPolynomial() + + // Polynomials cannot have negative powers, so dividing by a polynomial isn't valid. + assertThat(polynomial).isNotValidPolynomial() + } + + @Test + fun testReduce_x_dividedByX_returnsOnePolynomial() { + val expression = parseAlgebraicExpression("x/x") + + val polynomial = expression.reduceToPolynomial() + + // x/x is just '1'. + assertThat(polynomial).isConstantThat().isIntegerThat().isEqualTo(1) + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("1") + } + + @Test + fun testReduce_x_dividedByNegativeTwo_returnsNegativeOneHalfXPolynomial() { + val expression = parseAlgebraicExpression("x/-2") + + val polynomial = expression.reduceToPolynomial() + + // x/-2 is treated as (-1/2)x (that is, the variable 'x' with coefficient '-1/2'). + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isEqualTo(-ONE_HALF) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("(-1/2)x") + } + + @Test + fun testReduce_xPlusOne_dividedByTwo_returnsOneHalfXPlusOneHalfPolynomial() { + val expression = parseAlgebraicExpression("(x+1)/2") + + val polynomial = expression.reduceToPolynomial() + + // (x+1)/2 expands to (1/2)x+(1/2), a two-term polynomial. + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isEqualTo(ONE_HALF) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + term(1).apply { + hasCoefficientThat().isEqualTo(ONE_HALF) + hasVariableCountThat().isEqualTo(0) + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("(1/2)x + 1/2") + } + + @Test + fun testReduce_xSquaredPlusX_dividedByX_returnsXPlusOnePolynomial() { + val expression = parseAlgebraicExpression("(x^2+x)/x") + + val polynomial = expression.reduceToPolynomial() + + // (x^2+x)/x becomes x+1 ('x' is factored out). + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(0) + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("x + 1") + } + + @Test + fun testReduce_xyPlusY_dividedByX_returnsNullPolynomial() { + val expression = parseAlgebraicExpression("(xy+y)/x") + + val polynomial = expression.reduceToPolynomial() + + // 'x' cannot be fully factored out of 'xy+y'. + assertThat(polynomial).isNotValidPolynomial() + } + + @Test + fun testReduce_xyPlusY_dividedByY_returnsXPlusOnePolynomial() { + val expression = parseAlgebraicExpression("(xy+y)/y") + + val polynomial = expression.reduceToPolynomial() + + // (xy+y)/y becomes x+1 ('y' is factored out). + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(0) + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("x + 1") + } + + @Test + fun testReduce_xyPlusY_dividedByXy_returnsNullPolynomial() { + val expression = parseAlgebraicExpression("(xy+y)/(xy)") + + val polynomial = expression.reduceToPolynomial() + + // 'xy' cannot be cleanly factored out of 'xy+y'. + assertThat(polynomial).isNotValidPolynomial() + } + + @Test + fun testReduce_xYMinusFiveY_dividedByY_returnsXMinusFivePolynomial() { + val expression = parseAlgebraicExpression("(xy-5y)/y") + + val polynomial = expression.reduceToPolynomial() + + // (xy-5y)/y becomes x-5 (demonstrates that variables become coefficients in such cases). + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-5) + hasVariableCountThat().isEqualTo(0) + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("x - 5") + } + + @Test + fun testReduce_xSquaredMinusOne_dividedByXPlusOne_returnsXMinusOnePolynomial() { + val expression = parseAlgebraicExpression("(x^2-1)/(x+1)") + + val polynomial = expression.reduceToPolynomial() + + // (x^2-1)/(x+1)=x-1. + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-1) + hasVariableCountThat().isEqualTo(0) + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("x - 1") + } + + @Test + fun testReduce_xSquaredMinusOne_dividedByXMinusOne_returnsXPlusOnePolynomial() { + val expression = parseAlgebraicExpression("(x^2-1)/(x-1)") + + val polynomial = expression.reduceToPolynomial() + + // (x^2-1)/(x-1)=x+1. + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(0) + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("x + 1") + } + + @Test + fun testReduce_xSquaredPlusTwoXPlusOne_dividedByXPlusOne_returnsXPlusOnePolynomial() { + val expression = parseAlgebraicExpression("(x^2+2x+1)/(x+1)") + + val polynomial = expression.reduceToPolynomial() + + // (x^2+2x+1)/(x+1)=x+1. + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(0) + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("x + 1") + } + + @Test + fun testReduce_negThreeXSqAddTwentyThreeXSubFourteen_dividedBySevenSubX_retsThreeXSubTwoPoly() { + val expression = parseAlgebraicExpression("(-3x^2+23x-14)/(7-x)") + + val polynomial = expression.reduceToPolynomial() + + // (-3x^2+23x-14)/(7-x)=3x-2 (demonstrates both deriving a non-one coefficient in the quotient, + // and dividing with negative leading terms). + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(3) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-2) + hasVariableCountThat().isEqualTo(0) + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("3x - 2") + } + + @Test + fun testReduce_xSquaredMinusTwoXyPlusYSquared_dividedByXMinusY_returnsXMinusYPolynomial() { + val expression = parseAlgebraicExpression("(x^2-2xy+y^2)/(x-y)") + + val polynomial = expression.reduceToPolynomial() + + // (x^2-2xy+y^2)/(x-y)=x-y (demonstrates factoring out both 'x' and 'y' terms). + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("x - y") + } + + @Test + fun testReduce_xCubedMinusYCubed_dividedByXMinusY_returnsXSquaredPlusXyPlusYSquaredPolynomial() { + val expression = parseAlgebraicExpression("(x^3-y^3)/(x-y)") + + val polynomial = expression.reduceToPolynomial() + + // (x^3-y^3)/(x-y)=x^2+xy+y^2. This demonstrates a more complex case where a new term can appear + // due to the division. This example comes from: + // https://www.kristakingmath.com/blog/predator-prey-systems-ghtcp-5e2r4-427ab. + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(3) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + term(2).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(2) + } + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("x^2 + xy + y^2") + } + + @Test + fun testReduce_xCubedMinusThreeXSqYPlusXySqMinusYCubed_dividedByXMinusYSq_retsXMinusYPoly() { + val expression = parseAlgebraicExpression("(x^3-3x^2y+3xy^2-y^3)/(x-y)^2") + + val polynomial = expression.reduceToPolynomial() + + // (x^3-3x^2y+3xy^2-y^3)/(x-y)^2=x-y (demonstrates dividing a variable term with a power larger + // than 1). + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("x - y") + } + + @Test + fun testReduce_zeroRaisedToZero_returnsOne() { + val expression = parseAlgebraicExpression("0^0") + + val polynomial = expression.reduceToPolynomial() + + // 0^0=1 (for consistency with other 'pow' functions). + assertThat(polynomial).isConstantThat().isIntegerThat().isEqualTo(1) + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("1") + } + + @Test + fun testReduce_zeroRaisedToOne_returnsZero() { + val expression = parseAlgebraicExpression("0^1") + + val polynomial = expression.reduceToPolynomial() + + // 0^1=0. + assertThat(polynomial).isConstantThat().isIntegerThat().isEqualTo(0) + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("0") + } + + @Test + fun testReduce_oneRaisedToZero_returnsOne() { + val expression = parseAlgebraicExpression("1^0") + + val polynomial = expression.reduceToPolynomial() + + // 1^0=1. + assertThat(polynomial).isConstantThat().isIntegerThat().isEqualTo(1) + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("1") + } + + @Test + fun testReduce_twoRaisedToZero_returnsOne() { + val expression = parseAlgebraicExpression("2^0") + + val polynomial = expression.reduceToPolynomial() + + // 2^0=1. + assertThat(polynomial).isConstantThat().isIntegerThat().isEqualTo(1) + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("1") + } + + @Test + fun testReduce_xRaisedToZero_returnsOnePolynomial() { + val expression = parseAlgebraicExpression("x^0") + + val polynomial = expression.reduceToPolynomial() + + // x^0 is just 1 since anything raised to '1' is 1. + assertThat(polynomial).isConstantThat().isIntegerThat().isEqualTo(1) + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("1") + } + + @Test + fun testReduce_xRaisedToOne_returnsXPolynomial() { + val expression = parseAlgebraicExpression("x^1") + + val polynomial = expression.reduceToPolynomial() + + // x^1 is just 'x' (i.e. a polynomial with a variable term 'x' with power '1'). + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("x") + } + + @Test + fun testReduce_xRaisedToNegativeOne_returnsNullPolynomial() { + val expression = parseAlgebraicExpression("x^-1") + + val polynomial = expression.reduceToPolynomial() + + // Polynomials cannot have negative powers. + assertThat(polynomial).isNotValidPolynomial() + } + + @Test + fun testReduce_twoRaisedToQuantityThreeMinusSix_returnsOneEighthPolynomial() { + val expression = parseAlgebraicExpression("2^(3-6)") + + val polynomial = expression.reduceToPolynomial() + + // 2^(3-6) evaluates to 1/8 (i.e. constants can be raised to negative powers). + assertThat(polynomial).isConstantThat().isRationalThat().apply { + hasNegativePropertyThat().isFalse() + hasWholeNumberThat().isEqualTo(0) + hasNumeratorThat().isEqualTo(1) + hasDenominatorThat().isEqualTo(8) + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("1/8") + } + + @Test + fun testReduce_xRaisedToQuantityThreeMinusSix_returnsNullPolynomial() { + val expression = parseAlgebraicExpression("x^(3-6)") + + val polynomial = expression.reduceToPolynomial() + + // Polynomials cannot have negative powers. + assertThat(polynomial).isNotValidPolynomial() + } + + @Test + fun testReduce_negativeTwoXQuantityRaisedToTwo_returnsFourXSquaredPolynomial() { + val expression = parseAlgebraicExpression("(-2x)^2") + + val polynomial = expression.reduceToPolynomial() + + // (-2x)^2=4x^2 (negative term goes away and coefficient is multiplied). + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(4) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("4x^2") + } + + @Test + fun testReduce_negativeTwoXQuantityRaisedToThree_returnsNegativeEightXCubedPolynomial() { + val expression = parseAlgebraicExpression("(-2x)^3") + + val polynomial = expression.reduceToPolynomial() + + // (-2x)^3=-8x^3 (the negative is kept due to an odd power. + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-8) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(3) + } + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("-8x^3") + } + + @Test + fun testReduce_xYRaisedToTwo_returnsXYSquaredPolynomial() { + val expression = parseAlgebraicExpression("xy^2") + + val polynomial = expression.reduceToPolynomial() + + // For 'xy^2' the 'y' will have power '2' and 'x' will have power '1'. This and related tests + // help to verify that exponentiation assigns the power to the correct variable when parsing + // polynomial syntax. + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(2) + } + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("xy^2") + } + + @Test + fun testReduce_yXRaisedToTwo_returnsXSquaredYPolynomial() { + val expression = parseAlgebraicExpression("yx^2") + + val polynomial = expression.reduceToPolynomial() + + // For 'x^2y' the 'x' will have power '2' and 'y' will have power '1'. + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("x^2y") + } + + @Test + fun testReduce_xRaisedToTwoYRaisedToTwo_returnsXSquaredYSquaredPolynomial() { + val expression = parseAlgebraicExpression("x^2y^2") + + val polynomial = expression.reduceToPolynomial() + + // For 'x^2y^2' both variables have power '2'. + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(2) + } + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("x^2y^2") + } + + @Test + fun testReduce_twoRaisedToX_returnsNullPolynomial() { + // Raising to a variable term is an optional error that needs to be disabled for this check. + val expression = parseAlgebraicExpression("2^x", errorCheckingMode = REQUIRED_ONLY) + + val polynomial = expression.reduceToPolynomial() + + // 2^x is not a polynomial since powers must be positive integers. + assertThat(polynomial).isNotValidPolynomial() + } + + @Test + fun testReduce_xRaisedToX_returnsNullPolynomial() { + // Raising to a variable term is an optional error that needs to be disabled for this check. + val expression = parseAlgebraicExpression("x^x", errorCheckingMode = REQUIRED_ONLY) + + val polynomial = expression.reduceToPolynomial() + + // x^x is not a polynomial since powers must be positive integers. + assertThat(polynomial).isNotValidPolynomial() + } + + @Test + fun testReduce_squareRootX_returnsNullPolynomial() { + val expression = parseAlgebraicExpression("sqrt(x)") + + val polynomial = expression.reduceToPolynomial() + + // sqrt(x) is not a polynomial. + assertThat(polynomial).isNotValidPolynomial() + } + + @Test + fun testReduce_squareRootXQuantitySquared_returnsXPolynomial() { + val expression = parseAlgebraicExpression("sqrt(x)^2") + + val polynomial = expression.reduceToPolynomial() + + // This doesn't currently evaluate correctly due to a limitation in the reduction algorithm (it + // can't represent sub-polynomials with fractional powers). + assertThat(polynomial).isNotValidPolynomial() + } + + @Test + fun testReduce_squareRootOfXSquared_returnsXPolynomial() { + val expression = parseAlgebraicExpression("sqrt(x^2)") + + val polynomial = expression.reduceToPolynomial() + + // sqrt(x^2) is simplified to 'x'. + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("x") + } + + @Test + fun testReduce_squareRootOfOnePlusX_returnsNullPolynomial() { + val expression = parseAlgebraicExpression("sqrt(1+x)") + + val polynomial = expression.reduceToPolynomial() + + // 1+x has no square root. + assertThat(polynomial).isNotValidPolynomial() + } + + @Test + fun testReduce_squareRootOfFourXSquared_returnsTwoXPolynomial() { + val expression = parseAlgebraicExpression("sqrt(4x^2)") + + val polynomial = expression.reduceToPolynomial() + + // sqrt(4x^2)=2x. + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(2) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("2x") + } + + @Test + fun testReduce_squareRootOfNegativeFourXSquared_returnsNullPolynomial() { + val expression = parseAlgebraicExpression("sqrt(-4x^2)") + + val polynomial = expression.reduceToPolynomial() + + // sqrt(-4x^2) is not valid since negative even roots result in imaginary results. + assertThat(polynomial).isNotValidPolynomial() + } + + @Test + fun testReduce_squareRootOfXSquaredYSquared_returnsXyPolynomial() { + val expression = parseAlgebraicExpression("√(x^2y^2)") + + val polynomial = expression.reduceToPolynomial() + + // √(x^2y^2) evaluates to xy (i.e. individual variable terms can be extracted and rooted). + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("xy") + } + + @Test + fun testReduce_squareTwoXSquared_returnsIrrationalCoefficientXPolynomial() { + val expression = parseAlgebraicExpression("√(2x^2)") + + val polynomial = expression.reduceToPolynomial() + + // √(2x^2) evaluates to a polynomial with a decimal coefficient. + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIrrationalThat().isWithin(1e-5).of(1.414213562) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + } + assertThat(polynomial).evaluatesToPlainTextThat().matches("1.414\\d+x") + } + + @Test + fun testReduce_sixteenXToTheFourth_raisedToOneFourth_returnsTwoXPolynomial() { + val expression = parseAlgebraicExpression("((2x)^4)^(1/4)") + + val polynomial = expression.reduceToPolynomial() + + // ((2x)^4)^(1/4)=2x (demonstrates root-based operations with exponentiation). + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(2) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("2x") + } + + @Test + fun testReduce_negativeSixteenXToTheFourth_raisedToOneFourth_returnsNullPolynomial() { + val expression = parseAlgebraicExpression("(-16x^4)^(1/4)") + + val polynomial = expression.reduceToPolynomial() + + // (-16x^4)^(1/4) is not valid since negative even roots result in imaginary results. + assertThat(polynomial).isNotValidPolynomial() + } + + @Test + fun testReduce_negativeTwentySevenYCubed_raisedToOneThird_returnsNegativeThreeXPolynomial() { + val expression = parseAlgebraicExpression("(-27y^3)^(1/3)") + + val polynomial = expression.reduceToPolynomial() + + // (-27y^3)^(1/3)=-3y (shows that odd roots can accept real-valued negative radicands). + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-3) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("-3y") + } + + @Test + fun testReduce_xSquared_raisedToOneHalf_returnsXPolynomial() { + val expression = parseAlgebraicExpression("(x^2)^(1/2)") + + val polynomial = expression.reduceToPolynomial() + + // (x^2)^(1/2) simplifies to just 'x'. + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("x") + } + + @Test + fun testReduce_xToTheOneHalf_squared_returnsXPolynomial() { + val expression = parseAlgebraicExpression("(x^(1/2))^2") + + val polynomial = expression.reduceToPolynomial() + + // This doesn't currently evaluate correctly due to a limitation in the reduction algorithm (it + // can't represent sub-polynomials with fractional powers). + assertThat(polynomial).isNotValidPolynomial() + } + + @Test + fun testReduce_xCubed_raisedToOneThird_returnsXPolynomial() { + val expression = parseAlgebraicExpression("(x^3)^(1/3)") + + val polynomial = expression.reduceToPolynomial() + + // (x^3)^(1/3) simplifies to just 'x'. + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("x") + } + + @Test + fun testReduce_xCubed_raisedToTwoThirds_returnsXSquaredPolynomial() { + val expression = parseAlgebraicExpression("(x^3)^(2/3)") + + val polynomial = expression.reduceToPolynomial() + + // (x^3)^(2/3) simplifies to 'x^2'. + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("x^2") + } + + @Test + fun testReduce_xToTheOneThird_cubed_returnsXPolynomial() { + val expression = parseAlgebraicExpression("(x^(1/3))^3") + + val polynomial = expression.reduceToPolynomial() + + // This doesn't currently evaluate correctly due to a limitation in the reduction algorithm (it + // can't represent sub-polynomials with fractional powers). + assertThat(polynomial).isNotValidPolynomial() + } + + @Test + fun testReduce_xPlusOne_squared_returnsXSquaredPlusTwoXPlusOnePolynomial() { + val expression = parseAlgebraicExpression("(x+1)^2") + + val polynomial = expression.reduceToPolynomial() + + // (x+1)^2=x^2+2x+1 (simple binomial multiplication). + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(3) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(2) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + term(2).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(0) + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("x^2 + 2x + 1") + } + + @Test + fun testReduce_xPlusOne_cubed_returnsXCubedPlusThreeXSquaredPlusThreeXPlusOnePolynomial() { + val expression = parseAlgebraicExpression("(x+1)^3") + + val polynomial = expression.reduceToPolynomial() + + // (x+1)^3=x^3+3x^2+3x+1 (simple binomial multiplication per Pascal's triangle). + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(4) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(3) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(3) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + term(2).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(3) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + term(3).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(0) + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("x^3 + 3x^2 + 3x + 1") + } + + @Test + fun testReduce_xMinusYCubed_returnsXCubedMinusThreeXSqYPlusThreeXYSqMinusYCubedPolynomial() { + val expression = parseAlgebraicExpression("(x-y)^3") + + val polynomial = expression.reduceToPolynomial() + + // (x-y)^3=x^3-3x^2y+3xy^2-y^3 (show that exponentiation works with double variable terms, too). + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(4) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(3) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-3) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + term(2).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(3) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(2) + } + } + term(3).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(3) + } + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("x^3 - 3x^2y + 3xy^2 - y^3") + } + + @Test + fun testReduce_xSquaredPlusTwoXPlusOne_raisedToOneHalf_returnsNullPolynomial() { + val expression = parseAlgebraicExpression("(x^2+2x+1)^(1/2)") + + val polynomial = expression.reduceToPolynomial() + + // While (x^2+2x+1)^(1/2) can technically be factored to (x+1), the system doesn't yet support + // factoring polynomials via roots. + assertThat(polynomial).isNotValidPolynomial() + } + + @Test + fun testReduce_xRaisedToTwoPlusTwo_returnsXToTheFourthPolynomial() { + val expression = parseAlgebraicExpression("x^(2+2)") + + val polynomial = expression.reduceToPolynomial() + + // x^(2+2)=x^4 (the exponent is evaluated). + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(4) + } + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("x^4") + } + + @Test + fun testReduce_xRaisedToTwoMinusTwo_returnsOnePolynomial() { + val expression = parseAlgebraicExpression("x^(2-2)") + + val polynomial = expression.reduceToPolynomial() + + // x^(2-2)=1 (since 2-2 evaluates to 0, and x^0 is 1). + assertThat(polynomial).isConstantThat().isIntegerThat().isEqualTo(1) + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("1") + } + + @Test + fun testReduce_moreComplexArithmeticExpression_returnsCorrectlyComputedCoefficientsPolynomial() { + val expression = parseAlgebraicExpression("133+3.14*x/(11-15)^2") + + val polynomial = expression.reduceToPolynomial() + + // 133+3.14*x/(11-15)^2 simplifies to 0.19625x+133. + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIrrationalThat().isWithin(1e-5).of(0.19625) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(133) + hasVariableCountThat().isEqualTo(0) + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("0.19625x + 133") + } + + /* + * Tests to verify that ordering matches https://en.wikipedia.org/wiki/Polynomial#Definition + * (where multiple variables are sorted lexicographically). + */ + + @Test + fun testReduce_xCubedPlusXSquaredPlusXPlusOne_returnsSameOrderPolynomial() { + val expression = parseAlgebraicExpression("x^3+x^2+x+1") + + val polynomial = expression.reduceToPolynomial() + + // x^3+x^2+x+1 retains its order since higher power terms are ordered first. + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(4) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(3) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + term(2).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + term(3).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(0) + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("x^3 + x^2 + x + 1") + } + + @Test + fun testReduce_onePlusXPlusXSquaredPlusXCubed_returnsXCubedPlusXSquaredPlusXPlusOnePolynomial() { + val expression = parseAlgebraicExpression("1+x+x^2+x^3") + + val polynomial = expression.reduceToPolynomial() + + // 1+x+x^2+x^3 is reversed to x^3+x^2+x+1 since higher power terms are ordered first. + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(4) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(3) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + term(2).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + term(3).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(0) + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("x^3 + x^2 + x + 1") + } + + @Test + fun testReduce_xyPlusXzPlusYz_returnsSameOrderPolynomial() { + val expression = parseAlgebraicExpression("xy+xz+yz") + + val polynomial = expression.reduceToPolynomial() + + // xy+xz+yz retains its order since multivariable terms are ordered lexicographically. + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(3) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + variable(1).apply { + hasNameThat().isEqualTo("z") + hasPowerThat().isEqualTo(1) + } + } + term(2).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + variable(1).apply { + hasNameThat().isEqualTo("z") + hasPowerThat().isEqualTo(1) + } + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("xy + xz + yz") + } + + @Test + fun testReduce_zYPlusZxPlusYX_returnsXyPlusXzPlusYzPolynomial() { + val expression = parseAlgebraicExpression("zy+zx+yx") + + val polynomial = expression.reduceToPolynomial() + + // zy+zx+yx is reversed in ordered and terms to be xy+xz+yz since multivariable terms are + // ordered lexicographically. + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(3) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + variable(1).apply { + hasNameThat().isEqualTo("z") + hasPowerThat().isEqualTo(1) + } + } + term(2).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + variable(1).apply { + hasNameThat().isEqualTo("z") + hasPowerThat().isEqualTo(1) + } + } + } + assertThat(polynomial).evaluatesToPlainTextThat().isEqualTo("xy + xz + yz") + } + + @Test + fun testReduce_complexMultiVariableOutOfOrderExpression_returnsCorrectlyOrderedPolynomial() { + val expression = parseAlgebraicExpression("3+y+x+yx+x^2y+x^2y^2+y^2x") + + val polynomial = expression.reduceToPolynomial() + + // 3+y+x+yx+x^2y+x^2y^2+y^2x is sorted to: x^2y^2+x^2y+xy^2+xy+x+y+3 per term sorting rules. + // ordered lexicographically. + assertThat(polynomial).apply { + hasTermCountThat().isEqualTo(7) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(2) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + term(2).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(2) + } + } + term(3).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + term(4).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + term(5).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + term(6).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(3) + hasVariableCountThat().isEqualTo(0) + } + } + assertThat(polynomial) + .evaluatesToPlainTextThat() + .isEqualTo("x^2y^2 + x^2y + xy^2 + xy + x + y + 3") + } + + @Test + fun testEquals_twoPolynomial_twoPolynomial_areEqual() { + val polynomial1 = parsePolynomialFromAlgebraicExpression("2") + val polynomial2 = parsePolynomialFromAlgebraicExpression("2") + + assertThat(polynomial1).isEqualTo(polynomial2) + } + + @Test + fun testEquals_zeroPolynomial_negativeZeroPolynomial_areEqual() { + val polynomial1 = parsePolynomialFromAlgebraicExpression("0") + val polynomial2 = parsePolynomialFromAlgebraicExpression("-0") + + assertThat(polynomial1).isEqualTo(polynomial2) + } + + @Test + fun testEquals_twoPolynomial_negativeTwoPolynomial_areNotEqual() { + val polynomial1 = parsePolynomialFromAlgebraicExpression("2") + val polynomial2 = parsePolynomialFromAlgebraicExpression("-2") + + assertThat(polynomial1).isNotEqualTo(polynomial2) + } + + @Test + fun testEquals_onePlusTwoPolynomial_threePolynomial_areEqual() { + val polynomial1 = parsePolynomialFromAlgebraicExpression("1+2") + val polynomial2 = parsePolynomialFromAlgebraicExpression("3") + + assertThat(polynomial1).isEqualTo(polynomial2) + } + + @Test + fun testEquals_threePolynomial_onePlusTwoPolynomial_areEqual() { + val polynomial1 = parsePolynomialFromAlgebraicExpression("3") + val polynomial2 = parsePolynomialFromAlgebraicExpression("1+2") + + assertThat(polynomial1).isEqualTo(polynomial2) + } + + @Test + fun testEquals_oneMinusTwoPolynomial_negativeOnePolynomial_areEqual() { + val polynomial1 = parsePolynomialFromAlgebraicExpression("1-2") + val polynomial2 = parsePolynomialFromAlgebraicExpression("-1") + + assertThat(polynomial1).isEqualTo(polynomial2) + } + + @Test + fun testEquals_twoTimesSixPolynomial_sixPolynomial_areEqual() { + val polynomial1 = parsePolynomialFromAlgebraicExpression("2*3") + val polynomial2 = parsePolynomialFromAlgebraicExpression("6") + + assertThat(polynomial1).isEqualTo(polynomial2) + } + + @Test + fun testEquals_twoRaisedToThreePolynomial_eightPolynomial_areEqual() { + val polynomial1 = parsePolynomialFromAlgebraicExpression("2^3") + val polynomial2 = parsePolynomialFromAlgebraicExpression("8") + + assertThat(polynomial1).isEqualTo(polynomial2) + } + + @Test + fun testEquals_xPolynomial_xPolynomial_areEqual() { + val polynomial1 = parsePolynomialFromAlgebraicExpression("x") + val polynomial2 = parsePolynomialFromAlgebraicExpression("x") + + assertThat(polynomial1).isEqualTo(polynomial2) + } + + @Test + fun testEquals_xPolynomial_twoPolynomial_areNotEqual() { + val polynomial1 = parsePolynomialFromAlgebraicExpression("x") + val polynomial2 = parsePolynomialFromAlgebraicExpression("2") + + assertThat(polynomial1).isNotEqualTo(polynomial2) + } + + @Test + fun testEquals_onePlusXPolynomial_xPlusOnePolynomial_areEqual() { + val polynomial1 = parsePolynomialFromAlgebraicExpression("1+x") + val polynomial2 = parsePolynomialFromAlgebraicExpression("x+1") + + // Demonstrate that commutativity doesn't matter (for addition). + assertThat(polynomial1).isEqualTo(polynomial2) + } + + @Test + fun testEquals_xPlusYPolynomial_yPlusXPolynomial_areEqual() { + val polynomial1 = parsePolynomialFromAlgebraicExpression("x+y") + val polynomial2 = parsePolynomialFromAlgebraicExpression("y+x") + + // Commutativity doesn't change for variable ordering. + assertThat(polynomial1).isEqualTo(polynomial2) + } + + @Test + fun testEquals_oneMinusXPolynomial_xMinusOnePolynomial_areNotEqual() { + val polynomial1 = parsePolynomialFromAlgebraicExpression("1-x") + val polynomial2 = parsePolynomialFromAlgebraicExpression("x-1") + + // Subtraction is not commutative. + assertThat(polynomial1).isNotEqualTo(polynomial2) + } + + @Test + fun testEquals_twoXPolynomial_xTimesTwoPolynomial_areEqual() { + val polynomial1 = parsePolynomialFromAlgebraicExpression("2x") + val polynomial2 = parsePolynomialFromAlgebraicExpression("x*2") + + // Demonstrate that commutativity doesn't matter (for multiplication). + assertThat(polynomial1).isEqualTo(polynomial2) + } + + @Test + fun testEquals_twoDividedByXPolynomial_xDividedByTwoPolynomial_areNotEqual() { + val polynomial1 = parsePolynomialFromAlgebraicExpression("2/x") + val polynomial2 = parsePolynomialFromAlgebraicExpression("x/2") + + // Division is not commutative. + assertThat(polynomial1).isNotEqualTo(polynomial2) + } + + @Test + fun testEquals_xTimesQuantityXPlusOnePolynomial_xSquaredPlusXPolynomial_areEqual() { + val polynomial1 = parsePolynomialFromAlgebraicExpression("x(x+1)") + val polynomial2 = parsePolynomialFromAlgebraicExpression("x^2+x") + + // Multiplication is expanded. + assertThat(polynomial1).isEqualTo(polynomial2) + } + + @Test + fun testEquals_threeXSubTwoTimesSevenSubX_minusThreeXSqAddTwentyThreeXSubFourteen_areEqual() { + val polynomial1 = parsePolynomialFromAlgebraicExpression("(3x-2)(7-x)") + val polynomial2 = parsePolynomialFromAlgebraicExpression("-3x^2+23x-14") + + // Multiplication is expanded. + assertThat(polynomial1).isEqualTo(polynomial2) + } + + @Test + fun testEquals_quantityXPlusOneSquaredPolynomial_xSquaredPlusTwoXPlusOnePolynomial_areEqual() { + val polynomial1 = parsePolynomialFromAlgebraicExpression("(x+1)^2") + val polynomial2 = parsePolynomialFromAlgebraicExpression("x^2+2x+1") + + // Exponentiation is expanded. + assertThat(polynomial1).isEqualTo(polynomial2) + } + + @Test + fun testEquals_quantityXPlusOneDividedByTwoPolynomial_oneHalfXPlusOneHalfPolynomial_areEqual() { + val polynomial1 = parsePolynomialFromAlgebraicExpression("(x+1)/2") + val polynomial2 = parsePolynomialFromAlgebraicExpression("x/2+(1/2)") + + // Division distributes. + assertThat(polynomial1).isEqualTo(polynomial2) + } + + @Test + fun testEquals_squareRootOnePlusOnePolynomial_squareRootTwoPolynomial_areEqual() { + val polynomial1 = parsePolynomialFromAlgebraicExpression("sqrt(1+1)") + val polynomial2 = parsePolynomialFromAlgebraicExpression("sqrt(2)") + + // The two are equal after evaluation (to contrast with comparable operations). + assertThat(polynomial1).isEqualTo(polynomial2) + } + + @Test + fun testEquals_squareRootTwoPolynomial_squareRootThreePolynomial_areNotEqual() { + val polynomial1 = parsePolynomialFromAlgebraicExpression("sqrt(2)") + val polynomial2 = parsePolynomialFromAlgebraicExpression("sqrt(3)") + + // The evaluated constants are actually different. + assertThat(polynomial1).isNotEqualTo(polynomial2) + } + + @Test + fun testEquals_squareRootTwoXSquaredPolynomial_twoXSquaredToOneHalfPolynomial_areEqual() { + val polynomial1 = parsePolynomialFromAlgebraicExpression("sqrt(2x^2)") + val polynomial2 = parsePolynomialFromAlgebraicExpression("(2x^2)^(1/2)") + + // sqrt() is the same as raising to 1/2. + assertThat(polynomial1).isEqualTo(polynomial2) + } + + @Test + fun testEquals_complexPolynomial_samePolynomialInDifferentOrder_areEqual() { + val polynomial1 = parsePolynomialFromAlgebraicExpression("3+y+x+yx+x^2y+x^2y^2+y^2x") + val polynomial2 = parsePolynomialFromAlgebraicExpression("xy+xy^2+x^2y+y^2x^2+3+x+y") + + // Order doesn't matter. + assertThat(polynomial1).isEqualTo(polynomial2) + } + + private fun parsePolynomialFromAlgebraicExpression(expression: String) = + parseAlgebraicExpression(expression).reduceToPolynomial() + + private companion object { + private fun parseAlgebraicExpression( + expression: String, errorCheckingMode: ErrorCheckingMode = ALL_ERRORS + ): MathExpression { + return MathExpressionParser.parseAlgebraicExpression( + expression, allowedVariables = listOf("x", "y", "z"), errorCheckingMode + ).getExpectedSuccess() + } + + private inline fun MathParsingResult.getExpectedSuccess(): T { + assertThat(this).isInstanceOf(MathParsingResult.Success::class.java) + return (this as MathParsingResult.Success).result + } + } +} diff --git a/utility/src/test/java/org/oppia/android/util/math/ExpressionToPolynomialTest.kt b/utility/src/test/java/org/oppia/android/util/math/ExpressionToPolynomialTest.kt deleted file mode 100644 index b66c801e1ba..00000000000 --- a/utility/src/test/java/org/oppia/android/util/math/ExpressionToPolynomialTest.kt +++ /dev/null @@ -1,945 +0,0 @@ -package org.oppia.android.util.math - -import androidx.test.ext.junit.runners.AndroidJUnit4 -import org.junit.Test -import org.junit.runner.RunWith -import org.oppia.android.app.model.MathExpression -import org.oppia.android.testing.math.PolynomialSubject.Companion.assertThat -import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode -import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult -import org.robolectric.annotation.LooperMode - -/** Tests for [MathExpressionParser]. */ -@RunWith(AndroidJUnit4::class) -@LooperMode(LooperMode.Mode.PAUSED) -class ExpressionToPolynomialTest { - // TODO: add high-level checks for the three types, but don't test in detail since there are - // separate suites. Also, document the separate suites' existence in this suites's KDoc. - - @Test - fun testPolynomials() { - // TODO: split up & move to separate test suites. Finish test cases (if anymore are needed). - - val poly1 = parseNumericExpressionSuccessfully("1").toPolynomial() - assertThat(poly1).evaluatesToPlainTextThat().isEqualTo("1") - assertThat(poly1).isConstantThat().isIntegerThat().isEqualTo(1) - - val poly13 = parseNumericExpressionSuccessfully("1-1").toPolynomial() - assertThat(poly13).evaluatesToPlainTextThat().isEqualTo("0") - assertThat(poly13).isConstantThat().isIntegerThat().isEqualTo(0) - - val poly2 = parseNumericExpressionSuccessfully("3 + 4 * 2 / (1 - 5) ^ 2").toPolynomial() - assertThat(poly2).evaluatesToPlainTextThat().isEqualTo("7/2") - assertThat(poly2).isConstantThat().isRationalThat().apply { - hasNegativePropertyThat().isFalse() - hasWholeNumberThat().isEqualTo(3) - hasNumeratorThat().isEqualTo(1) - hasDenominatorThat().isEqualTo(2) - } - - val poly3 = - parseAlgebraicExpressionSuccessfullyWithAllErrors("133+3.14*x/(11-15)^2").toPolynomial() - assertThat(poly3).evaluatesToPlainTextThat().isEqualTo("0.19625x + 133") - assertThat(poly3).hasTermCountThat().isEqualTo(2) - assertThat(poly3).term(0).hasCoefficientThat().isIrrationalThat().isWithin(1e-5).of(0.19625) - assertThat(poly3).term(0).hasVariableCountThat().isEqualTo(1) - assertThat(poly3).term(0).variable(0).hasNameThat().isEqualTo("x") - assertThat(poly3).term(0).variable(0).hasPowerThat().isEqualTo(1) - assertThat(poly3).term(1).hasCoefficientThat().isIntegerThat().isEqualTo(133) - assertThat(poly3).term(1).hasVariableCountThat().isEqualTo(0) - - val poly4 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x^2").toPolynomial() - assertThat(poly4).evaluatesToPlainTextThat().isEqualTo("x^2") - assertThat(poly4).hasTermCountThat().isEqualTo(1) - assertThat(poly4).term(0).hasVariableCountThat().isEqualTo(1) - assertThat(poly4).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(1) - assertThat(poly4).term(0).variable(0).hasNameThat().isEqualTo("x") - assertThat(poly4).term(0).variable(0).hasPowerThat().isEqualTo(2) - - val poly5 = parseAlgebraicExpressionSuccessfullyWithAllErrors("xy+x").toPolynomial() - assertThat(poly5).evaluatesToPlainTextThat().isEqualTo("xy + x") - assertThat(poly5).hasTermCountThat().isEqualTo(2) - assertThat(poly5).term(0).hasVariableCountThat().isEqualTo(2) - assertThat(poly5).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(1) - assertThat(poly5).term(0).variable(0).hasNameThat().isEqualTo("x") - assertThat(poly5).term(0).variable(0).hasPowerThat().isEqualTo(1) - assertThat(poly5).term(0).variable(1).hasNameThat().isEqualTo("y") - assertThat(poly5).term(0).variable(1).hasPowerThat().isEqualTo(1) - assertThat(poly5).term(1).hasVariableCountThat().isEqualTo(1) - assertThat(poly5).term(1).hasCoefficientThat().isIntegerThat().isEqualTo(1) - assertThat(poly5).term(1).variable(0).hasNameThat().isEqualTo("x") - assertThat(poly5).term(1).variable(0).hasPowerThat().isEqualTo(1) - - val poly6 = parseAlgebraicExpressionSuccessfullyWithAllErrors("2x").toPolynomial() - assertThat(poly6).evaluatesToPlainTextThat().isEqualTo("2x") - assertThat(poly6).hasTermCountThat().isEqualTo(1) - assertThat(poly6).term(0).hasVariableCountThat().isEqualTo(1) - assertThat(poly6).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(2) - assertThat(poly6).term(0).variable(0).hasNameThat().isEqualTo("x") - assertThat(poly6).term(0).variable(0).hasPowerThat().isEqualTo(1) - - val poly30 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x+2").toPolynomial() - assertThat(poly30).evaluatesToPlainTextThat().isEqualTo("x + 2") - assertThat(poly30).hasTermCountThat().isEqualTo(2) - assertThat(poly30).term(0).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(1) - hasVariableCountThat().isEqualTo(1) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(1) - } - } - assertThat(poly30).term(1).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(2) - hasVariableCountThat().isEqualTo(0) - } - - val poly29 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x^2-3*x-10").toPolynomial() - assertThat(poly29).evaluatesToPlainTextThat().isEqualTo("x^2 - 3x - 10") - assertThat(poly29).hasTermCountThat().isEqualTo(3) - assertThat(poly29).term(0).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(1) - hasVariableCountThat().isEqualTo(1) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(2) - } - } - assertThat(poly29).term(1).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(-3) - hasVariableCountThat().isEqualTo(1) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(1) - } - } - assertThat(poly29).term(2).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(-10) - hasVariableCountThat().isEqualTo(0) - } - - val poly31 = parseAlgebraicExpressionSuccessfullyWithAllErrors("4*(x+2)").toPolynomial() - assertThat(poly31).evaluatesToPlainTextThat().isEqualTo("4x + 8") - assertThat(poly31).hasTermCountThat().isEqualTo(2) - assertThat(poly31).term(0).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(4) - hasVariableCountThat().isEqualTo(1) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(1) - } - } - assertThat(poly31).term(1).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(8) - hasVariableCountThat().isEqualTo(0) - } - - val poly7 = parseAlgebraicExpressionSuccessfullyWithAllErrors("2xy^2z^3").toPolynomial() - assertThat(poly7).evaluatesToPlainTextThat().isEqualTo("2xy^2z^3") - assertThat(poly7).hasTermCountThat().isEqualTo(1) - assertThat(poly7).term(0).hasVariableCountThat().isEqualTo(3) - assertThat(poly7).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(2) - assertThat(poly7).term(0).variable(0).hasNameThat().isEqualTo("x") - assertThat(poly7).term(0).variable(0).hasPowerThat().isEqualTo(1) - assertThat(poly7).term(0).variable(1).hasNameThat().isEqualTo("y") - assertThat(poly7).term(0).variable(1).hasPowerThat().isEqualTo(2) - assertThat(poly7).term(0).variable(2).hasNameThat().isEqualTo("z") - assertThat(poly7).term(0).variable(2).hasPowerThat().isEqualTo(3) - - // Show that 7+xy+yz-3-xz-yz+3xy-4 combines into 4xy-xz (the eliminated terms should be gone). - val poly8 = parseAlgebraicExpressionSuccessfullyWithAllErrors("xy+yz-xz-yz+3xy").toPolynomial() - assertThat(poly8).evaluatesToPlainTextThat().isEqualTo("4xy - xz") - assertThat(poly8).hasTermCountThat().isEqualTo(2) - assertThat(poly8).term(0).hasVariableCountThat().isEqualTo(2) - assertThat(poly8).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(4) - assertThat(poly8).term(0).variable(0).hasNameThat().isEqualTo("x") - assertThat(poly8).term(0).variable(0).hasPowerThat().isEqualTo(1) - assertThat(poly8).term(0).variable(1).hasNameThat().isEqualTo("y") - assertThat(poly8).term(0).variable(1).hasPowerThat().isEqualTo(1) - assertThat(poly8).term(1).hasVariableCountThat().isEqualTo(2) - assertThat(poly8).term(1).hasCoefficientThat().isIntegerThat().isEqualTo(-1) - assertThat(poly8).term(1).variable(0).hasNameThat().isEqualTo("x") - assertThat(poly8).term(1).variable(0).hasPowerThat().isEqualTo(1) - assertThat(poly8).term(1).variable(1).hasNameThat().isEqualTo("z") - assertThat(poly8).term(1).variable(1).hasPowerThat().isEqualTo(1) - - // x+2x should become 3x since like terms are combined. - val poly9 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x+2x").toPolynomial() - assertThat(poly9).evaluatesToPlainTextThat().isEqualTo("3x") - assertThat(poly9).hasTermCountThat().isEqualTo(1) - assertThat(poly9).term(0).hasVariableCountThat().isEqualTo(1) - assertThat(poly9).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(3) - assertThat(poly9).term(0).variable(0).hasNameThat().isEqualTo("x") - assertThat(poly9).term(0).variable(0).hasPowerThat().isEqualTo(1) - - // xx^2 should become x^3 since like terms are combined. - val poly10 = parseAlgebraicExpressionSuccessfullyWithAllErrors("xx^2").toPolynomial() - assertThat(poly10).evaluatesToPlainTextThat().isEqualTo("x^3") - assertThat(poly10).hasTermCountThat().isEqualTo(1) - assertThat(poly10).term(0).hasVariableCountThat().isEqualTo(1) - assertThat(poly10).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(1) - assertThat(poly10).term(0).variable(0).hasNameThat().isEqualTo("x") - assertThat(poly10).term(0).variable(0).hasPowerThat().isEqualTo(3) - - // No terms in this polynomial should be combined. - val poly11 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x^2+x+1").toPolynomial() - assertThat(poly11).evaluatesToPlainTextThat().isEqualTo("x^2 + x + 1") - assertThat(poly11).hasTermCountThat().isEqualTo(3) - assertThat(poly11).term(0).hasVariableCountThat().isEqualTo(1) - assertThat(poly11).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(1) - assertThat(poly11).term(0).variable(0).hasNameThat().isEqualTo("x") - assertThat(poly11).term(0).variable(0).hasPowerThat().isEqualTo(2) - assertThat(poly11).term(1).hasVariableCountThat().isEqualTo(1) - assertThat(poly11).term(1).hasCoefficientThat().isIntegerThat().isEqualTo(1) - assertThat(poly11).term(1).variable(0).hasNameThat().isEqualTo("x") - assertThat(poly11).term(1).variable(0).hasPowerThat().isEqualTo(1) - assertThat(poly11).term(2).hasVariableCountThat().isEqualTo(0) - assertThat(poly11).term(2).hasCoefficientThat().isIntegerThat().isEqualTo(1) - - // No terms in this polynomial should be combined. - val poly12 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x^2 + x^2y").toPolynomial() - assertThat(poly12).evaluatesToPlainTextThat().isEqualTo("x^2y + x^2") - assertThat(poly12).hasTermCountThat().isEqualTo(2) - assertThat(poly12).term(0).hasVariableCountThat().isEqualTo(2) - assertThat(poly12).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(1) - assertThat(poly12).term(0).variable(0).hasNameThat().isEqualTo("x") - assertThat(poly12).term(0).variable(0).hasPowerThat().isEqualTo(2) - assertThat(poly12).term(0).variable(1).hasNameThat().isEqualTo("y") - assertThat(poly12).term(0).variable(1).hasPowerThat().isEqualTo(1) - assertThat(poly12).term(1).hasVariableCountThat().isEqualTo(1) - assertThat(poly12).term(1).hasCoefficientThat().isIntegerThat().isEqualTo(1) - assertThat(poly12).term(1).variable(0).hasNameThat().isEqualTo("x") - assertThat(poly12).term(1).variable(0).hasPowerThat().isEqualTo(2) - - // Ordering tests. Verify that ordering matches - // https://en.wikipedia.org/wiki/Polynomial#Definition (where multiple variables are sorted - // lexicographically). - - // The order of the terms in this polynomial should be reversed. - val poly14 = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+x+x^2+x^3").toPolynomial() - assertThat(poly14).evaluatesToPlainTextThat().isEqualTo("x^3 + x^2 + x + 1") - assertThat(poly14).hasTermCountThat().isEqualTo(4) - assertThat(poly14).term(0).hasVariableCountThat().isEqualTo(1) - assertThat(poly14).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(1) - assertThat(poly14).term(0).variable(0).hasNameThat().isEqualTo("x") - assertThat(poly14).term(0).variable(0).hasPowerThat().isEqualTo(3) - assertThat(poly14).term(1).hasVariableCountThat().isEqualTo(1) - assertThat(poly14).term(1).hasCoefficientThat().isIntegerThat().isEqualTo(1) - assertThat(poly14).term(1).variable(0).hasNameThat().isEqualTo("x") - assertThat(poly14).term(1).variable(0).hasPowerThat().isEqualTo(2) - assertThat(poly14).term(2).hasVariableCountThat().isEqualTo(1) - assertThat(poly14).term(2).hasCoefficientThat().isIntegerThat().isEqualTo(1) - assertThat(poly14).term(2).variable(0).hasNameThat().isEqualTo("x") - assertThat(poly14).term(2).variable(0).hasPowerThat().isEqualTo(1) - assertThat(poly14).term(3).hasVariableCountThat().isEqualTo(0) - assertThat(poly14).term(3).hasCoefficientThat().isIntegerThat().isEqualTo(1) - - // The order of the terms in this polynomial should be preserved. - val poly15 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x^3+x^2+x+1").toPolynomial() - assertThat(poly15).evaluatesToPlainTextThat().isEqualTo("x^3 + x^2 + x + 1") - assertThat(poly15).hasTermCountThat().isEqualTo(4) - assertThat(poly15).term(0).hasVariableCountThat().isEqualTo(1) - assertThat(poly15).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(1) - assertThat(poly15).term(0).variable(0).hasNameThat().isEqualTo("x") - assertThat(poly15).term(0).variable(0).hasPowerThat().isEqualTo(3) - assertThat(poly15).term(1).hasVariableCountThat().isEqualTo(1) - assertThat(poly15).term(1).hasCoefficientThat().isIntegerThat().isEqualTo(1) - assertThat(poly15).term(1).variable(0).hasNameThat().isEqualTo("x") - assertThat(poly15).term(1).variable(0).hasPowerThat().isEqualTo(2) - assertThat(poly15).term(2).hasVariableCountThat().isEqualTo(1) - assertThat(poly15).term(2).hasCoefficientThat().isIntegerThat().isEqualTo(1) - assertThat(poly15).term(2).variable(0).hasNameThat().isEqualTo("x") - assertThat(poly15).term(2).variable(0).hasPowerThat().isEqualTo(1) - assertThat(poly15).term(3).hasVariableCountThat().isEqualTo(0) - assertThat(poly15).term(3).hasCoefficientThat().isIntegerThat().isEqualTo(1) - - // The order of the terms in this polynomial should be reversed. - val poly16 = parseAlgebraicExpressionSuccessfullyWithAllErrors("xy+xz+yz").toPolynomial() - assertThat(poly16).evaluatesToPlainTextThat().isEqualTo("xy + xz + yz") - assertThat(poly16).hasTermCountThat().isEqualTo(3) - assertThat(poly16).term(0).hasVariableCountThat().isEqualTo(2) - assertThat(poly16).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(1) - assertThat(poly16).term(0).variable(0).hasNameThat().isEqualTo("x") - assertThat(poly16).term(0).variable(0).hasPowerThat().isEqualTo(1) - assertThat(poly16).term(0).variable(1).hasNameThat().isEqualTo("y") - assertThat(poly16).term(0).variable(1).hasPowerThat().isEqualTo(1) - assertThat(poly16).term(1).hasVariableCountThat().isEqualTo(2) - assertThat(poly16).term(1).hasCoefficientThat().isIntegerThat().isEqualTo(1) - assertThat(poly16).term(1).variable(0).hasNameThat().isEqualTo("x") - assertThat(poly16).term(1).variable(0).hasPowerThat().isEqualTo(1) - assertThat(poly16).term(1).variable(1).hasNameThat().isEqualTo("z") - assertThat(poly16).term(1).variable(1).hasPowerThat().isEqualTo(1) - assertThat(poly16).term(2).hasVariableCountThat().isEqualTo(2) - assertThat(poly16).term(2).hasCoefficientThat().isIntegerThat().isEqualTo(1) - assertThat(poly16).term(2).variable(0).hasNameThat().isEqualTo("y") - assertThat(poly16).term(2).variable(0).hasPowerThat().isEqualTo(1) - assertThat(poly16).term(2).variable(1).hasNameThat().isEqualTo("z") - assertThat(poly16).term(2).variable(1).hasPowerThat().isEqualTo(1) - - // The order of the terms in this polynomial should be preserved. - val poly17 = parseAlgebraicExpressionSuccessfullyWithAllErrors("yz+xz+xy").toPolynomial() - assertThat(poly17).evaluatesToPlainTextThat().isEqualTo("xy + xz + yz") - assertThat(poly17).hasTermCountThat().isEqualTo(3) - assertThat(poly17).term(0).hasVariableCountThat().isEqualTo(2) - assertThat(poly17).term(0).hasCoefficientThat().isIntegerThat().isEqualTo(1) - assertThat(poly17).term(0).variable(0).hasNameThat().isEqualTo("x") - assertThat(poly17).term(0).variable(0).hasPowerThat().isEqualTo(1) - assertThat(poly17).term(0).variable(1).hasNameThat().isEqualTo("y") - assertThat(poly17).term(0).variable(1).hasPowerThat().isEqualTo(1) - assertThat(poly17).term(1).hasVariableCountThat().isEqualTo(2) - assertThat(poly17).term(1).hasCoefficientThat().isIntegerThat().isEqualTo(1) - assertThat(poly17).term(1).variable(0).hasNameThat().isEqualTo("x") - assertThat(poly17).term(1).variable(0).hasPowerThat().isEqualTo(1) - assertThat(poly17).term(1).variable(1).hasNameThat().isEqualTo("z") - assertThat(poly17).term(1).variable(1).hasPowerThat().isEqualTo(1) - assertThat(poly17).term(2).hasVariableCountThat().isEqualTo(2) - assertThat(poly17).term(2).hasCoefficientThat().isIntegerThat().isEqualTo(1) - assertThat(poly17).term(2).variable(0).hasNameThat().isEqualTo("y") - assertThat(poly17).term(2).variable(0).hasPowerThat().isEqualTo(1) - assertThat(poly17).term(2).variable(1).hasNameThat().isEqualTo("z") - assertThat(poly17).term(2).variable(1).hasPowerThat().isEqualTo(1) - - val poly18 = - parseAlgebraicExpressionSuccessfullyWithAllErrors("3+x+y+xy+x^2y+xy^2+x^2y^2").toPolynomial() - assertThat(poly18).evaluatesToPlainTextThat().isEqualTo("x^2y^2 + x^2y + xy^2 + xy + x + y + 3") - assertThat(poly18).hasTermCountThat().isEqualTo(7) - assertThat(poly18).term(0).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(1) - hasVariableCountThat().isEqualTo(2) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(2) - } - variable(1).apply { - hasNameThat().isEqualTo("y") - hasPowerThat().isEqualTo(2) - } - } - assertThat(poly18).term(1).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(1) - hasVariableCountThat().isEqualTo(2) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(2) - } - variable(1).apply { - hasNameThat().isEqualTo("y") - hasPowerThat().isEqualTo(1) - } - } - assertThat(poly18).term(2).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(1) - hasVariableCountThat().isEqualTo(2) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(1) - } - variable(1).apply { - hasNameThat().isEqualTo("y") - hasPowerThat().isEqualTo(2) - } - } - assertThat(poly18).term(3).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(1) - hasVariableCountThat().isEqualTo(2) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(1) - } - variable(1).apply { - hasNameThat().isEqualTo("y") - hasPowerThat().isEqualTo(1) - } - } - assertThat(poly18).term(4).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(1) - hasVariableCountThat().isEqualTo(1) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(1) - } - } - assertThat(poly18).term(5).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(1) - hasVariableCountThat().isEqualTo(1) - variable(0).apply { - hasNameThat().isEqualTo("y") - hasPowerThat().isEqualTo(1) - } - } - assertThat(poly18).term(6).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(3) - hasVariableCountThat().isEqualTo(0) - } - - // Ensure variables of coefficient and power of 0 are removed. - val poly22 = parseAlgebraicExpressionSuccessfullyWithAllErrors("0x").toPolynomial() - assertThat(poly22).evaluatesToPlainTextThat().isEqualTo("0") - assertThat(poly22).hasTermCountThat().isEqualTo(1) - assertThat(poly22).term(0).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(0) - hasVariableCountThat().isEqualTo(0) - } - - val poly23 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x-x").toPolynomial() - assertThat(poly23).evaluatesToPlainTextThat().isEqualTo("0") - assertThat(poly23).hasTermCountThat().isEqualTo(1) - assertThat(poly23).term(0).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(0) - hasVariableCountThat().isEqualTo(0) - } - - val poly24 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x^0").toPolynomial() - assertThat(poly24).evaluatesToPlainTextThat().isEqualTo("1") - assertThat(poly24).hasTermCountThat().isEqualTo(1) - assertThat(poly24).term(0).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(1) - hasVariableCountThat().isEqualTo(0) - } - - val poly25 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x/x").toPolynomial() - assertThat(poly25).evaluatesToPlainTextThat().isEqualTo("1") - assertThat(poly25).hasTermCountThat().isEqualTo(1) - assertThat(poly25).term(0).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(1) - hasVariableCountThat().isEqualTo(0) - } - - val poly26 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x^(2-2)").toPolynomial() - assertThat(poly26).evaluatesToPlainTextThat().isEqualTo("1") - assertThat(poly26).hasTermCountThat().isEqualTo(1) - assertThat(poly26).term(0).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(1) - hasVariableCountThat().isEqualTo(0) - } - - val poly28 = parseAlgebraicExpressionSuccessfullyWithAllErrors("(x+1)/2").toPolynomial() - assertThat(poly28).evaluatesToPlainTextThat().isEqualTo("(1/2)x + 1/2") - assertThat(poly28).hasTermCountThat().isEqualTo(2) - assertThat(poly28).term(0).apply { - hasCoefficientThat().isRationalThat().apply { - hasNegativePropertyThat().isFalse() - hasWholeNumberThat().isEqualTo(0) - hasNumeratorThat().isEqualTo(1) - hasDenominatorThat().isEqualTo(2) - } - hasVariableCountThat().isEqualTo(1) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(1) - } - } - assertThat(poly28).term(1).apply { - hasCoefficientThat().isRationalThat().apply { - hasNegativePropertyThat().isFalse() - hasWholeNumberThat().isEqualTo(0) - hasNumeratorThat().isEqualTo(1) - hasDenominatorThat().isEqualTo(2) - } - hasVariableCountThat().isEqualTo(0) - } - - // Ensure like terms are combined after polynomial multiplication. - val poly20 = parseAlgebraicExpressionSuccessfullyWithAllErrors("(x-5)(x+2)").toPolynomial() - assertThat(poly20).evaluatesToPlainTextThat().isEqualTo("x^2 - 3x - 10") - assertThat(poly20).hasTermCountThat().isEqualTo(3) - assertThat(poly20).term(0).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(1) - hasVariableCountThat().isEqualTo(1) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(2) - } - } - assertThat(poly20).term(1).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(-3) - hasVariableCountThat().isEqualTo(1) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(1) - } - } - assertThat(poly20).term(2).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(-10) - hasVariableCountThat().isEqualTo(0) - } - - val poly21 = parseAlgebraicExpressionSuccessfullyWithAllErrors("(1+x)^3").toPolynomial() - assertThat(poly21).evaluatesToPlainTextThat().isEqualTo("x^3 + 3x^2 + 3x + 1") - assertThat(poly21).hasTermCountThat().isEqualTo(4) - assertThat(poly21).term(0).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(1) - hasVariableCountThat().isEqualTo(1) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(3) - } - } - assertThat(poly21).term(1).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(3) - hasVariableCountThat().isEqualTo(1) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(2) - } - } - assertThat(poly21).term(2).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(3) - hasVariableCountThat().isEqualTo(1) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(1) - } - } - assertThat(poly21).term(3).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(1) - hasVariableCountThat().isEqualTo(0) - } - - val poly27 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x^2*y^2 + 2").toPolynomial() - assertThat(poly27).evaluatesToPlainTextThat().isEqualTo("x^2y^2 + 2") - assertThat(poly27).hasTermCountThat().isEqualTo(2) - assertThat(poly27).term(0).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(1) - hasVariableCountThat().isEqualTo(2) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(2) - } - variable(1).apply { - hasNameThat().isEqualTo("y") - hasPowerThat().isEqualTo(2) - } - } - assertThat(poly27).term(1).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(2) - hasVariableCountThat().isEqualTo(0) - } - - val poly32 = - parseAlgebraicExpressionSuccessfullyWithAllErrors("(x^2-3*x-10)*(x+2)").toPolynomial() - assertThat(poly32).evaluatesToPlainTextThat().isEqualTo("x^3 - x^2 - 16x - 20") - assertThat(poly32).hasTermCountThat().isEqualTo(4) - assertThat(poly32).term(0).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(1) - hasVariableCountThat().isEqualTo(1) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(3) - } - } - assertThat(poly32).term(1).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(-1) - hasVariableCountThat().isEqualTo(1) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(2) - } - } - assertThat(poly32).term(2).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(-16) - hasVariableCountThat().isEqualTo(1) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(1) - } - } - assertThat(poly32).term(3).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(-20) - hasVariableCountThat().isEqualTo(0) - } - - val poly33 = parseAlgebraicExpressionSuccessfullyWithAllErrors("(x-y)^3").toPolynomial() - assertThat(poly33).evaluatesToPlainTextThat().isEqualTo("x^3 - 3x^2y + 3xy^2 - y^3") - assertThat(poly33).hasTermCountThat().isEqualTo(4) - assertThat(poly33).term(0).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(1) - hasVariableCountThat().isEqualTo(1) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(3) - } - } - assertThat(poly33).term(1).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(-3) - hasVariableCountThat().isEqualTo(2) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(2) - } - variable(1).apply { - hasNameThat().isEqualTo("y") - hasPowerThat().isEqualTo(1) - } - } - assertThat(poly33).term(2).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(3) - hasVariableCountThat().isEqualTo(2) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(1) - } - variable(1).apply { - hasNameThat().isEqualTo("y") - hasPowerThat().isEqualTo(2) - } - } - assertThat(poly33).term(3).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(-1) - hasVariableCountThat().isEqualTo(1) - variable(0).apply { - hasNameThat().isEqualTo("y") - hasPowerThat().isEqualTo(3) - } - } - - // Ensure polynomial division works. - val poly19 = - parseAlgebraicExpressionSuccessfullyWithAllErrors("(x^2-3*x-10)/(x+2)").toPolynomial() - assertThat(poly19).evaluatesToPlainTextThat().isEqualTo("x - 5") - assertThat(poly19).hasTermCountThat().isEqualTo(2) - assertThat(poly19).term(0).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(1) - hasVariableCountThat().isEqualTo(1) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(1) - } - } - assertThat(poly19).term(1).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(-5) - hasVariableCountThat().isEqualTo(0) - } - - val poly35 = parseAlgebraicExpressionSuccessfullyWithAllErrors("(xy-5y)/y").toPolynomial() - assertThat(poly35).evaluatesToPlainTextThat().isEqualTo("x - 5") - assertThat(poly35).hasTermCountThat().isEqualTo(2) - assertThat(poly35).term(0).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(1) - hasVariableCountThat().isEqualTo(1) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(1) - } - } - assertThat(poly35).term(1).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(-5) - hasVariableCountThat().isEqualTo(0) - } - - val poly36 = - parseAlgebraicExpressionSuccessfullyWithAllErrors("(x^2-2xy+y^2)/(x-y)").toPolynomial() - assertThat(poly36).evaluatesToPlainTextThat().isEqualTo("x - y") - assertThat(poly36).hasTermCountThat().isEqualTo(2) - assertThat(poly36).term(0).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(1) - hasVariableCountThat().isEqualTo(1) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(1) - } - } - assertThat(poly36).term(1).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(-1) - hasVariableCountThat().isEqualTo(1) - variable(0).apply { - hasNameThat().isEqualTo("y") - hasPowerThat().isEqualTo(1) - } - } - - // Example from https://www.kristakingmath.com/blog/predator-prey-systems-ghtcp-5e2r4-427ab. - val poly37 = parseAlgebraicExpressionSuccessfullyWithAllErrors("(x^3-y^3)/(x-y)").toPolynomial() - assertThat(poly37).evaluatesToPlainTextThat().isEqualTo("x^2 + xy + y^2") - assertThat(poly37).hasTermCountThat().isEqualTo(3) - assertThat(poly37).term(0).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(1) - hasVariableCountThat().isEqualTo(1) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(2) - } - } - assertThat(poly37).term(1).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(1) - hasVariableCountThat().isEqualTo(2) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(1) - } - variable(1).apply { - hasNameThat().isEqualTo("y") - hasPowerThat().isEqualTo(1) - } - } - assertThat(poly37).term(2).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(1) - hasVariableCountThat().isEqualTo(1) - variable(0).apply { - hasNameThat().isEqualTo("y") - hasPowerThat().isEqualTo(2) - } - } - - // Multi-variable & more complex division. - val poly34 = - parseAlgebraicExpressionSuccessfullyWithAllErrors( - "(x^3-3x^2y+3xy^2-y^3)/(x-y)^2" - ).toPolynomial() - assertThat(poly34).evaluatesToPlainTextThat().isEqualTo("x - y") - assertThat(poly34).hasTermCountThat().isEqualTo(2) - assertThat(poly34).term(0).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(1) - hasVariableCountThat().isEqualTo(1) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(1) - } - } - assertThat(poly34).term(1).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(-1) - hasVariableCountThat().isEqualTo(1) - variable(0).apply { - hasNameThat().isEqualTo("y") - hasPowerThat().isEqualTo(1) - } - } - - val poly38 = parseNumericExpressionSuccessfully("2^-4").toPolynomial() - assertThat(poly38).evaluatesToPlainTextThat().isEqualTo("1/16") - assertThat(poly38).hasTermCountThat().isEqualTo(1) - assertThat(poly38).term(0).apply { - hasCoefficientThat().isRationalThat().apply { - hasNegativePropertyThat().isFalse() - hasWholeNumberThat().isEqualTo(0) - hasNumeratorThat().isEqualTo(1) - hasDenominatorThat().isEqualTo(16) - } - hasVariableCountThat().isEqualTo(0) - } - - val poly39 = parseNumericExpressionSuccessfully("2^(3-6)").toPolynomial() - assertThat(poly39).evaluatesToPlainTextThat().isEqualTo("1/8") - assertThat(poly39).hasTermCountThat().isEqualTo(1) - assertThat(poly39).term(0).apply { - hasCoefficientThat().isRationalThat().apply { - hasNegativePropertyThat().isFalse() - hasWholeNumberThat().isEqualTo(0) - hasNumeratorThat().isEqualTo(1) - hasDenominatorThat().isEqualTo(8) - } - hasVariableCountThat().isEqualTo(0) - } - - // x^-3 is not a valid polynomial (since polynomials can't have negative powers). - val poly40 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x^(3-6)").toPolynomial() - assertThat(poly40).isNotValidPolynomial() - - // 2^x is not a polynomial. - val poly41 = parseAlgebraicExpressionSuccessfullyWithoutOptionalErrors("2^x").toPolynomial() - assertThat(poly41).isNotValidPolynomial() - - // 1/x is not a polynomial. - val poly42 = parseAlgebraicExpressionSuccessfullyWithoutOptionalErrors("1/x").toPolynomial() - assertThat(poly42).isNotValidPolynomial() - - val poly43 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x/2").toPolynomial() - assertThat(poly43).evaluatesToPlainTextThat().isEqualTo("(1/2)x") - assertThat(poly43).hasTermCountThat().isEqualTo(1) - assertThat(poly43).term(0).apply { - hasCoefficientThat().isRationalThat().apply { - hasNegativePropertyThat().isFalse() - hasWholeNumberThat().isEqualTo(0) - hasNumeratorThat().isEqualTo(1) - hasDenominatorThat().isEqualTo(2) - } - hasVariableCountThat().isEqualTo(1) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(1) - } - } - - val poly44 = parseAlgebraicExpressionSuccessfullyWithAllErrors("(x-3)/2").toPolynomial() - assertThat(poly44).evaluatesToPlainTextThat().isEqualTo("(1/2)x - 3/2") - assertThat(poly44).hasTermCountThat().isEqualTo(2) - assertThat(poly44).term(0).apply { - hasCoefficientThat().isRationalThat().apply { - hasNegativePropertyThat().isFalse() - hasWholeNumberThat().isEqualTo(0) - hasNumeratorThat().isEqualTo(1) - hasDenominatorThat().isEqualTo(2) - } - hasVariableCountThat().isEqualTo(1) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(1) - } - } - assertThat(poly44).term(1).apply { - hasCoefficientThat().isRationalThat().apply { - hasNegativePropertyThat().isTrue() - hasWholeNumberThat().isEqualTo(1) - hasNumeratorThat().isEqualTo(1) - hasDenominatorThat().isEqualTo(2) - } - hasVariableCountThat().isEqualTo(0) - } - - val poly45 = parseAlgebraicExpressionSuccessfullyWithAllErrors("(x-1)(x+1)").toPolynomial() - assertThat(poly45).evaluatesToPlainTextThat().isEqualTo("x^2 - 1") - assertThat(poly45).hasTermCountThat().isEqualTo(2) - assertThat(poly45).term(0).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(1) - hasVariableCountThat().isEqualTo(1) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(2) - } - } - assertThat(poly45).term(1).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(-1) - hasVariableCountThat().isEqualTo(0) - } - - // √x is not a polynomial. - val poly46 = parseAlgebraicExpressionSuccessfullyWithAllErrors("sqrt(x)").toPolynomial() - assertThat(poly46).isNotValidPolynomial() - - val poly47 = parseAlgebraicExpressionSuccessfullyWithAllErrors("√(x^2)").toPolynomial() - assertThat(poly47).evaluatesToPlainTextThat().isEqualTo("x") - assertThat(poly47).hasTermCountThat().isEqualTo(1) - assertThat(poly47).term(0).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(1) - hasVariableCountThat().isEqualTo(1) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(1) - } - } - - val poly51 = parseAlgebraicExpressionSuccessfullyWithAllErrors("√(x^2y^2)").toPolynomial() - assertThat(poly51).evaluatesToPlainTextThat().isEqualTo("xy") - assertThat(poly51).hasTermCountThat().isEqualTo(1) - assertThat(poly51).term(0).apply { - hasCoefficientThat().isIntegerThat().isEqualTo(1) - hasVariableCountThat().isEqualTo(2) - variable(0).apply { - hasNameThat().isEqualTo("x") - hasPowerThat().isEqualTo(1) - } - variable(1).apply { - hasNameThat().isEqualTo("y") - hasPowerThat().isEqualTo(1) - } - } - - // A limitation in the current polynomial conversion is that sqrt(x) will fail due to it not - // have any polynomial representation. - val poly48 = parseAlgebraicExpressionSuccessfullyWithAllErrors("√x^2").toPolynomial() - assertThat(poly48).isNotValidPolynomial() - - // √(x^2+2) may evaluate to a polynomial, but it requires factoring (which isn't yet supported). - val poly50 = parseAlgebraicExpressionSuccessfullyWithAllErrors("√(x^2+2)").toPolynomial() - assertThat(poly50).isNotValidPolynomial() - - // Division by zero is undefined, so a polynomial can't be constructed. - val poly49 = parseAlgebraicExpressionSuccessfullyWithoutOptionalErrors("(x+2)/0").toPolynomial() - assertThat(poly49).isNotValidPolynomial() - - val poly52 = parsePolynomialFromNumericExpression("1") - val poly53 = parsePolynomialFromNumericExpression("0") - assertThat(poly52).isNotEqualTo(poly53) - - val poly54 = parsePolynomialFromNumericExpression("1+2") - val poly55 = parsePolynomialFromNumericExpression("3") - assertThat(poly54).isEqualTo(poly55) - - val poly56 = parsePolynomialFromNumericExpression("1-2") - val poly57 = parsePolynomialFromNumericExpression("-1") - assertThat(poly56).isEqualTo(poly57) - - val poly58 = parsePolynomialFromNumericExpression("2*3") - val poly59 = parsePolynomialFromNumericExpression("6") - assertThat(poly58).isEqualTo(poly59) - - val poly60 = parsePolynomialFromNumericExpression("2^3") - val poly61 = parsePolynomialFromNumericExpression("8") - assertThat(poly60).isEqualTo(poly61) - - val poly62 = parsePolynomialFromAlgebraicExpression("1+x") - val poly63 = parsePolynomialFromAlgebraicExpression("x+1") - assertThat(poly62).isEqualTo(poly63) - - val poly64 = parsePolynomialFromAlgebraicExpression("y+x") - val poly65 = parsePolynomialFromAlgebraicExpression("x+y") - assertThat(poly64).isEqualTo(poly65) - - val poly66 = parsePolynomialFromAlgebraicExpression("(x+1)^2") - val poly67 = parsePolynomialFromAlgebraicExpression("x^2+2x+1") - assertThat(poly66).isEqualTo(poly67) - - val poly68 = parsePolynomialFromAlgebraicExpression("(x+1)/2") - val poly69 = parsePolynomialFromAlgebraicExpression("x/2+(1/2)") - assertThat(poly68).isEqualTo(poly69) - - val poly70 = parsePolynomialFromAlgebraicExpression("x*2") - val poly71 = parsePolynomialFromAlgebraicExpression("2x") - assertThat(poly70).isEqualTo(poly71) - - val poly72 = parsePolynomialFromAlgebraicExpression("x(x+1)") - val poly73 = parsePolynomialFromAlgebraicExpression("x^2+x") - assertThat(poly72).isEqualTo(poly73) - } - - private fun parsePolynomialFromNumericExpression(expression: String) = - parseNumericExpressionSuccessfully(expression).toPolynomial() - - private fun parsePolynomialFromAlgebraicExpression(expression: String) = - parseAlgebraicExpressionSuccessfullyWithAllErrors(expression).toPolynomial() - - private companion object { - // TODO: fix helper API. - - private fun parseNumericExpressionSuccessfully(expression: String): MathExpression { - val result = parseNumericExpressionWithAllErrors(expression) - return (result as MathParsingResult.Success).result - } - - private fun parseNumericExpressionWithAllErrors( - expression: String - ): MathParsingResult { - return MathExpressionParser.parseNumericExpression(expression, ErrorCheckingMode.ALL_ERRORS) - } - - private fun parseAlgebraicExpressionSuccessfullyWithAllErrors( - expression: String, - allowedVariables: List = listOf("x", "y", "z") - ): MathExpression { - val result = - parseAlgebraicExpressionInternal(expression, ErrorCheckingMode.ALL_ERRORS, allowedVariables) - return (result as MathParsingResult.Success).result - } - - private fun parseAlgebraicExpressionSuccessfullyWithoutOptionalErrors( - expression: String, - allowedVariables: List = listOf("x", "y", "z") - ): MathExpression { - val result = - parseAlgebraicExpressionInternal( - expression, ErrorCheckingMode.REQUIRED_ONLY, allowedVariables - ) - return (result as MathParsingResult.Success).result - } - - private fun parseAlgebraicExpressionInternal( - expression: String, - errorCheckingMode: ErrorCheckingMode, - allowedVariables: List = listOf("x", "y", "z") - ): MathParsingResult { - return MathExpressionParser.parseAlgebraicExpression( - expression, allowedVariables, errorCheckingMode - ) - } - } -} diff --git a/utility/src/test/java/org/oppia/android/util/math/MathExpressionExtensionsTest.kt b/utility/src/test/java/org/oppia/android/util/math/MathExpressionExtensionsTest.kt index 85a734a7ac4..8be3ee05bc3 100644 --- a/utility/src/test/java/org/oppia/android/util/math/MathExpressionExtensionsTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/MathExpressionExtensionsTest.kt @@ -6,6 +6,8 @@ import org.junit.Test import org.junit.runner.RunWith import org.oppia.android.app.model.MathEquation import org.oppia.android.app.model.MathExpression +import org.oppia.android.testing.math.PolynomialSubject +import org.oppia.android.testing.math.PolynomialSubject.Companion.assertThat import org.oppia.android.testing.math.RealSubject.Companion.assertThat import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode.ALL_ERRORS import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult @@ -20,7 +22,8 @@ import org.robolectric.annotation.LooperMode * verifications for operations like LaTeX conversion and expression evaluation are part of more * targeted test suites such as [ExpressionToLatexConverterTest] and * [NumericExpressionEvaluatorTest]. For comparable operations, see - * [ExpressionToComparableOperationConverterTest]. + * [ExpressionToComparableOperationConverterTest]. For polynomials, see + * [ExpressionToPolynomialConverterTest]. */ // FunctionName: test names are conventionally named with underscores. // SameParameterValue: tests should have specific context included/excluded for readability. @@ -95,6 +98,43 @@ class MathExpressionExtensionsTest { assertThat(operation1).isNotEqualTo(operation2) } + @Test + fun testToPolynomial_algebraicExpression_returnsCorrectPolynomial() { + val expression = parseAlgebraicExpression("(x^3-y^3)/(x-y)") + + val polynomial = expression.toPolynomial() + + assertThat(polynomial).hasTermCountThat().isEqualTo(3) + assertThat(polynomial).term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + assertThat(polynomial).term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + assertThat(polynomial).term(2).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(2) + } + } + } + private companion object { private fun parseNumericExpression(expression: String): MathExpression { return parseNumericExpression(expression, ALL_ERRORS).retrieveExpectedSuccessfulResult() diff --git a/utility/src/test/java/org/oppia/android/util/math/PolynomialExtensionsTest.kt b/utility/src/test/java/org/oppia/android/util/math/PolynomialExtensionsTest.kt index 4fac2ae26b0..ed4887261ff 100644 --- a/utility/src/test/java/org/oppia/android/util/math/PolynomialExtensionsTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/PolynomialExtensionsTest.kt @@ -1,6 +1,5 @@ package org.oppia.android.util.math -import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat import org.junit.Test import org.junit.runner.RunWith @@ -9,21 +8,24 @@ import org.oppia.android.app.model.Polynomial import org.oppia.android.app.model.Polynomial.Term import org.oppia.android.app.model.Polynomial.Term.Variable import org.oppia.android.app.model.Real +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Iteration +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Parameter +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.RunParameterized import org.oppia.android.testing.math.RealSubject.Companion.assertThat import org.robolectric.annotation.LooperMode +import org.oppia.android.testing.math.PolynomialSubject.Companion.assertThat /** Tests for [Polynomial] extensions. */ // FunctionName: test names are conventionally named with underscores. @Suppress("FunctionName") -@RunWith(AndroidJUnit4::class) +@RunWith(OppiaParameterizedTestRunner::class) @LooperMode(LooperMode.Mode.PAUSED) class PolynomialExtensionsTest { private companion object { - private const val PI = 3.1415 - - private val ONE_HALF_FRACTION = Fraction.newBuilder().apply { + private val ONE_THIRD_FRACTION = Fraction.newBuilder().apply { numerator = 1 - denominator = 2 + denominator = 3 }.build() private val ONE_AND_ONE_HALF_FRACTION = Fraction.newBuilder().apply { @@ -32,40 +34,64 @@ class PolynomialExtensionsTest { wholeNumber = 1 }.build() - private val ZERO_REAL = Real.newBuilder().apply { - integer = 0 + private val THREE_ONES_FRACTION = Fraction.newBuilder().apply { + numerator = 3 + denominator = 1 }.build() - private val ONE_REAL = Real.newBuilder().apply { - integer = 1 + private val THREE_FRACTION = Fraction.newBuilder().apply { + wholeNumber = 3 + denominator = 1 }.build() private val TWO_REAL = Real.newBuilder().apply { integer = 2 }.build() - private val ONE_HALF_REAL = Real.newBuilder().apply { - rational = ONE_HALF_FRACTION + private val THREE_REAL = Real.newBuilder().apply { + integer = 3 + }.build() + + private val FOUR_REAL = Real.newBuilder().apply { + integer = 4 + }.build() + + private val FIVE_REAL = Real.newBuilder().apply { + integer = 5 + }.build() + + private val SEVEN_REAL = Real.newBuilder().apply { + integer = 7 + }.build() + + private val ONE_THIRD_REAL = Real.newBuilder().apply { + rational = ONE_THIRD_FRACTION }.build() private val ONE_AND_ONE_HALF_REAL = Real.newBuilder().apply { rational = ONE_AND_ONE_HALF_FRACTION }.build() - private val PI_REAL = Real.newBuilder().apply { - irrational = PI + private val THREE_ONES_REAL = Real.newBuilder().apply { + rational = THREE_ONES_FRACTION + }.build() + + private val THREE_FRACTION_REAL = Real.newBuilder().apply { + rational = THREE_FRACTION }.build() - private val ZERO_POLYNOMIAL = createPolynomial(createTerm(coefficient = ZERO_REAL)) + private val PI_REAL = Real.newBuilder().apply { + irrational = 3.14 + }.build() private val TWO_POLYNOMIAL = createPolynomial(createTerm(coefficient = TWO_REAL)) private val NEGATIVE_TWO_POLYNOMIAL = createPolynomial(createTerm(coefficient = -TWO_REAL)) - private val ONE_HALF_POLYNOMIAL = createPolynomial(createTerm(coefficient = ONE_HALF_REAL)) + private val ONE_HALF_POLYNOMIAL = createPolynomial(createTerm(coefficient = ONE_HALF)) private val NEGATIVE_ONE_HALF_POLYNOMIAL = - createPolynomial(createTerm(coefficient = -ONE_HALF_REAL)) + createPolynomial(createTerm(coefficient = -ONE_HALF)) private val ONE_AND_ONE_HALF_POLYNOMIAL = createPolynomial(createTerm(coefficient = ONE_AND_ONE_HALF_REAL)) @@ -78,21 +104,39 @@ class PolynomialExtensionsTest { private val NEGATIVE_PI_POLYNOMIAL = createPolynomial(createTerm(coefficient = -PI_REAL)) private val ONE_X_POLYNOMIAL = - createPolynomial(createTerm(coefficient = ONE_REAL, createVariable(name = "x", power = 1))) + createPolynomial(createTerm(coefficient = ONE, createVariable(name = "x", power = 1))) private val NEGATIVE_ONE_X_POLYNOMIAL = - createPolynomial(createTerm(coefficient = -ONE_REAL, createVariable(name = "x", power = 1))) + createPolynomial(createTerm(coefficient = -ONE, createVariable(name = "x", power = 1))) private val TWO_X_POLYNOMIAL = createPolynomial(createTerm(coefficient = TWO_REAL, createVariable(name = "x", power = 1))) private val ONE_PLUS_X_POLYNOMIAL = createPolynomial( - createTerm(coefficient = ONE_REAL), - createTerm(coefficient = ONE_REAL, createVariable(name = "x", power = 1)) + createTerm(coefficient = ONE), + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)) ) } + @Parameter lateinit var var1: String + @Parameter lateinit var var2: String + @Parameter lateinit var var3: String + + @Test + fun testZeroPolynomial_isEqualToZero() { + val subject = ZERO_POLYNOMIAL + + assertThat(subject).isConstantThat().isIntegerThat().isEqualTo(0) + } + + @Test + fun testOnePolynomial_isEqualToOne() { + val subject = ONE_POLYNOMIAL + + assertThat(subject).isConstantThat().isIntegerThat().isEqualTo(1) + } + @Test fun testIsConstant_default_returnsFalse() { val defaultPolynomial = Polynomial.getDefaultInstance() @@ -175,7 +219,7 @@ class PolynomialExtensionsTest { @Test fun testIsConstant_one_and_two_returnsFalse() { val onePlusTwoPolynomial = - createPolynomial(createTerm(coefficient = ONE_REAL), createTerm(coefficient = TWO_REAL)) + createPolynomial(createTerm(coefficient = ONE), createTerm(coefficient = TWO_REAL)) val result = onePlusTwoPolynomial.isConstant() @@ -223,14 +267,14 @@ class PolynomialExtensionsTest { fun testGetConstant_pi_returnsPi() { val result = PI_POLYNOMIAL.getConstant() - assertThat(result).isIrrationalThat().isWithin(1e-5).of(PI) + assertThat(result).isIrrationalThat().isWithin(1e-5).of(3.14) } @Test fun testGetConstant_negativePi_returnsNegativePi() { val result = NEGATIVE_PI_POLYNOMIAL.getConstant() - assertThat(result).isIrrationalThat().isWithin(1e-5).of(-PI) + assertThat(result).isIrrationalThat().isWithin(1e-5).of(-3.14) } @Test @@ -272,14 +316,14 @@ class PolynomialExtensionsTest { fun testToPlainText_pi_returnsPiString() { val result = PI_POLYNOMIAL.toPlainText() - assertThat(result).isEqualTo("3.1415") + assertThat(result).isEqualTo("3.14") } @Test fun testToPlainText_negativePi_returnsMinusPiString() { val result = NEGATIVE_PI_POLYNOMIAL.toPlainText() - assertThat(result).isEqualTo("-3.1415") + assertThat(result).isEqualTo("-3.14") } @Test @@ -313,8 +357,8 @@ class PolynomialExtensionsTest { @Test fun testToPlainText_oneAndNegativeX_returnsOneMinusXString() { val oneMinusXPolynomial = createPolynomial( - createTerm(coefficient = ONE_REAL), - createTerm(coefficient = -ONE_REAL, createVariable(name = "x", power = 1)) + createTerm(coefficient = ONE), + createTerm(coefficient = -ONE, createVariable(name = "x", power = 1)) ) val result = oneMinusXPolynomial.toPlainText() @@ -326,7 +370,7 @@ class PolynomialExtensionsTest { fun testToPlainText_oneAndOneHalfXAndY_returnsThreeHalvesXPlusYString() { val oneMinusXPolynomial = createPolynomial( createTerm(coefficient = ONE_AND_ONE_HALF_REAL, createVariable(name = "x", power = 1)), - createTerm(coefficient = ONE_REAL, createVariable(name = "y", power = 1)) + createTerm(coefficient = ONE, createVariable(name = "y", power = 1)) ) val result = oneMinusXPolynomial.toPlainText() @@ -337,9 +381,9 @@ class PolynomialExtensionsTest { @Test fun testToPlainText_oneAndXAndXSquared_returnsOnePlusXPlusXSquaredString() { val oneMinusXPolynomial = createPolynomial( - createTerm(coefficient = ONE_REAL), - createTerm(coefficient = ONE_REAL, createVariable(name = "x", power = 1)), - createTerm(coefficient = ONE_REAL, createVariable(name = "x", power = 2)) + createTerm(coefficient = ONE), + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)), + createTerm(coefficient = ONE, createVariable(name = "x", power = 2)) ) val result = oneMinusXPolynomial.toPlainText() @@ -350,9 +394,9 @@ class PolynomialExtensionsTest { @Test fun testToPlainText_xSquaredAndXAndOne_returnsXSquaredPlusXPlusOneString() { val oneMinusXPolynomial = createPolynomial( - createTerm(coefficient = ONE_REAL, createVariable(name = "x", power = 2)), - createTerm(coefficient = ONE_REAL, createVariable(name = "x", power = 1)), - createTerm(coefficient = ONE_REAL) + createTerm(coefficient = ONE, createVariable(name = "x", power = 2)), + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)), + createTerm(coefficient = ONE) ) val result = oneMinusXPolynomial.toPlainText() @@ -364,15 +408,2548 @@ class PolynomialExtensionsTest { @Test fun testToPlainText_xSquaredYCubedAndOne_returnsXSquaredYCubedPlusOneString() { val oneMinusXPolynomial = createPolynomial( - createTerm(coefficient = ONE_REAL, createVariable(name = "x", power = 2)), - createTerm(coefficient = ONE_REAL, createVariable(name = "y", power = 3)), - createTerm(coefficient = ONE_REAL) + createTerm(coefficient = ONE, createVariable(name = "x", power = 2)), + createTerm(coefficient = ONE, createVariable(name = "y", power = 3)), + createTerm(coefficient = ONE) ) val result = oneMinusXPolynomial.toPlainText() assertThat(result).isEqualTo("x^2 + y^3 + 1") } + + @Test + fun testRemoveUnnecessaryVariables_zeroX_returnsZero() { + val polynomial = createPolynomial( + createTerm(coefficient = ZERO, createVariable(name = "x", power = 1)) + ) + + val result = polynomial.removeUnnecessaryVariables() + + // 0x becomes just 0. + assertThat(result).isConstantThat().isIntegerThat().isEqualTo(0) + } + + @Test + fun testRemoveUnnecessaryVariables_xPlusZero_returnsX() { + val polynomial = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)), + createTerm(coefficient = ZERO) + ) + + val result = polynomial.removeUnnecessaryVariables() + + // x+0 is just x. + assertThat(result).isEqualTo(ONE_X_POLYNOMIAL) + } + + @Test + fun testRemoveUnnecessaryVariables_zeroXPlusOne_returnsOne() { + val polynomial = createPolynomial( + createTerm(coefficient = ZERO, createVariable(name = "x", power = 1)), + createTerm(coefficient = ONE) + ) + + val result = polynomial.removeUnnecessaryVariables() + + // 0x+1 is just 1. + assertThat(result).isConstantThat().isIntegerThat().isEqualTo(1) + } + + @Test + fun testRemoveUnnecessaryVariables_zeroXPlusZero_returnsZero() { + val polynomial = createPolynomial( + createTerm(coefficient = ZERO, createVariable(name = "x", power = 1)), + createTerm(coefficient = ZERO) + ) + + val result = polynomial.removeUnnecessaryVariables() + + // 0x+0 is just 0. + assertThat(result).isConstantThat().isIntegerThat().isEqualTo(0) + } + + @Test + fun testRemoveUnnecessaryVariables_zeroXSquaredPlusZeroXPlusTwo_returnsTwo() { + val polynomial = createPolynomial( + createTerm(coefficient = ZERO, createVariable(name = "x", power = 2)), + createTerm(coefficient = ZERO, createVariable(name = "x", power = 1)), + createTerm(coefficient = TWO_REAL) + ) + + val result = polynomial.removeUnnecessaryVariables() + + // 0x^2+0x+2 is just 2. + assertThat(result).isConstantThat().isIntegerThat().isEqualTo(2) + } + + @Test + fun testRemoveUnnecessaryVariables_zeroPlusOnePlusZeroXPlusZero_returnsOne() { + val polynomial = createPolynomial( + createTerm(coefficient = ZERO), + createTerm(coefficient = ONE), + createTerm(coefficient = ZERO, createVariable(name = "x", power = 1)), + createTerm(coefficient = ZERO) + ) + + val result = polynomial.removeUnnecessaryVariables() + + // 0+1+0x+0 is just 1. + assertThat(result).isConstantThat().isIntegerThat().isEqualTo(1) + } + + @Test + fun testSimplifyRationals_oneX_returnsOneX() { + val polynomial = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)) + ) + + val result = polynomial.simplifyRationals() + + // x stays as x. + assertThat(result).isEqualTo(ONE_X_POLYNOMIAL) + } + + @Test + fun testSimplifyRationals_oneHalfX_returnsOneHalfX() { + val polynomial = createPolynomial( + createTerm(coefficient = ONE_HALF, createVariable(name = "x", power = 1)) + ) + + val result = polynomial.simplifyRationals() + + // (1/2)x stays as (1/2)x. + assertThat(result).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasVariableCountThat().isEqualTo(1) + hasCoefficientThat().isEqualTo(ONE_HALF) + variable(0).hasNameThat().isEqualTo("x") + variable(0).hasPowerThat().isEqualTo(1) + } + } + } + + @Test + fun testSimplifyRationals_threeOnesX_returnsThreeOnesX() { + val polynomial = createPolynomial( + createTerm(coefficient = THREE_ONES_REAL, createVariable(name = "x", power = 1)) + ) + + val result = polynomial.simplifyRationals() + + // (3/1)x stays as 3x. + assertThat(result).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasVariableCountThat().isEqualTo(1) + hasCoefficientThat().isIntegerThat().isEqualTo(3) + variable(0).hasNameThat().isEqualTo("x") + variable(0).hasPowerThat().isEqualTo(1) + } + } + } + + @Test + fun testSimplifyRationals_negativeThreeXAsFraction_returnsNegativeThreeXWithInteger() { + val polynomial = createPolynomial( + createTerm(coefficient = -THREE_FRACTION_REAL, createVariable(name = "x", power = 1)) + ) + + val result = polynomial.simplifyRationals() + + // -3x (fraction) becomes -3x (integer). + assertThat(result).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasVariableCountThat().isEqualTo(1) + hasCoefficientThat().isIntegerThat().isEqualTo(-3) + variable(0).hasNameThat().isEqualTo("x") + variable(0).hasPowerThat().isEqualTo(1) + } + } + } + + @Test + fun testSimplifyRationals_xPlusThreeFractionXSquared_returnsXPlusThreeXSquared() { + val polynomial = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)), + createTerm(coefficient = THREE_FRACTION_REAL, createVariable(name = "x", power = 2)) + ) + + val result = polynomial.simplifyRationals() + + // x+3x (fraction) becomes x+3x (integer). + assertThat(result).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasVariableCountThat().isEqualTo(1) + hasCoefficientThat().isIntegerThat().isEqualTo(1) + variable(0).hasNameThat().isEqualTo("x") + variable(0).hasPowerThat().isEqualTo(1) + } + term(1).apply { + hasVariableCountThat().isEqualTo(1) + hasCoefficientThat().isIntegerThat().isEqualTo(3) + variable(0).hasNameThat().isEqualTo("x") + variable(0).hasPowerThat().isEqualTo(2) + } + } + } + + @Test + fun testSort_one_returnsOne() { + val polynomial = createPolynomial(createTerm(coefficient = ONE)) + + val result = polynomial.sort() + + assertThat(result).isConstantThat().isIntegerThat().isEqualTo(1) + } + + @Test + fun testSort_x_returnsX() { + val polynomial = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)) + ) + + val result = polynomial.sort() + + assertThat(result).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + } + } + + @Test + fun testSort_onePlusTwo_returnsTwoPlusOne() { + val polynomial = createPolynomial( + createTerm(coefficient = ONE), createTerm(coefficient = TWO_REAL) + ) + + val result = polynomial.sort() + + // 1+2 becomes 2+1 (larger number sorted first). + assertThat(result).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(2) + hasVariableCountThat().isEqualTo(0) + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(0) + } + } + } + + @Test + fun testSort_twoPlusOne_returnsTwoPlusOne() { + val polynomial = createPolynomial( + createTerm(coefficient = TWO_REAL), createTerm(coefficient = ONE) + ) + + val result = polynomial.sort() + + // 2+1 stays as 2+1 (larger number sorted first). + assertThat(result).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(2) + hasVariableCountThat().isEqualTo(0) + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(0) + } + } + } + + @Test + fun testSort_xPlusX_returnsXPlusX() { + val polynomial = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)), + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)) + ) + + val result = polynomial.sort() + + // x+x is symmetrical, so nothing changes. + assertThat(result).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + } + } + + @Test + fun testSort_xPlusOne_returnsXPlusOne() { + val polynomial = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)), + createTerm(coefficient = ONE) + ) + + val result = polynomial.sort() + + // x+1 stays as x+1 (variables are sorted first). + assertThat(result).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(0) + } + } + } + + @Test + fun testSort_onePlusX_returnsXPlusOne() { + val polynomial = createPolynomial( + createTerm(coefficient = ONE), + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)) + ) + + val result = polynomial.sort() + + // 1+x becomes x+1 (variables are sorted first). + assertThat(result).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(0) + } + } + } + + @Test + fun testSort_xPlusTwoX_returnsTwoXPlusX() { + val polynomial = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)), + createTerm(coefficient = TWO_REAL, createVariable(name = "x", power = 1)) + ) + + val result = polynomial.sort() + + // x+2x becomes 2x+x (larger coefficients are sorted first). + assertThat(result).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(2) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + } + } + + @Test + fun testSort_xPlusXSquared_returnsXSquaredPlusX() { + val polynomial = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)), + createTerm(coefficient = ONE, createVariable(name = "x", power = 2)) + ) + + val result = polynomial.sort() + + // x+x^2 becomes x^2+x (larger powers are sorted first). + assertThat(result).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + } + } + + @Test + fun testSort_xSquaredPlusX_returnsXSquaredPlusX() { + val polynomial = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 2)), + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)) + ) + + val result = polynomial.sort() + + // x^2+x stays as x^2+x (larger powers are sorted first). + assertThat(result).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + } + } + + @Test + fun testSort_xMinusXSquared_returnsNegativeXSquaredPlusX() { + val polynomial = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)), + createTerm(coefficient = -ONE, createVariable(name = "x", power = 2)) + ) + + val result = polynomial.sort() + + // 1-x^2 becomes -x^2+1 (larger powers are sorted first). + assertThat(result).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + } + } + + @Test + fun testSort_negativeXSquaredPlusX_returnsNegativeXSquaredPlusX() { + val polynomial = createPolynomial( + createTerm(coefficient = -ONE, createVariable(name = "x", power = 2)), + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)) + ) + + val result = polynomial.sort() + + // -x^2+1 stays as -x^2+1 (larger powers are sorted first). + assertThat(result).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + } + } + + @Test + fun testSort_yPlusXy_returnsXyPlusY() { + val polynomial = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "y", power = 1)), + createTerm( + coefficient = ONE, + createVariable(name = "x", power = 1), + createVariable(name = "y", power = 1) + ) + ) + + val result = polynomial.sort() + + // y+xy becomes xy+y (x variables are sorted first). + assertThat(result).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + } + } + + @Test + fun testSort_xPlusXy_returnsXyPlusX() { + val polynomial = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)), + createTerm( + coefficient = ONE, + createVariable(name = "x", power = 1), + createVariable(name = "y", power = 1) + ) + ) + + val result = polynomial.sort() + + // x+xy becomes xy+x (more variables are sorted first) + assertThat(result).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + } + } + + @Test + fun testSort_xyPlusZyx_returnsXyzPlusXy() { + val polynomial = createPolynomial( + createTerm( + coefficient = ONE, + createVariable(name = "x", power = 1), + createVariable(name = "y", power = 1) + ), + createTerm( + coefficient = ONE, + createVariable(name = "x", power = 1), + createVariable(name = "y", power = 1), + createVariable(name = "z", power = 1) + ) + ) + + val result = polynomial.sort() + + // xy+zyx becomes xyz+xy (again, more variables are sorted first). Also, variables are + // rearranged lexicographically. + assertThat(result).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(3) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + variable(2).apply { + hasNameThat().isEqualTo("z") + hasPowerThat().isEqualTo(1) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + } + } + + @Test + fun testSort_zyPlusYx_returnsXyPlusYz() { + val polynomial = createPolynomial( + createTerm( + coefficient = ONE, + createVariable(name = "z", power = 1), + createVariable(name = "y", power = 1) + ), + createTerm( + coefficient = ONE, + createVariable(name = "x", power = 1), + createVariable(name = "y", power = 1) + ) + ) + + val result = polynomial.sort() + + // zy+yx becomes xy+yz (sorted lexicographically). + assertThat(result).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + variable(1).apply { + hasNameThat().isEqualTo("z") + hasPowerThat().isEqualTo(1) + } + } + } + } + + @Test + fun testSort_xyzPlusYXSquared_returnsXSquaredYPlusXyz() { + val polynomial = createPolynomial( + createTerm( + coefficient = ONE, + createVariable(name = "x", power = 1), + createVariable(name = "y", power = 1), + createVariable(name = "z", power = 1) + ), + createTerm( + coefficient = ONE, + createVariable(name = "y", power = 1), + createVariable(name = "x", power = 2) + ) + ) + + val result = polynomial.sort() + + // xyz+yx^2 becomes x^2y+xyz (despite xyz having more variables, the higher power of x^2y + // prioritizes it). + assertThat(result).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(3) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + variable(2).apply { + hasNameThat().isEqualTo("z") + hasPowerThat().isEqualTo(1) + } + } + } + } + + @Test + fun testSort_xSquaredY_plusX_plusYCubed_plusXSquared_returnsYCubedPlusXSqYPlusXSqPlusX() { + val polynomial = createPolynomial( + createTerm( + coefficient = ONE, + createVariable(name = "x", power = 2), + createVariable(name = "y", power = 1) + ), + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)), + createTerm(coefficient = ONE, createVariable(name = "y", power = 3)), + createTerm(coefficient = ONE, createVariable(name = "x", power = 2)) + ) + + val result = polynomial.sort() + + // x^2y+x+y^3+x^2 becomes x^2y+x^2+x+y^3 per rules demonstrated in earlier tests. This test + // brings more of them together in one example, plus note that x terms are always fully listed + // first. + assertThat(result).apply { + hasTermCountThat().isEqualTo(4) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + term(2).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + term(3).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(3) + } + } + } + } + + @Test + @RunParameterized( + Iteration("x+y+z", "var1=x", "var2=y", "var3=z"), + Iteration("x+z+y", "var1=x", "var2=z", "var3=y"), + Iteration("y+x+z", "var1=y", "var2=x", "var3=z"), + Iteration("y+z+x", "var1=y", "var2=z", "var3=x"), + Iteration("z+x+y", "var1=z", "var2=x", "var3=y"), + Iteration("z+y+x", "var1=z", "var2=y", "var3=x") + ) + fun testSort_xPlusYPlusZ_inAnyOrder_returnsXPlusYPlusZ() { + val polynomial = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = var1, power = 1)), + createTerm(coefficient = ONE, createVariable(name = var2, power = 1)), + createTerm(coefficient = ONE, createVariable(name = var3, power = 1)) + ) + + val result = polynomial.sort() + + // Regardless of what order x, y, and z are combined in a polynomial, the sorted result is + // always x+y+z (per lexicographical sorting of the variable names themselves). + assertThat(result).apply { + hasTermCountThat().isEqualTo(3) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + term(2).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("z") + hasPowerThat().isEqualTo(1) + } + } + } + } + + /* Operator tests. */ + + @Test + fun testUnaryMinus_zero_returnsZero() { + val polynomial = ZERO_POLYNOMIAL + + val result = -polynomial + + // negate(0) stays as 0. + assertThat(result).isConstantThat().isIntegerThat().isEqualTo(0) + } + + @Test + fun testUnaryMinus_one_returnsNegativeOne() { + val polynomial = ONE_POLYNOMIAL + + val result = -polynomial + + // negate(1) becomes -1. + assertThat(result).isConstantThat().isIntegerThat().isEqualTo(-1) + } + + @Test + fun testUnaryMinus_x_returnsNegativeX() { + val polynomial = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)) + ) + + val result = -polynomial + + // negate(x) becomes -x. + assertThat(result).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + } + } + + @Test + fun testUnaryMinus_negativeX_returnsX() { + val polynomial = createPolynomial( + createTerm(coefficient = -ONE, createVariable(name = "x", power = 1)) + ) + + val result = -polynomial + + // negate(-x) becomes x. + assertThat(result).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + } + } + + @Test + fun testUnaryMinus_xSquaredPlusX_returnsNegativeXSquaredMinusX() { + val polynomial = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 2)), + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)) + ) + + val result = -polynomial + + // negate(x^2+x) becomes -x^2-x. + assertThat(result).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + } + } + + @Test + fun testUnaryMinus_oneMinusX_returnsNegativeOnePlusX() { + val polynomial = createPolynomial( + createTerm(coefficient = ONE), + createTerm(coefficient = -ONE, createVariable(name = "x", power = 1)) + ) + + val result = -polynomial + + // negate(1-x) becomes -1+x. + assertThat(result).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-1) + hasVariableCountThat().isEqualTo(0) + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + } + } + + @Test + fun testPlus_zeroAndZero_returnsZero() { + val polynomial1 = ZERO_POLYNOMIAL + val polynomial2 = ZERO_POLYNOMIAL + + val result = polynomial1 + polynomial2 + + // 0+0=0. + assertThat(result).isConstantThat().isIntegerThat().isEqualTo(0) + } + + @Test + fun testPlus_zeroAndOne_returnsOne() { + val polynomial1 = ZERO_POLYNOMIAL + val polynomial2 = ONE_POLYNOMIAL + + val result = polynomial1 + polynomial2 + + // 0+1=1. + assertThat(result).isConstantThat().isIntegerThat().isEqualTo(1) + } + + @Test + fun testPlus_zeroAndX_returnsX() { + val polynomial1 = ZERO_POLYNOMIAL + val polynomial2 = ONE_X_POLYNOMIAL + + val result = polynomial1 + polynomial2 + + // 0+x=x. + assertThat(result).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + } + } + + @Test + fun testPlus_oneAndX_returnsOnePlusX() { + val polynomial1 = ONE_POLYNOMIAL + val polynomial2 = ONE_X_POLYNOMIAL + + val result = polynomial1 + polynomial2 + + // poly(1)+poly(x)=poly(1+x). + assertThat(result).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(0) + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + } + } + + @Test + fun testPlus_xAndOne_returnsXPlusOne() { + val polynomial1 = ONE_X_POLYNOMIAL + val polynomial2 = ONE_POLYNOMIAL + + val result = polynomial1 + polynomial2 + + // poly(x)+poly(1)=poly(x+1). Per sorting, this shows commutativity with the above operation. + assertThat(result).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(0) + } + } + } + + @Test + fun testPlus_xAndX_returnsTwoX() { + val polynomial1 = ONE_X_POLYNOMIAL + val polynomial2 = ONE_X_POLYNOMIAL + + val result = polynomial1 + polynomial2 + + // x+x=2x (shows combining like terms). + assertThat(result).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(2) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + } + } + + @Test + fun testPlus_xAndNegativeX_returnsZero() { + val polynomial1 = ONE_X_POLYNOMIAL + val polynomial2 = NEGATIVE_ONE_X_POLYNOMIAL + + val result = polynomial1 + polynomial2 + + // x+-x=0 (term elimination). + assertThat(result).isConstantThat().isIntegerThat().isEqualTo(0) + } + + @Test + fun testPlus_xSquaredAndX_returnsXSquaredPlusX() { + val polynomial1 = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 2)) + ) + val polynomial2 = ONE_X_POLYNOMIAL + + val result = polynomial1 + polynomial2 + + // poly(x^2)+poly(x)=poly(x^+x). + assertThat(result).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + } + } + + @Test + fun testMinus_zeroAndZero_returnsZero() { + val polynomial1 = ZERO_POLYNOMIAL + val polynomial2 = ZERO_POLYNOMIAL + + val result = polynomial1 - polynomial2 + + // 0-0=0 (term elimination). + assertThat(result).isConstantThat().isIntegerThat().isEqualTo(0) + } + + @Test + fun testMinus_oneAndZero_returnsOne() { + val polynomial1 = ONE_POLYNOMIAL + val polynomial2 = ZERO_POLYNOMIAL + + val result = polynomial1 - polynomial2 + + // 1-0=1. + assertThat(result).isConstantThat().isIntegerThat().isEqualTo(1) + } + + @Test + fun testMinus_xAndZero_returnsX() { + val polynomial1 = ONE_X_POLYNOMIAL + val polynomial2 = ZERO_POLYNOMIAL + + val result = polynomial1 - polynomial2 + + // x-0=x. + assertThat(result).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + } + } + + @Test + fun testMinus_xAndOne_returnsXMinusOne() { + val polynomial1 = ONE_X_POLYNOMIAL + val polynomial2 = ONE_POLYNOMIAL + + val result = polynomial1 - polynomial2 + + // poly(x)-poly(1)=poly(x-1). + assertThat(result).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-1) + hasVariableCountThat().isEqualTo(0) + } + } + } + + @Test + fun testMinus_oneAndX_returnsOneMinusX() { + val polynomial1 = ONE_POLYNOMIAL + val polynomial2 = ONE_X_POLYNOMIAL + + val result = polynomial1 - polynomial2 + + // poly(1)-poly(x)=poly(1-x). Shows anticommutativity. + assertThat(result).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(0) + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + } + } + + @Test + fun testMinus_twoXAndX_returnsX() { + val polynomial1 = TWO_X_POLYNOMIAL + val polynomial2 = ONE_X_POLYNOMIAL + + val result = polynomial1 - polynomial2 + + // 2x-x=x. + assertThat(result).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + } + } + + @Test + fun testMinus_xAndX_returnsZero() { + val polynomial1 = ONE_X_POLYNOMIAL + val polynomial2 = ONE_X_POLYNOMIAL + + val result = polynomial1 - polynomial2 + + // x-x=0. + assertThat(result).isConstantThat().isIntegerThat().isEqualTo(0) + } + + @Test + fun testMinus_xAndNegativeX_returnsTwoX() { + val polynomial1 = ONE_X_POLYNOMIAL + val polynomial2 = NEGATIVE_ONE_X_POLYNOMIAL + + val result = polynomial1 - polynomial2 + + // x - -x=2x. + assertThat(result).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(2) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + } + } + + @Test + fun testMinus_negativeXAndX_returnsNegativeTwoX() { + val polynomial1 = NEGATIVE_ONE_X_POLYNOMIAL + val polynomial2 = ONE_X_POLYNOMIAL + + val result = polynomial1 - polynomial2 + + // -x - x=-2x. + assertThat(result).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-2) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + } + } + + @Test + fun testMinus_negativeXAndNegativeX_returnsZero() { + val polynomial1 = NEGATIVE_ONE_X_POLYNOMIAL + val polynomial2 = NEGATIVE_ONE_X_POLYNOMIAL + + val result = polynomial1 - polynomial2 + + // -x - -x=0. + assertThat(result).isConstantThat().isIntegerThat().isEqualTo(0) + } + + @Test + fun testMinus_xSquaredAndX_returnsXSquaredMinusX() { + val polynomial1 = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 2)) + ) + val polynomial2 = ONE_X_POLYNOMIAL + + val result = polynomial1 - polynomial2 + + // poly(x^2)-poly(x)=poly(x^2-x). + assertThat(result).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + } + } + + @Test + fun testTimes_zeroAndZero_returnsZero() { + val polynomial1 = ZERO_POLYNOMIAL + val polynomial2 = ZERO_POLYNOMIAL + + val result = polynomial1 * polynomial2 + + // 0*0=0. + assertThat(result).isConstantThat().isIntegerThat().isEqualTo(0) + } + + @Test + fun testTimes_zeroAndOne_returnsZero() { + val polynomial1 = ZERO_POLYNOMIAL + val polynomial2 = ONE_POLYNOMIAL + + val result = polynomial1 * polynomial2 + + // 0*1=0. + assertThat(result).isConstantThat().isIntegerThat().isEqualTo(0) + } + + @Test + fun testTimes_oneAndOne_returnsOne() { + val polynomial1 = ONE_POLYNOMIAL + val polynomial2 = ONE_POLYNOMIAL + + val result = polynomial1 * polynomial2 + + // 1*1=1. + assertThat(result).isConstantThat().isIntegerThat().isEqualTo(1) + } + + @Test + fun testTimes_twoAndThree_returnsSix() { + val polynomial1 = TWO_POLYNOMIAL + val polynomial2 = createPolynomial(createTerm(coefficient = THREE_REAL)) + + val result = polynomial1 * polynomial2 + + // 2*3=6. + assertThat(result).isConstantThat().isIntegerThat().isEqualTo(6) + } + + @Test + fun testTimes_xAndZero_returnsZero() { + val polynomial1 = ONE_X_POLYNOMIAL + val polynomial2 = ZERO_POLYNOMIAL + + val result = polynomial1 * polynomial2 + + // x*0=0. + assertThat(result).isConstantThat().isIntegerThat().isEqualTo(0) + } + + @Test + fun testTimes_xAndOne_returnsX() { + val polynomial1 = ONE_X_POLYNOMIAL + val polynomial2 = ONE_POLYNOMIAL + + val result = polynomial1 * polynomial2 + + // x*1=x. + assertThat(result).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + } + } + + @Test + fun testTimes_xAndX_returnsXSquared() { + val polynomial1 = ONE_X_POLYNOMIAL + val polynomial2 = ONE_X_POLYNOMIAL + + val result = polynomial1 * polynomial2 + + // x*x=x^2. + assertThat(result).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + } + } + + @Test + fun testTimes_threeXSquaredAndTwoX_returnsSixXCubed() { + val polynomial1 = createPolynomial( + createTerm(coefficient = THREE_REAL, createVariable(name = "x", power = 2)) + ) + val polynomial2 = createPolynomial( + createTerm(coefficient = TWO_REAL, createVariable(name = "x", power = 1)) + ) + + val result = polynomial1 * polynomial2 + + // 3x^2*2x=6x^3. + assertThat(result).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(6) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(3) + } + } + } + } + + @Test + fun testTimes_twoXAndThreeXSquared_returnsSixXCubed() { + val polynomial1 = createPolynomial( + createTerm(coefficient = TWO_REAL, createVariable(name = "x", power = 1)) + ) + val polynomial2 = createPolynomial( + createTerm(coefficient = THREE_REAL, createVariable(name = "x", power = 2)) + ) + + val result = polynomial1 * polynomial2 + + // 2x*3x^2=6x^3. This demonstrates multiplication commutativity. + assertThat(result).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(6) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(3) + } + } + } + } + + @Test + fun testTimes_xAndNegativeX_returnsNegativeXSquared() { + val polynomial1 = ONE_X_POLYNOMIAL + val polynomial2 = NEGATIVE_ONE_X_POLYNOMIAL + + val result = polynomial1 * polynomial2 + + // x*(-x)=-x^2. + assertThat(result).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + } + } + + @Test + fun testTimes_negativeXAndX_returnsNegativeXSquared() { + val polynomial1 = NEGATIVE_ONE_X_POLYNOMIAL + val polynomial2 = ONE_X_POLYNOMIAL + + val result = polynomial1 * polynomial2 + + // (-x)*x=-x^2. + assertThat(result).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + } + } + + @Test + fun testTimes_negativeXAndNegativeX_returnsXSquared() { + val polynomial1 = NEGATIVE_ONE_X_POLYNOMIAL + val polynomial2 = NEGATIVE_ONE_X_POLYNOMIAL + + val result = polynomial1 * polynomial2 + + // (-x)*(-x)=x^2 (negatives cancel out). + assertThat(result).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + } + } + + @Test + fun testTimes_negativeFiveX_sevenX_returnsNegativeThirtyFiveXSquared() { + val polynomial1 = createPolynomial( + createTerm(coefficient = -FIVE_REAL, createVariable(name = "x", power = 1)) + ) + val polynomial2 = createPolynomial( + createTerm(coefficient = SEVEN_REAL, createVariable(name = "x", power = 1)) + ) + + val result = polynomial1 * polynomial2 + + // -5x*7x=-35x^2. + assertThat(result).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-35) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + } + } + + @Test + fun testTimes_onePlusX_onePlusX_returnsOnePlus2XPlusXSquared() { + val polynomial1 = createPolynomial( + createTerm(coefficient = ONE), + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)) + ) + val polynomial2 = createPolynomial( + createTerm(coefficient = ONE), + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)) + ) + + val result = polynomial1 * polynomial2 + + // (1+x)*(1+x)=1+2x+x^2 (like terms are combined). + assertThat(result).apply { + hasTermCountThat().isEqualTo(3) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(0) + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(2) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + term(2).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + } + } + + @Test + fun testTimes_xPlusOne_xMinusOne_returnsXSquaredMinusOne() { + val polynomial1 = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)), + createTerm(coefficient = ONE) + ) + val polynomial2 = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)), + createTerm(coefficient = -ONE) + ) + + val result = polynomial1 * polynomial2 + + // (x+1)*(x-1)=x^2-1 (negative terms are eliminated). + assertThat(result).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-1) + hasVariableCountThat().isEqualTo(0) + } + } + } + + @Test + fun testTimes_xMinusOne_xPlusOne_returnsXSquaredMinusOne() { + val polynomial1 = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)), + createTerm(coefficient = -ONE) + ) + val polynomial2 = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)), + createTerm(coefficient = ONE) + ) + + val result = polynomial1 * polynomial2 + + // (x-1)*(x+1)=x^2-1 (commutativity works for combining terms, too). + assertThat(result).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-1) + hasVariableCountThat().isEqualTo(0) + } + } + } + + @Test + fun testTimes_twoXy_threeXSquaredY_returnsSixXCubedYSquared() { + val polynomial1 = createPolynomial( + createTerm( + coefficient = TWO_REAL, + createVariable(name = "x", power = 1), + createVariable(name = "y", power = 1) + ) + ) + val polynomial2 = createPolynomial( + createTerm( + coefficient = THREE_REAL, + createVariable(name = "x", power = 2), + createVariable(name = "y", power = 1) + ) + ) + + val result = polynomial1 * polynomial2 + + // 2xy*3x^2y=6x^3y^2. + assertThat(result).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(6) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(3) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(2) + } + } + } + } + + @Test + fun testDiv_oneAndZero_returnsNull() { + val polynomial1 = ONE_POLYNOMIAL + val polynomial2 = ZERO_POLYNOMIAL + + val result = polynomial1 / polynomial2 + + // Cannot divide by zero. + assertThat(result).isNotValidPolynomial() + } + + @Test + fun testDiv_threeAndTwo_returnsOneAndOneHalf() { + val polynomial1 = createPolynomial(createTerm(coefficient = THREE_REAL)) + val polynomial2 = TWO_POLYNOMIAL + + val result = polynomial1 / polynomial2 + + // 3/2=1 1/2 (fraction) to demonstrate fully constant division. + assertThat(result).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isRationalThat().apply { + hasNegativePropertyThat().isFalse() + hasWholeNumberThat().isEqualTo(1) + hasNumeratorThat().isEqualTo(1) + hasDenominatorThat().isEqualTo(2) + } + hasVariableCountThat().isEqualTo(0) + } + } + } + + @Test + fun testDiv_piAndTwo_returnsHalfPi() { + val polynomial1 = PI_POLYNOMIAL + val polynomial2 = TWO_POLYNOMIAL + + val result = polynomial1 / polynomial2 + + // 3.14/2=1.57 (irrational) to demonstrate fully constant division. + assertThat(result).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIrrationalThat().isWithin(1e-5).of(1.57) + hasVariableCountThat().isEqualTo(0) + } + } + } + + @Test + fun testDiv_xAndOne_returnsX() { + val polynomial1 = ONE_X_POLYNOMIAL + val polynomial2 = ONE_POLYNOMIAL + + val result = polynomial1 / polynomial2 + + // x/1=x. + assertThat(result).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + } + } + + @Test + fun testDiv_oneAndX_returnsNull() { + val polynomial1 = ONE_POLYNOMIAL + val polynomial2 = ONE_X_POLYNOMIAL + + val result = polynomial1 / polynomial2 + + // 1/x fails (cannot have negative power terms in polynomials, and this also shows that division + // is not commutative). + assertThat(result).isNotValidPolynomial() + } + + @Test + fun testDiv_xSquared_x_returnsX() { + val polynomial1 = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 2)) + ) + val polynomial2 = ONE_X_POLYNOMIAL + + val result = polynomial1 / polynomial2 + + // x^2/x=x. + assertThat(result).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + } + } + + @Test + fun testDiv_onePlus2XPlusXSquared_onePlusX_returnsOnePlusX() { + val polynomial1 = createPolynomial( + createTerm(coefficient = ONE), + createTerm(coefficient = TWO_REAL, createVariable(name = "x", power = 1)), + createTerm(coefficient = ONE, createVariable(name = "x", power = 2)) + ) + val polynomial2 = createPolynomial( + createTerm(coefficient = ONE), + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)) + ) + + val result = polynomial1 / polynomial2 + + // (1+2x+x^2)/(1+x)=x+1 (full polynomial division). + assertThat(result).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(0) + } + } + } + + @Test + fun testDiv_xSquaredPlus2XPlusOne_onePlusX_returnsOnePlusX() { + val polynomial1 = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 2)), + createTerm(coefficient = TWO_REAL, createVariable(name = "x", power = 1)), + createTerm(coefficient = ONE) + ) + val polynomial2 = createPolynomial( + createTerm(coefficient = ONE), + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)) + ) + + val result = polynomial1 / polynomial2 + + // (x^2+2x+1)/(1+x)=x+1 (order of terms for the dividend doesn't matter). + assertThat(result).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(0) + } + } + } + + @Test + fun testDiv_xSquaredPlus2XPlusOne_oneMinusX_returnsNull() { + val polynomial1 = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 2)), + createTerm(coefficient = TWO_REAL, createVariable(name = "x", power = 1)), + createTerm(coefficient = ONE) + ) + val polynomial2 = createPolynomial( + createTerm(coefficient = ONE), + createTerm(coefficient = -ONE, createVariable(name = "x", power = 1)) + ) + + val result = polynomial1 / polynomial2 + + // (x^2+2x+1)/(1-x) fails (division doesn't result in a perfect polynomial). + assertThat(result).isNotValidPolynomial() + } + + @Test + fun testDiv_negativeXCubed_xSquared_returnsNegativeX() { + val polynomial1 = createPolynomial( + createTerm(coefficient = -ONE, createVariable(name = "x", power = 3)) + ) + val polynomial2 = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 2)) + ) + + val result = polynomial1 / polynomial2 + + // -x^3/x^2=-x (negatives are retained). + assertThat(result).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + } + } + + @Test + fun testDiv_xSquaredMinusOne_xPlusOne_returnsXMinusOne() { + val polynomial1 = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 2)), + createTerm(coefficient = -ONE) + ) + val polynomial2 = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)), + createTerm(coefficient = ONE) + ) + + val result = polynomial1 / polynomial2 + + // (x^2-1)/(x+1)=x-1. + assertThat(result).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-1) + hasVariableCountThat().isEqualTo(0) + } + } + } + + @Test + fun testDiv_xSquaredMinusOne_xMinusOne_returnsXPlusOne() { + val polynomial1 = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 2)), + createTerm(coefficient = -ONE) + ) + val polynomial2 = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)), + createTerm(coefficient = -ONE) + ) + + val result = polynomial1 / polynomial2 + + // (x^2-1)/(x-1)=x+1. + assertThat(result).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(0) + } + } + } + + @Test + fun testDiv_xSquaredMinusOne_x_returnsNull() { + val polynomial1 = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 2)), + createTerm(coefficient = -ONE) + ) + val polynomial2 = ONE_X_POLYNOMIAL + + val result = polynomial1 / polynomial2 + + // (x^2-1)/x fails. + assertThat(result).isNotValidPolynomial() + } + + @Test + fun testDiv_xSquaredMinusOne_negativeOne_negativeXSquaredPlusOne() { + val polynomial1 = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 2)), + createTerm(coefficient = -ONE) + ) + val polynomial2 = createPolynomial(createTerm(coefficient = -ONE)) + + val result = polynomial1 / polynomial2 + + // (x^2-1)/(-1)=-x^2+1 (reverses negative signs). + assertThat(result).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(0) + } + } + } + + @Test + fun testDiv_xSquaredMinusOne_two_returnsNull() { + val polynomial1 = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 2)), + createTerm(coefficient = -ONE) + ) + val polynomial2 = TWO_POLYNOMIAL + + val result = polynomial1 / polynomial2 + + // (x^2-1)/2=(1/2)x^2-1/2 (since non-zero constants can always be factored). + assertThat(result).apply { + hasTermCountThat().isEqualTo(2) + term(0).apply { + hasCoefficientThat().isEqualTo(ONE_HALF) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + term(1).apply { + hasCoefficientThat().isEqualTo(-ONE_HALF) + hasVariableCountThat().isEqualTo(0) + } + } + } + + @Test + fun testDiv_negativeThreeXSquared_xSquared_returnsNegativeThree() { + val polynomial1 = createPolynomial( + createTerm(coefficient = -THREE_REAL, createVariable(name = "x", power = 2)) + ) + val polynomial2 = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 2)) + ) + + val result = polynomial1 / polynomial2 + + // (-3x^2)/(x^2)=-3 (coefficient is retained). + assertThat(result).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-3) + hasVariableCountThat().isEqualTo(0) + } + } + } + + @Test + fun testDiv_negativeThreeXSquared_negativeXSquared_returnsThree() { + val polynomial1 = createPolynomial( + createTerm(coefficient = -THREE_REAL, createVariable(name = "x", power = 2)) + ) + val polynomial2 = createPolynomial( + createTerm(coefficient = -ONE, createVariable(name = "x", power = 2)) + ) + + val result = polynomial1 / polynomial2 + + // (-3x^2)/(-x^2)=3 (negatives cancel during division). + assertThat(result).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(3) + hasVariableCountThat().isEqualTo(0) + } + } + } + + @Test + fun testDiv_xSquaredY_y_returnsXSquared() { + val polynomial1 = createPolynomial( + createTerm( + coefficient = ONE, + createVariable(name = "x", power = 2), + createVariable(name = "y", power = 1) + ) + ) + val polynomial2 = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "y", power = 1)) + ) + + val result = polynomial1 / polynomial2 + + // x^2y / y=x^2 (variable elimination). + assertThat(result).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + } + } + + @Test + fun testDiv_xSquaredY_x_returnsXTimesY() { + val polynomial1 = createPolynomial( + createTerm( + coefficient = ONE, + createVariable(name = "x", power = 2), + createVariable(name = "y", power = 1) + ) + ) + val polynomial2 = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)) + ) + + val result = polynomial1 / polynomial2 + + // x^2y / x=xy (variable power elimination). + assertThat(result).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(2) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + variable(1).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + } + } + + @Test + fun testDiv_xSquaredY_xSquared_returnsY() { + val polynomial1 = createPolynomial( + createTerm( + coefficient = ONE, + createVariable(name = "x", power = 2), + createVariable(name = "y", power = 1) + ) + ) + val polynomial2 = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 2)) + ) + + val result = polynomial1 / polynomial2 + + // x^2y / x^2=y (variable elimination). + assertThat(result).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("y") + hasPowerThat().isEqualTo(1) + } + } + } + } + + @Test + fun testDiv_xSquaredY_yXSquared_returnsOne() { + val polynomial1 = createPolynomial( + createTerm( + coefficient = ONE, + createVariable(name = "x", power = 2), + createVariable(name = "y", power = 1) + ) + ) + val polynomial2 = createPolynomial( + createTerm( + coefficient = ONE, + createVariable(name = "y", power = 1), + createVariable(name = "x", power = 2) + ) + ) + + val result = polynomial1 / polynomial2 + + // x^2y / yx^2=1 (multi-variable elimination). + assertThat(result).isConstantThat().isIntegerThat().isEqualTo(1) + } + + @Test + fun testDiv_xSquaredY_ySquared_returnsNull() { + val polynomial1 = createPolynomial( + createTerm( + coefficient = ONE, + createVariable(name = "x", power = 2), + createVariable(name = "y", power = 1) + ) + ) + val polynomial2 = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "y", power = 2)) + ) + + val result = polynomial1 / polynomial2 + + // x^2y / y^2 fails (no polynomial exists). + assertThat(result).isNotValidPolynomial() + } + + @Test + fun testPow_zeroAndZero_returnsOne() { + val polynomial1 = ZERO_POLYNOMIAL + val polynomial2 = ZERO_POLYNOMIAL + + val result = polynomial1 pow polynomial2 + + // 0^0=1 (conventionally despite this power not existing in mathematics). + assertThat(result).isConstantThat().isIntegerThat().isEqualTo(1) + } + + @Test + fun testPow_zeroAndOne_returnsZero() { + val polynomial1 = ZERO_POLYNOMIAL + val polynomial2 = ONE_POLYNOMIAL + + val result = polynomial1 pow polynomial2 + + // 0^1=0. + assertThat(result).isConstantThat().isIntegerThat().isEqualTo(0) + } + + @Test + fun testPow_oneAndZero_returnsOne() { + val polynomial1 = ONE_POLYNOMIAL + val polynomial2 = ZERO_POLYNOMIAL + + val result = polynomial1 pow polynomial2 + + // 1^0=1 (i.e. exponentiation is not commutative). + assertThat(result).isConstantThat().isIntegerThat().isEqualTo(1) + } + + @Test + fun testPow_oneAndOne_returnsOne() { + val polynomial1 = ONE_POLYNOMIAL + val polynomial2 = ONE_POLYNOMIAL + + val result = polynomial1 pow polynomial2 + + // 1^1=1. + assertThat(result).isConstantThat().isIntegerThat().isEqualTo(1) + } + + @Test + fun testPow_xAndOne_returnsX() { + val polynomial1 = ONE_X_POLYNOMIAL + val polynomial2 = ONE_POLYNOMIAL + + val result = polynomial1 pow polynomial2 + + // poly(x)^poly(1)=poly(x). + assertThat(result).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + } + } + + @Test + fun testPow_xAndX_returnsNull() { + val polynomial1 = ONE_X_POLYNOMIAL + val polynomial2 = ONE_X_POLYNOMIAL + + val result = polynomial1 pow polynomial2 + + // x^x fails since polynomials can't have variable exponents. + assertThat(result).isNotValidPolynomial() + } + + @Test + fun testPow_xAndTwo_returnsXSquared() { + val polynomial1 = ONE_X_POLYNOMIAL + val polynomial2 = TWO_POLYNOMIAL + + val result = polynomial1 pow polynomial2 + + // poly(x)^poly(2)=poly(x^2). + assertThat(result).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + } + } + + @Test + fun testPow_onePlusX_two_onePlus2XPlusXSquared() { + val polynomial1 = createPolynomial( + createTerm(coefficient = ONE), + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)) + ) + val polynomial2 = TWO_POLYNOMIAL + + val result = polynomial1 pow polynomial2 + + // (1+x)^2=1+2x+x^2 (binomial expansion). + assertThat(result).apply { + hasTermCountThat().isEqualTo(3) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(0) + } + term(1).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(2) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + term(2).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(2) + } + } + } + } + + @Test + fun testPow_x_negativeOne_returnsNull() { + val polynomial1 = ONE_X_POLYNOMIAL + val polynomial2 = createPolynomial(createTerm(coefficient = -ONE)) + + val result = polynomial1 pow polynomial2 + + // x^-1 fails since polynomials can't have negative powers. + assertThat(result).isNotValidPolynomial() + } + + @Test + fun testPow_two_negativeOne_returnsOneHalf() { + val polynomial1 = TWO_POLYNOMIAL + val polynomial2 = createPolynomial(createTerm(coefficient = -ONE)) + + val result = polynomial1 pow polynomial2 + + // 2^-1=1/2 (this demonstrates constant-only powers, and that negative powers sometimes work). + assertThat(result).isConstantThat().isEqualTo(ONE_HALF) + } + + @Test + fun testPow_four_negativeOneHalf_returnsOneHalf() { + val polynomial1 = createPolynomial(createTerm(coefficient = FOUR_REAL)) + val polynomial2 = NEGATIVE_ONE_HALF_POLYNOMIAL + + val result = polynomial1 pow polynomial2 + + // 4^(-1/2)=1/2 (this demonstrates constant-only powers, and that negative powers sometimes work). + assertThat(result).isConstantThat().isEqualTo(ONE_HALF) + } + + @Test + fun testPow_onePlusX_oneHalf_returnsNull() { + val polynomial1 = createPolynomial( + createTerm(coefficient = ONE), + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)) + ) + val polynomial2 = ONE_HALF_POLYNOMIAL + + val result = polynomial1 pow polynomial2 + + // (1+x)^(1/2) fails since 1+x has no square root. + assertThat(result).isNotValidPolynomial() + } + + @Test + fun testPow_onePlus2XPlusXSquared_oneHalf_returnsNull() { + val polynomial1 = createPolynomial( + createTerm(coefficient = ONE), + createTerm(coefficient = TWO_REAL, createVariable(name = "x", power = 1)), + createTerm(coefficient = ONE, createVariable(name = "x", power = 2)) + ) + val polynomial2 = ONE_HALF_POLYNOMIAL + + val result = polynomial1 pow polynomial2 + + // (1+2x+x^2)^(1/2) fails since multi-term factoring is not currently supported. + assertThat(result).isNotValidPolynomial() + } + + @Test + fun testPow_xSquaredMinusOne_oneHalf_returnsNull() { + val polynomial1 = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 2)), + createTerm(coefficient = -ONE) + ) + val polynomial2 = ONE_HALF_POLYNOMIAL + + val result = polynomial1 pow polynomial2 + + // (x^2-1)^(1/2) fails since multi-term factoring is not currently supported. + assertThat(result).isNotValidPolynomial() + } + + @Test + fun testPow_xSquared_oneHalf_returnsX() { + val polynomial1 = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 2)) + ) + val polynomial2 = ONE_HALF_POLYNOMIAL + + val result = polynomial1 pow polynomial2 + + // (x^2)^(1/2)=x. + assertThat(result).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(1) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + } + } + + @Test + fun testPow_fourXSquared_oneHalf_returnsTwoX() { + val polynomial1 = createPolynomial( + createTerm(coefficient = FOUR_REAL, createVariable(name = "x", power = 2)) + ) + val polynomial2 = ONE_HALF_POLYNOMIAL + + val result = polynomial1 pow polynomial2 + + // (4x^2)^(1/2)=2x (demonstrates that coefficients can also be rooted). + assertThat(result).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(2) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + } + } + + @Test + fun testPow_xSquared_negativeOneHalf_returnsNull() { + val polynomial1 = createPolynomial( + createTerm(coefficient = -ONE, createVariable(name = "x", power = 2)) + ) + val polynomial2 = ONE_HALF_POLYNOMIAL + + val result = polynomial1 pow polynomial2 + + // (-x^2)^(1/2) fails since a negative coefficient can't be square rooted. + assertThat(result).isNotValidPolynomial() + } + + @Test + fun testPow_negativeTwentySevenXCubed_oneThird_returnsNegativeThreeX() { + val twentySevenReal = checkNotNull(THREE_REAL pow THREE_REAL) + val polynomial1 = createPolynomial( + createTerm(coefficient = -twentySevenReal, createVariable(name = "x", power = 3)) + ) + val polynomial2 = createPolynomial(createTerm(coefficient = ONE_THIRD_REAL)) + + val result = polynomial1 pow polynomial2 + + // (-9x^3)^(1/3)=-3x (demonstrates real number rooting, i.e. support for negative coefficients + // in certain cases). + assertThat(result).apply { + hasTermCountThat().isEqualTo(1) + term(0).apply { + hasCoefficientThat().isIntegerThat().isEqualTo(-3) + hasVariableCountThat().isEqualTo(1) + variable(0).apply { + hasNameThat().isEqualTo("x") + hasPowerThat().isEqualTo(1) + } + } + } + } + + @Test + fun testPow_xSquared_oneThird_returnsNull() { + val twentySevenReal = checkNotNull(THREE_REAL pow THREE_REAL) + val polynomial1 = createPolynomial( + createTerm(coefficient = twentySevenReal, createVariable(name = "x", power = 2)) + ) + val polynomial2 = createPolynomial(createTerm(coefficient = ONE_THIRD_REAL)) + + val result = polynomial1 pow polynomial2 + + // (27x^2)^(1/3) fails since the power '2' cannot be taken to the 1/3 (i.e. 2/3 is not a valid + // polynomial power). + assertThat(result).isNotValidPolynomial() + } + + @Test + fun testPow_xAndPi_returnsNull() { + val polynomial1 = ONE_X_POLYNOMIAL + val polynomial2 = PI_POLYNOMIAL + + val result = polynomial1 pow polynomial2 + + // Cannot raise polynomials to non-integer powers. + assertThat(result).isNotValidPolynomial() + } } private fun createVariable(name: String, power: Int) = Variable.newBuilder().apply { diff --git a/utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt b/utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt index 43234d63feb..978b4a014f9 100644 --- a/utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt @@ -10,6 +10,7 @@ import org.oppia.android.testing.junit.OppiaParameterizedTestRunner import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Iteration import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Parameter import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.RunParameterized +import org.oppia.android.testing.math.RealSubject import org.oppia.android.testing.math.RealSubject.Companion.assertThat import org.robolectric.annotation.LooperMode @@ -51,6 +52,16 @@ class RealExtensionsTest { wholeNumber = 1 }.build() + private val THREE_FRACTION = Fraction.newBuilder().apply { + wholeNumber = 3 + denominator = 1 + }.build() + + private val THREE_ONES_FRACTION = Fraction.newBuilder().apply { + numerator = 3 + denominator = 1 + }.build() + private val ZERO_REAL = createIntegerReal(0) private val TWO_REAL = createIntegerReal(2) private val NEGATIVE_TWO_REAL = createIntegerReal(-2) @@ -59,6 +70,9 @@ class RealExtensionsTest { private val NEGATIVE_ONE_HALF_REAL = createRationalReal(-ONE_HALF_FRACTION) private val ONE_AND_ONE_HALF_REAL = createRationalReal(ONE_AND_ONE_HALF_FRACTION) private val NEGATIVE_ONE_AND_ONE_HALF_REAL = createRationalReal(-ONE_AND_ONE_HALF_FRACTION) + private val THREE_FRACTION_REAL = createRationalReal(THREE_FRACTION) + private val NEGATIVE_THREE_FRACTION_REAL = createRationalReal(-THREE_FRACTION) + private val THREE_ONES_REAL = createRationalReal(THREE_ONES_FRACTION) private val PI_REAL = createIrrationalReal(PI) private val NEGATIVE_PI_REAL = createIrrationalReal(-PI) @@ -76,6 +90,32 @@ class RealExtensionsTest { @Parameter lateinit var expFrac: String @Parameter var expDouble: Double = Double.MIN_VALUE + @Test + fun testZero_isZeroInteger() { + val subject = ZERO + + assertThat(subject).isIntegerThat().isEqualTo(0) + } + + @Test + fun testOne_isOneInteger() { + val subject = ONE + + assertThat(subject).isIntegerThat().isEqualTo(1) + } + + @Test + fun testOneHalf_isOneHalfRational() { + val subject = ONE_HALF + + assertThat(subject).isRationalThat().apply { + hasNegativePropertyThat().isFalse() + hasWholeNumberThat().isEqualTo(0) + hasNumeratorThat().isEqualTo(1) + hasDenominatorThat().isEqualTo(2) + } + } + @Test fun testIsRational_default_returnsFalse() { val defaultReal = Real.getDefaultInstance() @@ -136,6 +176,76 @@ class RealExtensionsTest { assertThat(result).isFalse() } + @Test + fun testIsWholeNumber_default_returnsFalse() { + val defaultReal = Real.getDefaultInstance() + + val result = defaultReal.isWholeNumber() + + assertThat(result).isFalse() + } + + @Test + fun testIsWholeNumber_twoInteger_returnsTrue() { + val result = TWO_REAL.isWholeNumber() + + assertThat(result).isTrue() + } + + @Test + fun testIsWholeNumber_negativeTwoInteger_returnsTrue() { + val result = NEGATIVE_TWO_REAL.isWholeNumber() + + assertThat(result).isTrue() + } + + @Test + fun testIsWholeNumber_oneHalfFraction_returnsFalse() { + val result = ONE_HALF_REAL.isWholeNumber() + + assertThat(result).isFalse() + } + + @Test + fun testIsWholeNumber_threeOnesFraction_returnsFalse() { + val result = THREE_ONES_REAL.isWholeNumber() + + // 3/1 is treated as a fraction despite being numerically equivalent to a whole number. + assertThat(result).isFalse() + } + + @Test + fun testIsWholeNumber_threeFraction_returnsTrue() { + val result = THREE_FRACTION_REAL.isWholeNumber() + + assertThat(result).isTrue() + } + + @Test + fun testIsWholeNumber_negativeThreeFraction_returnsTrue() { + val result = NEGATIVE_THREE_FRACTION_REAL.isWholeNumber() + + assertThat(result).isTrue() + } + + @Test + fun testIsWholeNumber_piIrrational_returnsFalse() { + val result = PI_REAL.isWholeNumber() + + assertThat(result).isFalse() + } + + @Test + fun testIsWholeNumber_threeIrrational_returnsFalse() { + val real = createIrrationalReal(3.0) + + val result = real.isWholeNumber() + + // Despite 3.0 being approximately a whole number, it isn't considered one since it's a double + // (and thus can have precision loss). + assertThat(result).isFalse() + } + @Test fun testIsNegative_default_throwsException() { val defaultReal = Real.getDefaultInstance() @@ -187,6 +297,139 @@ class RealExtensionsTest { assertThat(result).isTrue() } + @Test + fun testIsApproximatelyEqualTo_zeroAndOne_returnsFalse() { + val result = ZERO_REAL.isApproximatelyEqualTo(1.0) + + assertThat(result).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_twoAndTwoWithinThreshold_returnsTrue() { + val result = TWO_REAL.isApproximatelyEqualTo(2.0 + FLOAT_EQUALITY_INTERVAL / 2.0) + + assertThat(result).isTrue() + } + + @Test + fun testIsApproximatelyEqualTo_twoAndTwoOutsideThreshold_returnsFalse() { + val result = TWO_REAL.isApproximatelyEqualTo(2.0 + FLOAT_EQUALITY_INTERVAL * 2.0) + + assertThat(result).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_oneHalfAndOne_returnsFalse() { + val result = ONE_HALF_REAL.isApproximatelyEqualTo(1.0) + + assertThat(result).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_oneHalfAndPointFive_returnsTrue() { + val result = ONE_HALF_REAL.isApproximatelyEqualTo(0.5) + + assertThat(result).isTrue() + } + + @Test + fun testIsApproximatelyEqualTo_oneHalfAndPointSix_returnsFalse() { + val result = ONE_HALF_REAL.isApproximatelyEqualTo(0.6) + + assertThat(result).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_pointFiveAndPointFive_returnsTrue() { + val pointFiveReal = createIrrationalReal(0.5) + + val result = pointFiveReal.isApproximatelyEqualTo(0.5) + + assertThat(result).isTrue() + } + + @Test + fun testIsApproximatelyEqualTo_pointFiveAndPointSix_returnsFalse() { + val pointFiveReal = createIrrationalReal(0.5) + + val result = pointFiveReal.isApproximatelyEqualTo(0.6) + + assertThat(result).isFalse() + } + + @Test + fun testIsApproximatelyZero_default_throwsException() { + val defaultReal = Real.getDefaultInstance() + + val exception = assertThrows(IllegalStateException::class) { defaultReal.isApproximatelyZero() } + + assertThat(exception).hasMessageThat().contains("Invalid real") + } + + @Test + fun testIsApproximatelyZero_zeroInteger_returnsTrue() { + val result = ZERO_REAL.isApproximatelyZero() + + assertThat(result).isTrue() + } + + @Test + fun testIsApproximatelyZero_twoInteger_returnsFalse() { + val result = TWO_REAL.isApproximatelyZero() + + assertThat(result).isFalse() + } + + @Test + fun testIsApproximatelyZero_zeroFraction_returnsTrue() { + val real = createRationalReal(ZERO_FRACTION) + + val result = real.isApproximatelyZero() + + assertThat(result).isTrue() + } + + @Test + fun testIsApproximatelyZero_negativeZeroFraction_returnsTrue() { + val real = createRationalReal(-ZERO_FRACTION) + + val result = real.isApproximatelyZero() + + assertThat(result).isTrue() + } + + @Test + fun testIsApproximatelyZero_oneHalfFraction_returnsFalse() { + val result = ONE_HALF_REAL.isApproximatelyZero() + + assertThat(result).isFalse() + } + + @Test + fun testIsApproximatelyZero_zeroIrrational_returnsTrue() { + val real = createIrrationalReal(0.0) + + val result = real.isApproximatelyZero() + + assertThat(result).isTrue() + } + + @Test + fun testIsApproximatelyZero_irrationalCloseToZero_returnsTrue() { + val real = createIrrationalReal(0.000000001) + + val result = real.isApproximatelyZero() + + assertThat(result).isTrue() + } + + @Test + fun testIsApproximatelyZero_piIrrational_returnsFalse() { + val result = PI_REAL.isApproximatelyZero() + + assertThat(result).isFalse() + } + @Test fun testToDouble_default_returnsZeroDouble() { val defaultReal = Real.getDefaultInstance() @@ -239,135 +482,145 @@ class RealExtensionsTest { } @Test - fun testToPlainText_default_returnsEmptyString() { + fun testAsWholeNumber_default_throwsException() { val defaultReal = Real.getDefaultInstance() - val result = defaultReal.toPlainText() + val exception = assertThrows(IllegalStateException::class) { defaultReal.asWholeNumber() } - assertThat(result).isEmpty() + assertThat(exception).hasMessageThat().contains("Invalid real") } @Test - fun testToPlainText_twoInteger_returnsTwoString() { - val result = TWO_REAL.toPlainText() + fun testAsWholeNumber_twoInteger_returnsTwo() { + val result = TWO_REAL.asWholeNumber() - assertThat(result).isEqualTo("2") + assertThat(result).isEqualTo(2) } @Test - fun testToPlainText_negativeTwoInteger_returnsMinusTwoString() { - val result = NEGATIVE_TWO_REAL.toPlainText() + fun testAsWholeNumber_negativeTwoInteger_returnsNegativeTwo() { + val result = NEGATIVE_TWO_REAL.asWholeNumber() - assertThat(result).isEqualTo("-2") + assertThat(result).isEqualTo(-2) } @Test - fun testToPlainText_oneHalfFraction_returnsOneHalfString() { - val result = ONE_HALF_REAL.toPlainText() + fun testAsWholeNumber_oneHalfFraction_returnsNull() { + val result = ONE_HALF_REAL.asWholeNumber() - assertThat(result).isEqualTo("1/2") + assertThat(result).isNull() } @Test - fun testToPlainText_negativeOneHalfFraction_returnsMinusOneHalfString() { - val result = NEGATIVE_ONE_HALF_REAL.toPlainText() + fun testAsWholeNumber_threeOnesFraction_returnsNull() { + val result = THREE_ONES_REAL.asWholeNumber() - assertThat(result).isEqualTo("-1/2") + // 3/1 is treated as a fraction despite being numerically equivalent to a whole number. + assertThat(result).isNull() } @Test - fun testToPlainText_oneAndOneHalfFraction_returnsThreeHalvesString() { - val result = ONE_AND_ONE_HALF_REAL.toPlainText() + fun testAsWholeNumber_threeFraction_returnsThree() { + val result = THREE_FRACTION_REAL.asWholeNumber() - assertThat(result).isEqualTo("3/2") + assertThat(result).isEqualTo(3) } @Test - fun testToPlainText_negativeOneAndOneHalfFraction_returnsMinusThreeHalvesString() { - val result = NEGATIVE_ONE_AND_ONE_HALF_REAL.toPlainText() + fun testAsWholeNumber_negativeThreeFraction_returnsNegativeThree() { + val result = NEGATIVE_THREE_FRACTION_REAL.asWholeNumber() - assertThat(result).isEqualTo("-3/2") + assertThat(result).isEqualTo(-3) } @Test - fun testToPlainText_piIrrational_returnsPiString() { - val result = PI_REAL.toPlainText() + fun testAsWholeNumber_piIrrational_returnsNull() { + val result = PI_REAL.asWholeNumber() - assertThat(result).isEqualTo("3.1415") + assertThat(result).isNull() } @Test - fun testToPlainText_negativePiIrrational_returnsMinusPiString() { - val result = NEGATIVE_PI_REAL.toPlainText() + fun testAsWholeNumber_threeIrrational_returnsNull() { + val real = createIrrationalReal(3.0) - assertThat(result).isEqualTo("-3.1415") + val result = real.asWholeNumber() + + // Despite 3.0 being approximately a whole number, it isn't considered one since it's a double + // (and thus can have precision loss). + assertThat(result).isNull() } @Test - fun testIsApproximatelyEqualTo_zeroIntegerAndZero_returnsTrue() { - val result = ZERO_REAL.isApproximatelyEqualTo(0.0) + fun testToPlainText_default_returnsEmptyString() { + val defaultReal = Real.getDefaultInstance() - assertThat(result).isTrue() + val result = defaultReal.toPlainText() + + assertThat(result).isEmpty() } @Test - fun testIsApproximatelyEqualTo_zeroAndOne_returnsFalse() { - val result = ZERO_REAL.isApproximatelyEqualTo(1.0) + fun testToPlainText_twoInteger_returnsTwoString() { + val result = TWO_REAL.toPlainText() - assertThat(result).isFalse() + assertThat(result).isEqualTo("2") } @Test - fun testIsApproximatelyEqualTo_twoAndTwoWithinThreshold_returnsTrue() { - val result = TWO_REAL.isApproximatelyEqualTo(2.0 + FLOAT_EQUALITY_INTERVAL / 2.0) + fun testToPlainText_negativeTwoInteger_returnsMinusTwoString() { + val result = NEGATIVE_TWO_REAL.toPlainText() - assertThat(result).isTrue() + assertThat(result).isEqualTo("-2") } @Test - fun testIsApproximatelyEqualTo_twoAndTwoOutsideThreshold_returnsFalse() { - val result = TWO_REAL.isApproximatelyEqualTo(2.0 + FLOAT_EQUALITY_INTERVAL * 2.0) + fun testToPlainText_oneHalfFraction_returnsOneHalfString() { + val result = ONE_HALF_REAL.toPlainText() - assertThat(result).isFalse() + assertThat(result).isEqualTo("1/2") } @Test - fun testIsApproximatelyEqualTo_oneHalfAndOne_returnsFalse() { - val result = ONE_HALF_REAL.isApproximatelyEqualTo(1.0) + fun testToPlainText_negativeOneHalfFraction_returnsMinusOneHalfString() { + val result = NEGATIVE_ONE_HALF_REAL.toPlainText() - assertThat(result).isFalse() + assertThat(result).isEqualTo("-1/2") } @Test - fun testIsApproximatelyEqualTo_oneHalfAndPointFive_returnsTrue() { - val result = ONE_HALF_REAL.isApproximatelyEqualTo(0.5) + fun testToPlainText_oneAndOneHalfFraction_returnsThreeHalvesString() { + val result = ONE_AND_ONE_HALF_REAL.toPlainText() - assertThat(result).isTrue() + assertThat(result).isEqualTo("3/2") } @Test - fun testIsApproximatelyEqualTo_oneHalfAndPointSix_returnsFalse() { - val result = ONE_HALF_REAL.isApproximatelyEqualTo(0.6) + fun testToPlainText_negativeOneAndOneHalfFraction_returnsMinusThreeHalvesString() { + val result = NEGATIVE_ONE_AND_ONE_HALF_REAL.toPlainText() - assertThat(result).isFalse() + assertThat(result).isEqualTo("-3/2") } @Test - fun testIsApproximatelyEqualTo_pointFiveAndPointFive_returnsTrue() { - val pointFiveReal = createIrrationalReal(0.5) - - val result = pointFiveReal.isApproximatelyEqualTo(0.5) + fun testToPlainText_piIrrational_returnsPiString() { + val result = PI_REAL.toPlainText() - assertThat(result).isTrue() + assertThat(result).isEqualTo("3.1415") } @Test - fun testIsApproximatelyEqualTo_pointFiveAndPointSix_returnsFalse() { - val pointFiveReal = createIrrationalReal(0.5) + fun testToPlainText_negativePiIrrational_returnsMinusPiString() { + val result = NEGATIVE_PI_REAL.toPlainText() - val result = pointFiveReal.isApproximatelyEqualTo(0.6) + assertThat(result).isEqualTo("-3.1415") + } - assertThat(result).isFalse() + @Test + fun testIsApproximatelyEqualTo_zeroIntegerAndZero_returnsTrue() { + val result = ZERO_REAL.isApproximatelyEqualTo(0.0) + + assertThat(result).isTrue() } @Test @@ -1559,13 +1812,14 @@ class RealExtensionsTest { } @Test - fun testPow_negativeIntToOneHalfFraction_throwsException() { + fun testPow_negativeIntToOneHalfFraction_returnsNull() { val lhsReal = createIntegerReal(-3) val rhsReal = createRationalReal(ONE_HALF_FRACTION) - val exception = assertThrows(IllegalStateException::class) { lhsReal pow rhsReal } + val result = lhsReal pow rhsReal - assertThat(exception).hasMessageThat().contains("Radicand results in imaginary number") + // Cannot take the square root of a negative number. + assertThat(result).isNull() } @Test @@ -1579,23 +1833,25 @@ class RealExtensionsTest { } @Test - fun testPow_negativeFractionToOneHalfFraction_throwsException() { + fun testPow_negativeFractionToOneHalfFraction_returnsNull() { val lhsReal = NEGATIVE_ONE_AND_ONE_HALF_REAL val rhsReal = createRationalReal(ONE_HALF_FRACTION) - val exception = assertThrows(IllegalStateException::class) { lhsReal pow rhsReal } + val result = lhsReal pow rhsReal - assertThat(exception).hasMessageThat().contains("Radicand results in imaginary number") + // Cannot take the square root of a negative number. + assertThat(result).isNull() } @Test - fun testPow_negativeFractionToNegativeFractionWithOddNumerator_throwsException() { + fun testPow_negativeFractionToNegativeFractionWithOddNumerator_returnsNull() { val lhsReal = createRationalReal((-4).toWholeNumberFraction()) val rhsReal = createRationalReal(-ONE_AND_ONE_HALF_FRACTION) - val exception = assertThrows(IllegalStateException::class) { lhsReal pow rhsReal } + val result = lhsReal pow rhsReal - assertThat(exception).hasMessageThat().contains("Radicand results in imaginary number") + // Cannot take an even root of a negative number. + assertThat(result).isNull() } @Test @@ -1640,12 +1896,13 @@ class RealExtensionsTest { } @Test - fun testSqrt_negativeInteger_throwsException() { + fun testSqrt_negativeInteger_returnsNull() { val real = createIntegerReal(-2) - val exception = assertThrows(IllegalStateException::class) { sqrt(real) } + val result = sqrt(real) - assertThat(exception).hasMessageThat().contains("Radicand results in imaginary number") + // Cannot take square root of a negative number. + assertThat(result).isNull() } @Test @@ -1676,12 +1933,13 @@ class RealExtensionsTest { } @Test - fun testSqrt_negativeFraction_throwsException() { + fun testSqrt_negativeFraction_returnsNull() { val real = createRationalReal((-2).toWholeNumberFraction()) - val exception = assertThrows(IllegalStateException::class) { sqrt(real) } + val result = sqrt(real) - assertThat(exception).hasMessageThat().contains("Radicand results in imaginary number") + // Cannot take the square root of a negative number. + assertThat(result).isNull() } @Test From 06ba279040bf14d859d25d86865c7e8f15bed251 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Tue, 1 Feb 2022 22:13:40 -0800 Subject: [PATCH 092/162] KDocs + exemptions. Also, clean up polynomial sorting. --- .../file_content_validation_checks.textproto | 1 + .../android/util/math/ComparatorExtensions.kt | 27 ++- .../math/ExpressionToPolynomialConverter.kt | 50 ++++- .../android/util/math/FloatExtensions.kt | 7 +- .../android/util/math/FractionExtensions.kt | 6 +- .../util/math/MathExpressionExtensions.kt | 5 + .../android/util/math/PolynomialExtensions.kt | 144 ++++++++++----- .../oppia/android/util/math/RealExtensions.kt | 20 ++ .../util/math/ComparatorExtensionsTest.kt | 174 ++++++++++++++++++ 9 files changed, 378 insertions(+), 56 deletions(-) diff --git a/scripts/assets/file_content_validation_checks.textproto b/scripts/assets/file_content_validation_checks.textproto index bcae6e30b40..0c9432585fb 100644 --- a/scripts/assets/file_content_validation_checks.textproto +++ b/scripts/assets/file_content_validation_checks.textproto @@ -290,6 +290,7 @@ file_content_checks { exempted_file_name: "utility/src/test/java/org/oppia/android/util/math/ExpressionToComparableOperationConverterTest.kt" exempted_file_name: "utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt" exempted_file_name: "utility/src/test/java/org/oppia/android/util/math/MathTokenizerTest.kt" + exempted_file_name: "utility/src/test/java/org/oppia/android/util/math/PolynomialExtensionsTest.kt" exempted_file_name: "utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt" exempted_file_patterns: "testing/src/main/java/org/oppia/android/testing/junit/.+?\\.kt" } diff --git a/utility/src/main/java/org/oppia/android/util/math/ComparatorExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/ComparatorExtensions.kt index e853a03bfb9..52469413541 100644 --- a/utility/src/main/java/org/oppia/android/util/math/ComparatorExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/ComparatorExtensions.kt @@ -14,11 +14,32 @@ import com.google.protobuf.MessageLite * all of their items are equal per this [Comparator], including duplicates. */ fun Comparator.compareIterables(first: Iterable, second: Iterable): Int { + return compareIterablesInternal(first, second, reverseItemSort = false) +} + +/** + * Compares two [Iterable]s based on an item [Comparator] and returns the result, in much the same + * way as [compareIterables] except this reverses the result (that is, [first] will be considered + * less than [second] if it's larger). + * + * This should be used in place of a standard 'reversed()' since it will properly reverse (both the + * internal sorting and the comparison needs to be reversed in order for the reversal to be + * correct). + */ +fun Comparator.compareIterablesReversed(first: Iterable, second: Iterable): Int { + // Note that first & second are reversed here. + return compareIterablesInternal(second, first, reverseItemSort = true) +} + +private fun Comparator.compareIterablesInternal( + first: Iterable, second: Iterable, reverseItemSort: Boolean +): Int { // Reference: https://stackoverflow.com/a/30107086. - val firstIter = first.sortedWith(this).iterator() - val secondIter = second.sortedWith(this).iterator() + val itemComparator = if (reverseItemSort) reversed() else this + val firstIter = first.sortedWith(itemComparator).iterator() + val secondIter = second.sortedWith(itemComparator).iterator() while (firstIter.hasNext() && secondIter.hasNext()) { - val comparison = this.compare(firstIter.next(), secondIter.next()) + val comparison = this.compare(firstIter.next(), secondIter.next()).coerceIn(-1 .. 1) if (comparison != 0) return comparison // Found a different item. } diff --git a/utility/src/main/java/org/oppia/android/util/math/ExpressionToPolynomialConverter.kt b/utility/src/main/java/org/oppia/android/util/math/ExpressionToPolynomialConverter.kt index e64aa1baa63..71a4c6c972a 100644 --- a/utility/src/main/java/org/oppia/android/util/math/ExpressionToPolynomialConverter.kt +++ b/utility/src/main/java/org/oppia/android/util/math/ExpressionToPolynomialConverter.kt @@ -25,15 +25,59 @@ import org.oppia.android.app.model.MathBinaryOperation.Operator as BinaryOperato import org.oppia.android.app.model.MathUnaryOperation.Operator as UnaryOperator import org.oppia.android.app.model.Real +/** + * Converter from [MathExpression] to [Polynomial]. + * + * See the separate protos for specifics on structure, and [reduceToPolynomial] for the actual + * conversion function. + */ class ExpressionToPolynomialConverter private constructor() { companion object { - // TODO: document that this generally only relate to algebraic expressions. - fun MathExpression.reduceToPolynomial(): Polynomial? = - replaceSquareRoots() + /** + * Returns a new [Polynomial] that represents this [MathExpression], or null if it's not a valid + * polynomial. + * + * Polynomials are defined as a list of terms where each term has a coefficient and zero or more + * variables. There are a number of specific constraints that this function guarantees for all + * returned polynomials: + * - Terms will never have duplicate variable expressions (e.g. there will never be a returned + * polynomial with multiple 'x' terms, but there can be an 'x' and 'x^2' term). This is + * because effort is taken to combine like terms. + * - Terms are always sorted by lexicography of the variable names and variable powers which + * allows for comparison that operates independently of commutativity, associativity, and + * distributivity. + * - There will only ever be at most one constant term in the polynomial. + * - There will always be at least 1 term (even if it's the constant zero). + * - The polynomial will be mathematically equivalent to the original expression. + * - Coefficients will be kept to the highest possible precision (i.e. integers and fractions + * will be preferred over irrationals unless a rounding error occurs). + * - Most polynomial operations will be computed, including unary negation, addition, + * subtraction, multiplication (both implicit and explicit), division, and powers. + * + * Note that this will return null if a polynomial cannot be computed, such as in the cases: + * - The expression represents a division where the result has a remainder polynomial. + * - The expression results in a variable with a negative power or a division by an expression. + * - The expression results in a non-integer power (which includes a current limitation for + * expressions like 'sqrt(x)^2'; these cannot pass because internally the method cannot + * represent 'x^1/2'). + * - The expression results in a power variable (which can never represent a polynomial). + * - The expression is invalid (e.g. a default proto instance). + * + * This function is only expected to be used in conjunction with algebraic expressions. It's + * suggested to use evaluation when comparing for equivalence among numeric expressions as it + * should yield the same result and be more performant. + * + * The tests for this method provide very thorough and broad examples of different cases that + * this function supports. In particular, the equality tests are useful to see what sorts of + * expressions can be considered the same per [Polynomial] representation. + */ + fun MathExpression.reduceToPolynomial(): Polynomial? { + return replaceSquareRoots() .reduceToPolynomialAux() ?.removeUnnecessaryVariables() ?.simplifyRationals() ?.sort() + } private fun MathExpression.replaceSquareRoots(): MathExpression { return when (expressionTypeCase) { diff --git a/utility/src/main/java/org/oppia/android/util/math/FloatExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/FloatExtensions.kt index 27ac002c08c..96d072a669c 100644 --- a/utility/src/main/java/org/oppia/android/util/math/FloatExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/FloatExtensions.kt @@ -2,7 +2,12 @@ package org.oppia.android.util.math import kotlin.math.abs -/** The error margin used for approximately [Float] and [Double] equality checking. */ +/** + * The error margin used for approximating [Float] and [Double] equality checking, that is, the + * largest distance from any particular number before a new value will be considered unequal (i.e. + * all values between a float and (float-interval, float+interval) will be considered equal to the + * float). + */ const val FLOAT_EQUALITY_INTERVAL = 1e-5 /** diff --git a/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt index bd0c8093ede..8a762f4515a 100644 --- a/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/FractionExtensions.kt @@ -10,8 +10,10 @@ fun Fraction.hasFractionalPart(): Boolean { } /** - * Returns whether this fraction only represents a whole number. Note that for the fraction '0' this - * will return true. + * Returns whether this fraction only represents a whole number. + * + * Note that for the fraction '0' this will return true. Furthermore, this will return false for + * whole number-like improper fractions such as '3/1'. */ fun Fraction.isOnlyWholeNumber(): Boolean { return !hasFractionalPart() diff --git a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt index 65d40515f5f..f537d3d00cf 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt @@ -47,4 +47,9 @@ fun MathExpression.evaluateAsNumericExpression(): Real? = evaluate() */ fun MathExpression.toComparableOperation(): ComparableOperation = convertToComparableOperation() +/** + * Returns the [Polynomial] representation of this [MathExpression]. + * + * See [reduceToPolynomial] for details. + */ fun MathExpression.toPolynomial(): Polynomial? = reduceToPolynomial() diff --git a/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt index e64f0aaabab..1b9f55d53c8 100644 --- a/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt @@ -5,35 +5,15 @@ import org.oppia.android.app.model.Polynomial import org.oppia.android.app.model.Polynomial.Term import org.oppia.android.app.model.Polynomial.Term.Variable import org.oppia.android.app.model.Real -import java.util.SortedSet +/** Represents a single-term constant polynomial with the value of 0. */ val ZERO_POLYNOMIAL: Polynomial = createConstantPolynomial(ZERO) +/** Represents a single-term constant polynomial with the value of 1. */ val ONE_POLYNOMIAL: Polynomial = createConstantPolynomial(ONE) -// TODO: Kotlin-ify. -private val POLYNOMIAL_VARIABLE_COMPARATOR: Comparator by lazy { - // Note that power is reversed because larger powers should actually be sorted ahead of smaller - // powers for the same variable name (but variable name still takes precedence). This ensures - // cases like x^2y+y^2x are sorted in that order. - Comparator.comparing(Variable::getName).thenComparingReversed(Variable::getPower) -} - -private val POLYNOMIAL_TERM_COMPARATOR: Comparator by lazy { - // First, sort by all variable names to ensure xy is placed ahead of xz. Then, sort by variable - // powers in order of the variables (such that x^2y is ranked higher thank xy). Finally, sort by - // the coefficient to ensure equality through the comparator works correctly (though in practice - // like terms should always be combined). Note the specific reversing happening here. It's done in - // this way so that sorted set bigger/smaller list is reversed (which matches expectations since - // larger terms should appear earlier in the results). This is implementing an ordering similar to - // https://en.wikipedia.org/wiki/Polynomial#Definition, except for multi-variable functions (where - // variables of higher degree are preferred over lower degree by lexicographical order of variable - // names). - Comparator.comparing>( - { term -> term.variableList.toSortedSet(POLYNOMIAL_VARIABLE_COMPARATOR) }, - POLYNOMIAL_VARIABLE_COMPARATOR.reversed().toSetComparator() - ).reversed().thenComparing(Term::getCoefficient, REAL_COMPARATOR.reversed()) -} +private val POLYNOMIAL_VARIABLE_COMPARATOR by lazy { createVariableComparator() } +private val POLYNOMIAL_TERM_COMPARATOR by lazy { createTermComparator() } /** Returns whether this polynomial is a constant-only polynomial (contains no variables). */ fun Polynomial.isConstant(): Boolean = termCount == 1 && getTerm(0).variableCount == 0 @@ -63,6 +43,12 @@ fun Polynomial.toPlainText(): String { } } +/** + * Returns a version of this [Polynomial] with all zero-coefficient terms removed. + * + * This function guarantees that the returned polynomial have at least 1 term (even if it's just the + * constant zero). + */ fun Polynomial.removeUnnecessaryVariables(): Polynomial { return Polynomial.newBuilder().apply { addAllTerm( @@ -73,6 +59,14 @@ fun Polynomial.removeUnnecessaryVariables(): Polynomial { }.build().ensureAtLeastConstant() } +/** + * Returns a version of this [Polynomial] with all rational coefficients potentially simplified to + * integer terms. + * + * A rational coefficient can be simplified iff: + * - It has no fractional representation (which includes zero fraction cases). + * - It has a denominator of 1 (which represents a whole number, even for improper fractions). + */ fun Polynomial.simplifyRationals(): Polynomial { return Polynomial.newBuilder().apply { addAllTerm( @@ -85,6 +79,19 @@ fun Polynomial.simplifyRationals(): Polynomial { }.build() } +/** + * Returns a sorted version of this [Polynomial]. + * + * The returned version guarantees a repeatable and deterministic order that prioritizes variables + * earlier in the alphabet (or have lower lexicographical order), and have higher powers. Some + * examples: + * - 'x' will appear before '1'. + * - 'x^2' will appear before 'x'. + * - 'x' will appear before 'y'. + * - 'xy' will appear before 'x' and 'y'. + * - 'x^2y' will appear before 'xy^2', but after 'x^2y^2'. + * - 'xy^2' will appear before 'xy'. + */ fun Polynomial.sort(): Polynomial = Polynomial.newBuilder().apply { // The double sorting here is less efficient, but it ensures both terms and variables are // correctly kept sorted. Fortunately, most internal operations will keep variables sorted by @@ -99,6 +106,10 @@ fun Polynomial.sort(): Polynomial = Polynomial.newBuilder().apply { ) }.build() +/** + * Returns the negated version of this [Polynomial] such that the original polynomial plus the + * negative version would yield zero. + */ operator fun Polynomial.unaryMinus(): Polynomial { // Negating a polynomial just requires flipping the signs on all coefficients. return toBuilder() @@ -107,6 +118,14 @@ operator fun Polynomial.unaryMinus(): Polynomial { .build() } +/** + * Returns the sum of this [Polynomial] with [rhs]. + * + * The returned polynomial is guaranteed to: + * - Have all like terms combined. + * - Have simplified rational coefficients (per [simplifyRationals]. + * - Have no zero coefficients (unless the entire polynomial is zero, in which case just 1). + */ operator fun Polynomial.plus(rhs: Polynomial): Polynomial { // Adding two polynomials just requires combining their terms lists (taking into account combining // common terms). @@ -115,11 +134,25 @@ operator fun Polynomial.plus(rhs: Polynomial): Polynomial { }.build().combineLikeTerms().simplifyRationals().removeUnnecessaryVariables() } +/** + * Returns the subtraction of [rhs] from this [Polynomial]. + * + * The returned polynomial, when added with [rhs], will always equal the original polynomial. + * + * The returned polynomial has the same guarantee as those returned from [Polynomial.plus]. + */ operator fun Polynomial.minus(rhs: Polynomial): Polynomial { // a - b = a + -b return this + -rhs } +/** + * Returns the product of this [Polynomial] with [rhs]. + * + * This will correctly cross-multiply terms, for example: (1+x)*(1-x) will become 1-x^2. + * + * The returned polynomial has the same guarantee as those returned from [Polynomial.plus]. + */ operator fun Polynomial.times(rhs: Polynomial): Polynomial { // Polynomial multiplication is simply multiplying each term in one by each term in the other. val crossMultipliedTerms = termList.flatMap { leftTerm -> @@ -134,6 +167,15 @@ operator fun Polynomial.times(rhs: Polynomial): Polynomial { }.reduce(Polynomial::plus).simplifyRationals().removeUnnecessaryVariables() } +/** + * Returns the division of [rhs] from this [Polynomial], or null if there's a remainder after + * attempting the division. + * + * If this function returns non-null, it's guaranteed that the quotient times the divisor will yield + * the dividend. + * + * The returned polynomial has the same guarantee as those returned from [Polynomial.plus]. + */ operator fun Polynomial.div(rhs: Polynomial): Polynomial? { // See https://en.wikipedia.org/wiki/Polynomial_long_division#Pseudocode for reference. if (rhs.isApproximatelyZero()) { @@ -160,6 +202,17 @@ operator fun Polynomial.div(rhs: Polynomial): Polynomial? { return quotient.takeIf { remainder.isApproximatelyZero() } } +/** + * Returns the [Polynomial] that represents this [Polynomial] raised to [exp], or null if the result + * is not a valid polynomial or if a proper polynomial could not be kept along the way. + * + * This function will fail in a number of cases, including: + * - If [exp] is not a constant polynomial. + * - If this polynomial has more than one term (since that requires factoring). + * - If the result would yield a polynomial with a negative power. + * + * The returned polynomial has the same guarantee as those returned from [Polynomial.plus]. + */ infix fun Polynomial.pow(exp: Polynomial): Polynomial? { // Polynomial exponentiation is only supported if the right side is a constant polynomial, // otherwise the result cannot be a polynomial (though could still be compared to another @@ -415,28 +468,25 @@ private fun Real.maybeSimplifyRationalToInteger(): Real = when (realTypeCase) { null -> this } -// TODO: figure out of this can be removed. -private fun > Comparator.thenComparingReversed( - keySelector: (T) -> U -): Comparator = thenComparing(Comparator.comparing(keySelector).reversed()) - -// TODO: figure out of this can be removed. -private fun Comparator.toSetComparator(): Comparator> { - val itemComparator = this - return Comparator { first, second -> - // Reference: https://stackoverflow.com/a/30107086. - val firstIter = first.iterator() - val secondIter = second.iterator() - while (firstIter.hasNext() && secondIter.hasNext()) { - val comparison = itemComparator.compare(firstIter.next(), secondIter.next()) - if (comparison != 0) return@Comparator comparison // Found a different item. - } +private fun createTermComparator(): Comparator { + // First, sort by all variable names to ensure xy is placed ahead of xz. Then, sort by variable + // powers in order of the variables (such that x^2y is ranked higher thank xy). Finally, sort by + // the coefficient to ensure equality through the comparator works correctly (though in practice + // like terms should always be combined). Note the specific reversing happening here. It's done in + // this way so that sorted set bigger/smaller list is reversed (which matches expectations since + // larger terms should appear earlier in the results). This is implementing an ordering similar to + // https://en.wikipedia.org/wiki/Polynomial#Definition, except for multi-variable functions (where + // variables of higher degree are preferred over lower degree by lexicographical order of variable + // names). + val reversedVariableComparator = POLYNOMIAL_VARIABLE_COMPARATOR.reversed() + return compareBy>( + reversedVariableComparator::compareIterablesReversed, Term::getVariableList + ).thenByDescending(REAL_COMPARATOR, Term::getCoefficient) +} - // Everything is equal up to here, see if the lists are different length. - return@Comparator when { - firstIter.hasNext() -> 1 // The first list is longer, therefore "greater." - secondIter.hasNext() -> -1 // Ditto, but for the second list. - else -> 0 // Otherwise, they're the same length with all equal items (and are thus equal). - } - } +private fun createVariableComparator(): Comparator { + // Note that power is reversed because larger powers should actually be sorted ahead of smaller + // powers for the same variable name (but variable name still takes precedence). This ensures + // cases like x^2y+y^2x are sorted in that order. + return compareBy(Variable::getName).thenByDescending(Variable::getPower) } diff --git a/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt index 25b520d5a3e..607277ed7ad 100644 --- a/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt @@ -9,14 +9,17 @@ import org.oppia.android.app.model.Real.RealTypeCase.REALTYPE_NOT_SET import kotlin.math.absoluteValue import kotlin.math.pow +/** Represents an integer [Real] with value 0. */ val ZERO: Real by lazy { Real.newBuilder().apply { integer = 0 }.build() } +/** Represents an integer [Real] with value 1. */ val ONE: Real by lazy { Real.newBuilder().apply { integer = 1 }.build() } +/** Represents a rational fraction [Real] with value 1/2. */ val ONE_HALF: Real by lazy { Real.newBuilder().apply { rational = Fraction.newBuilder().apply { @@ -48,6 +51,12 @@ fun Real.isRational(): Boolean = realTypeCase == RATIONAL */ fun Real.isInteger(): Boolean = realTypeCase == INTEGER +/** + * Returns whether this [Real] is explicitly a whole number, that is, either an integer or a + * [Fraction] that's also a whole number. + * + * Note that this has the same limitations as [Fraction.isOnlyWholeNumber] for rational values. + */ fun Real.isWholeNumber(): Boolean { return when (realTypeCase) { RATIONAL -> rational.isOnlyWholeNumber() @@ -72,11 +81,14 @@ fun Real.isApproximatelyEqualTo(value: Double): Boolean { return toDouble().approximatelyEquals(value) } +/** Returns whether this [Real] is approximately zero per [Double.approximatelyEquals]. */ fun Real.isApproximatelyZero(): Boolean = isApproximatelyEqualTo(0.0) /** * Returns a [Double] representation of this [Real] that is approximately the same value (per * [isApproximatelyEqualTo]). + * + * This method throws an exception if this [Real] is invalid (such as a default proto instance). */ fun Real.toDouble(): Double { return when (realTypeCase) { @@ -87,6 +99,14 @@ fun Real.toDouble(): Double { } } +/** + * Returns the whole-number representation of this [Real], or null if there isn't one. + * + * This function should only be called if [isWholeNumber] returns true. The contract of that + * function guarantees that a non-null integer can be returned here for whole number reals. + * + * This method throws an exception if this [Real] is invalid (such as a default proto instance). + */ fun Real.asWholeNumber(): Int? { return when (realTypeCase) { RATIONAL -> if (rational.isOnlyWholeNumber()) rational.toWholeNumber() else null diff --git a/utility/src/test/java/org/oppia/android/util/math/ComparatorExtensionsTest.kt b/utility/src/test/java/org/oppia/android/util/math/ComparatorExtensionsTest.kt index 5c44c17968e..a1c73b45d71 100644 --- a/utility/src/test/java/org/oppia/android/util/math/ComparatorExtensionsTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/ComparatorExtensionsTest.kt @@ -204,6 +204,180 @@ class ComparatorExtensionsTest { assertThat(compareResult).isEqualTo(0) } + @Test + fun testCompareIterablesReversed_emptyList_emptyList_returnsZero() { + val leftList = listOf() + val rightList = listOf() + + val compareResult = stringComparator.compareIterablesReversed(leftList, rightList) + + assertThat(compareResult).isEqualTo(0) + } + + @Test + fun testCompareIterablesReversed_singletonList_emptyList_returnsNegativeOne() { + val leftList = listOf("1") + val rightList = listOf() + + val compareResult = stringComparator.compareIterablesReversed(leftList, rightList) + + assertThat(compareResult).isEqualTo(-1) + } + + @Test + fun testCompareIterablesReversed_emptyList_singletonList_returnsOne() { + val leftList = listOf() + val rightList = listOf("1") + + val compareResult = stringComparator.compareIterablesReversed(leftList, rightList) + + assertThat(compareResult).isEqualTo(1) + } + + @Test + fun testCompareIterablesReversed_singletonList_singletonList_sameElems_returnsZero() { + val leftList = listOf("1") + val rightList = listOf("1") + + val compareResult = stringComparator.compareIterablesReversed(leftList, rightList) + + assertThat(compareResult).isEqualTo(0) + } + + @Test + fun testCompareIterablesReversed_twoItemList_singletonList_commonElem_returnsNegativeOne() { + val leftList = listOf("1", "2") + val rightList = listOf("1") + + val compareResult = stringComparator.compareIterablesReversed(leftList, rightList) + + assertThat(compareResult).isEqualTo(-1) + } + + @Test + fun testCompareIterablesReversed_singletonList_twoItemList_commonElem_returnsOne() { + val leftList = listOf("1") + val rightList = listOf("1", "2") + + val compareResult = stringComparator.compareIterablesReversed(leftList, rightList) + + assertThat(compareResult).isEqualTo(1) + } + + @Test + fun testCompareIterablesReversed_equalSizeLists_sameItems_sameOrder_returnsZero() { + val leftList = listOf("1", "2") + val rightList = listOf("1", "2") + + val compareResult = stringComparator.compareIterablesReversed(leftList, rightList) + + assertThat(compareResult).isEqualTo(0) + } + + @Test + fun testCompareIterablesReversed_equalSizeLists_sameItems_differentOrder_returnsZero() { + val leftList = listOf("1", "2") + val rightList = listOf("2", "1") + + val compareResult = stringComparator.compareIterablesReversed(leftList, rightList) + + // Order shouldn't matter. + assertThat(compareResult).isEqualTo(0) + } + + @Test + fun testCompareIterablesReversed_list223_list123_returnsNegativeOne() { + val leftList = listOf("2", "2", "3") + val rightList = listOf("1", "2", "3") + + val compareResult = stringComparator.compareIterablesReversed(leftList, rightList) + + assertThat(compareResult).isEqualTo(-1) + } + + @Test + fun testCompareIterablesReversed_list123_list223_returnsOne() { + val leftList = listOf("1", "2", "3") + val rightList = listOf("2", "2", "3") + + val compareResult = stringComparator.compareIterablesReversed(leftList, rightList) + + assertThat(compareResult).isEqualTo(1) + } + + @Test + fun testCompareIterablesReversed_list123_list11_returnsNegativeOne() { + val leftList = listOf("1", "2", "3") + val rightList = listOf("1", "1") + + val compareResult = stringComparator.compareIterablesReversed(leftList, rightList) + + assertThat(compareResult).isEqualTo(-1) + } + + @Test + fun testCompareIterablesReversed_list123_list13_returnsNegativeOne() { + val leftList = listOf("1", "2", "3") + val rightList = listOf("1", "3") + + val compareResult = stringComparator.compareIterablesReversed(leftList, rightList) + + assertThat(compareResult).isEqualTo(-1) + } + + @Test + fun testCompareIterablesReversed_list223_list1_returnsNegativeOne() { + val leftList = listOf("1", "2", "3") + val rightList = listOf("1") + + val compareResult = stringComparator.compareIterablesReversed(leftList, rightList) + + assertThat(compareResult).isEqualTo(-1) + } + + @Test + fun testCompareIterablesReversed_list123_list2_returnsNegativeNegativeOne() { + val leftList = listOf("1", "2", "3") + val rightList = listOf("2") + + val compareResult = stringComparator.compareIterablesReversed(leftList, rightList) + + assertThat(compareResult).isEqualTo(-1) + } + + @Test + fun testCompareIterablesReversed_list22_list2_returnsNegativeOne() { + val leftList = listOf("2", "2") + val rightList = listOf("2") + + val compareResult = stringComparator.compareIterablesReversed(leftList, rightList) + + // The first list has an extra element. This also verifies that duplicates are correctly + // considered during comparison. + assertThat(compareResult).isEqualTo(-1) + } + + @Test + fun testCompareIterablesReversed_list2_list22_returnsOne() { + val leftList = listOf("2") + val rightList = listOf("2", "2") + + val compareResult = stringComparator.compareIterablesReversed(leftList, rightList) + + // The second list has an extra element. + assertThat(compareResult).isEqualTo(1) + } + + @Test + fun testCompareIterablesReversed_list22_list22_returnsZero() { + val leftList = listOf("2", "2") + val rightList = listOf("2", "2") + + val compareResult = stringComparator.compareIterablesReversed(leftList, rightList) + + assertThat(compareResult).isEqualTo(0) + } + @Test fun testCompareProtos_defaultAndDefault_returnsZero() { val leftProto = TestMessage.newBuilder().build() From ad3091d5046c08666428235b722732b190093e06 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Tue, 1 Feb 2022 22:14:58 -0800 Subject: [PATCH 093/162] Lint fixes. --- .../org/oppia/android/util/math/ComparatorExtensions.kt | 6 ++++-- .../android/util/math/ExpressionToPolynomialConverter.kt | 4 ++-- .../oppia/android/util/math/MathExpressionExtensions.kt | 7 ------- .../org/oppia/android/util/math/PolynomialExtensions.kt | 5 +++-- .../util/math/ExpressionToPolynomialConverterTest.kt | 5 +++-- .../android/util/math/MathExpressionExtensionsTest.kt | 1 - .../oppia/android/util/math/PolynomialExtensionsTest.kt | 2 +- .../java/org/oppia/android/util/math/RealExtensionsTest.kt | 1 - 8 files changed, 13 insertions(+), 18 deletions(-) diff --git a/utility/src/main/java/org/oppia/android/util/math/ComparatorExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/ComparatorExtensions.kt index 52469413541..c952e56686e 100644 --- a/utility/src/main/java/org/oppia/android/util/math/ComparatorExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/ComparatorExtensions.kt @@ -32,14 +32,16 @@ fun Comparator.compareIterablesReversed(first: Iterable, second: Itera } private fun Comparator.compareIterablesInternal( - first: Iterable, second: Iterable, reverseItemSort: Boolean + first: Iterable, + second: Iterable, + reverseItemSort: Boolean ): Int { // Reference: https://stackoverflow.com/a/30107086. val itemComparator = if (reverseItemSort) reversed() else this val firstIter = first.sortedWith(itemComparator).iterator() val secondIter = second.sortedWith(itemComparator).iterator() while (firstIter.hasNext() && secondIter.hasNext()) { - val comparison = this.compare(firstIter.next(), secondIter.next()).coerceIn(-1 .. 1) + val comparison = this.compare(firstIter.next(), secondIter.next()).coerceIn(-1..1) if (comparison != 0) return comparison // Found a different item. } diff --git a/utility/src/main/java/org/oppia/android/util/math/ExpressionToPolynomialConverter.kt b/utility/src/main/java/org/oppia/android/util/math/ExpressionToPolynomialConverter.kt index 71a4c6c972a..3d487ff95b8 100644 --- a/utility/src/main/java/org/oppia/android/util/math/ExpressionToPolynomialConverter.kt +++ b/utility/src/main/java/org/oppia/android/util/math/ExpressionToPolynomialConverter.kt @@ -21,9 +21,9 @@ import org.oppia.android.app.model.MathUnaryOperation import org.oppia.android.app.model.MathUnaryOperation.Operator.NEGATE import org.oppia.android.app.model.MathUnaryOperation.Operator.POSITIVE import org.oppia.android.app.model.Polynomial +import org.oppia.android.app.model.Real import org.oppia.android.app.model.MathBinaryOperation.Operator as BinaryOperator import org.oppia.android.app.model.MathUnaryOperation.Operator as UnaryOperator -import org.oppia.android.app.model.Real /** * Converter from [MathExpression] to [Polynomial]. @@ -66,7 +66,7 @@ class ExpressionToPolynomialConverter private constructor() { * This function is only expected to be used in conjunction with algebraic expressions. It's * suggested to use evaluation when comparing for equivalence among numeric expressions as it * should yield the same result and be more performant. - * + * * The tests for this method provide very thorough and broad examples of different cases that * this function supports. In particular, the equality tests are useful to see what sorts of * expressions can be considered the same per [Polynomial] representation. diff --git a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt index f537d3d00cf..2924dc5418e 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt @@ -3,13 +3,6 @@ package org.oppia.android.util.math import org.oppia.android.app.model.ComparableOperation import org.oppia.android.app.model.MathEquation import org.oppia.android.app.model.MathExpression -import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.BINARY_OPERATION -import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.CONSTANT -import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.EXPRESSIONTYPE_NOT_SET -import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.FUNCTION_CALL -import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.GROUP -import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.UNARY_OPERATION -import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.VARIABLE import org.oppia.android.app.model.Polynomial import org.oppia.android.app.model.Real import org.oppia.android.util.math.ExpressionToComparableOperationConverter.Companion.convertToComparableOperation diff --git a/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt index 1b9f55d53c8..2b3dac6d421 100644 --- a/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt @@ -188,8 +188,9 @@ operator fun Polynomial.div(rhs: Polynomial): Polynomial? { val divisorVariable = leadingDivisorTerm.highestDegreeVariable() val divisorVariableName = divisorVariable?.name val divisorDegree = leadingDivisorTerm.highestDegree() - while (!remainder.isApproximatelyZero() - && (remainder.getDegree() ?: return null) >= divisorDegree) { + while (!remainder.isApproximatelyZero() && + (remainder.getDegree() ?: return null) >= divisorDegree + ) { // Attempt to divide the leading terms (this may fail). Note that the leading term should always // be based on the divisor variable being used (otherwise subsequent division steps will be // inconsistent and potentially fail to resolve). diff --git a/utility/src/test/java/org/oppia/android/util/math/ExpressionToPolynomialConverterTest.kt b/utility/src/test/java/org/oppia/android/util/math/ExpressionToPolynomialConverterTest.kt index 84d4f434731..bcf64991b55 100644 --- a/utility/src/test/java/org/oppia/android/util/math/ExpressionToPolynomialConverterTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/ExpressionToPolynomialConverterTest.kt @@ -1,7 +1,7 @@ package org.oppia.android.util.math -import com.google.common.truth.Truth.assertThat import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat import org.junit.Test import org.junit.runner.RunWith import org.oppia.android.app.model.MathExpression @@ -2310,7 +2310,8 @@ class ExpressionToPolynomialConverterTest { private companion object { private fun parseAlgebraicExpression( - expression: String, errorCheckingMode: ErrorCheckingMode = ALL_ERRORS + expression: String, + errorCheckingMode: ErrorCheckingMode = ALL_ERRORS ): MathExpression { return MathExpressionParser.parseAlgebraicExpression( expression, allowedVariables = listOf("x", "y", "z"), errorCheckingMode diff --git a/utility/src/test/java/org/oppia/android/util/math/MathExpressionExtensionsTest.kt b/utility/src/test/java/org/oppia/android/util/math/MathExpressionExtensionsTest.kt index 8be3ee05bc3..7a52039d973 100644 --- a/utility/src/test/java/org/oppia/android/util/math/MathExpressionExtensionsTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/MathExpressionExtensionsTest.kt @@ -6,7 +6,6 @@ import org.junit.Test import org.junit.runner.RunWith import org.oppia.android.app.model.MathEquation import org.oppia.android.app.model.MathExpression -import org.oppia.android.testing.math.PolynomialSubject import org.oppia.android.testing.math.PolynomialSubject.Companion.assertThat import org.oppia.android.testing.math.RealSubject.Companion.assertThat import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode.ALL_ERRORS diff --git a/utility/src/test/java/org/oppia/android/util/math/PolynomialExtensionsTest.kt b/utility/src/test/java/org/oppia/android/util/math/PolynomialExtensionsTest.kt index ed4887261ff..36b1aaa5224 100644 --- a/utility/src/test/java/org/oppia/android/util/math/PolynomialExtensionsTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/PolynomialExtensionsTest.kt @@ -12,9 +12,9 @@ import org.oppia.android.testing.junit.OppiaParameterizedTestRunner import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Iteration import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Parameter import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.RunParameterized +import org.oppia.android.testing.math.PolynomialSubject.Companion.assertThat import org.oppia.android.testing.math.RealSubject.Companion.assertThat import org.robolectric.annotation.LooperMode -import org.oppia.android.testing.math.PolynomialSubject.Companion.assertThat /** Tests for [Polynomial] extensions. */ // FunctionName: test names are conventionally named with underscores. diff --git a/utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt b/utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt index 978b4a014f9..842bd80f18c 100644 --- a/utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt @@ -10,7 +10,6 @@ import org.oppia.android.testing.junit.OppiaParameterizedTestRunner import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Iteration import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Parameter import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.RunParameterized -import org.oppia.android.testing.math.RealSubject import org.oppia.android.testing.math.RealSubject.Companion.assertThat import org.robolectric.annotation.LooperMode From b8ce188c751c444f4ab569c7295aca0182513b6f Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 2 Feb 2022 13:55:05 -0800 Subject: [PATCH 094/162] Post-merge fixes. Also, mark methods/classes that need tests. --- domain/BUILD.bazel | 2 +- ...AndInSimplestFormRuleClassifierProvider.kt | 4 +- ...putIsEquivalentToRuleClassifierProvider.kt | 4 +- ...ithUnitsIsEqualToRuleClassifierProvider.kt | 4 +- ...itsIsEquivalentToRuleClassifierProvider.kt | 4 +- ...putIsEquivalentToRuleClassifierProvider.kt | 21 ++++--- ...atchesExactlyWithRuleClassifierProvider.kt | 5 +- ...vialManipulationsRuleClassifierProvider.kt | 13 ++-- .../NumericExpressionInputModule.kt | 1 + ...umericInputEqualsRuleClassifierProvider.kt | 4 +- .../org/oppia/android/util/math/BUILD.bazel | 6 +- .../math/ComparableOperationExtensions.kt | 63 +++++++++++++++++++ .../math/ComparableOperationListExtensions.kt | 52 --------------- .../android/util/math/FloatExtensions.kt | 4 +- .../util/math/MathExpressionExtensions.kt | 19 +++--- .../android/util/math/MathExpressionParser.kt | 2 +- .../android/util/math/PolynomialExtensions.kt | 22 ++++--- .../oppia/android/util/math/RealExtensions.kt | 13 ++-- .../android/util/math/FloatExtensionsTest.kt | 22 +++---- 19 files changed, 144 insertions(+), 121 deletions(-) create mode 100644 utility/src/main/java/org/oppia/android/util/math/ComparableOperationExtensions.kt delete mode 100644 utility/src/main/java/org/oppia/android/util/math/ComparableOperationListExtensions.kt diff --git a/domain/BUILD.bazel b/domain/BUILD.bazel index 6c6bb620b5c..d40eceb2fd3 100755 --- a/domain/BUILD.bazel +++ b/domain/BUILD.bazel @@ -126,7 +126,7 @@ kt_android_library( "//utility/src/main/java/org/oppia/android/util/logging:event_logger", "//utility/src/main/java/org/oppia/android/util/logging:log_uploader", "//utility/src/main/java/org/oppia/android/util/math:extensions", - "//utility/src/main/java/org/oppia/android/util/math:parser", + "//utility/src/main/java/org/oppia/android/util/math:math_expression_parser", "//utility/src/main/java/org/oppia/android/util/networking:network_connection_util", "//utility/src/main/java/org/oppia/android/util/parser/html:exploration_html_parser_entity_type", "//utility/src/main/java/org/oppia/android/util/parser/image:image_parsing_annonations", diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProvider.kt index f9498f7d965..4a425174d31 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProvider.kt @@ -6,7 +6,7 @@ import org.oppia.android.app.model.WrittenTranslationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider -import org.oppia.android.util.math.approximatelyEquals +import org.oppia.android.util.math.isApproximatelyEqualTo import org.oppia.android.util.math.toDouble import org.oppia.android.util.math.toSimplestForm import javax.inject.Inject @@ -36,7 +36,7 @@ class FractionInputIsEquivalentToAndInSimplestFormRuleClassifierProvider input: Fraction, writtenTranslationContext: WrittenTranslationContext ): Boolean { - return answer.toDouble().approximatelyEquals(input.toDouble()) && + return answer.toDouble().isApproximatelyEqualTo(input.toDouble()) && answer == input.toSimplestForm() } } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToRuleClassifierProvider.kt index e2c42f7ec67..6846bd42652 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/FractionInputIsEquivalentToRuleClassifierProvider.kt @@ -6,7 +6,7 @@ import org.oppia.android.app.model.WrittenTranslationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider -import org.oppia.android.util.math.approximatelyEquals +import org.oppia.android.util.math.isApproximatelyEqualTo import org.oppia.android.util.math.toDouble import javax.inject.Inject @@ -34,6 +34,6 @@ class FractionInputIsEquivalentToRuleClassifierProvider @Inject constructor( input: Fraction, writtenTranslationContext: WrittenTranslationContext ): Boolean { - return answer.toDouble().approximatelyEquals(input.toDouble()) + return answer.toDouble().isApproximatelyEqualTo(input.toDouble()) } } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEqualToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEqualToRuleClassifierProvider.kt index 9a225cc41ca..387022c4563 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEqualToRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEqualToRuleClassifierProvider.kt @@ -7,7 +7,7 @@ import org.oppia.android.app.model.WrittenTranslationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider -import org.oppia.android.util.math.approximatelyEquals +import org.oppia.android.util.math.isApproximatelyEqualTo import javax.inject.Inject /** @@ -52,7 +52,7 @@ class NumberWithUnitsIsEqualToRuleClassifierProvider @Inject constructor( } private fun realMatches(answer: Double, input: Double): Boolean { - return input.approximatelyEquals(answer) + return input.isApproximatelyEqualTo(answer) } private fun fractionMatches(answer: Fraction, input: Fraction): Boolean { diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEquivalentToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEquivalentToRuleClassifierProvider.kt index e94fc9191e7..594291c3c29 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEquivalentToRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEquivalentToRuleClassifierProvider.kt @@ -6,7 +6,7 @@ import org.oppia.android.app.model.WrittenTranslationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider -import org.oppia.android.util.math.approximatelyEquals +import org.oppia.android.util.math.isApproximatelyEqualTo import org.oppia.android.util.math.toDouble import javax.inject.Inject @@ -41,7 +41,7 @@ class NumberWithUnitsIsEquivalentToRuleClassifierProvider @Inject constructor( } // Verify the float version of the value for approximate comparison. - return extractRealValue(input).approximatelyEquals(extractRealValue(answer)) + return extractRealValue(input).isApproximatelyEqualTo(extractRealValue(answer)) } private fun extractRealValue(number: NumberWithUnits): Double { diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProvider.kt index 50094be6215..9e533395999 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProvider.kt @@ -1,7 +1,8 @@ package org.oppia.android.domain.classify.rules.numericexpressioninput +import javax.inject.Inject import org.oppia.android.app.model.InteractionObject -import org.oppia.android.app.model.Polynomial +import org.oppia.android.app.model.Real import org.oppia.android.app.model.WrittenTranslationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier @@ -9,10 +10,10 @@ import org.oppia.android.domain.classify.rules.RuleClassifierProvider import org.oppia.android.util.logging.ConsoleLogger import org.oppia.android.util.math.MathExpressionParser import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult -import org.oppia.android.util.math.toPolynomial -import javax.inject.Inject -import org.oppia.android.util.math.approximatelyEquals +import org.oppia.android.util.math.evaluateAsNumericExpression +import org.oppia.android.util.math.isApproximatelyEqualTo +// TODO: add tests. class NumericExpressionInputIsEquivalentToRuleClassifierProvider @Inject constructor( private val classifierFactory: GenericRuleClassifier.Factory, private val consoleLogger: ConsoleLogger @@ -30,18 +31,18 @@ class NumericExpressionInputIsEquivalentToRuleClassifierProvider @Inject constru input: String, writtenTranslationContext: WrittenTranslationContext ): Boolean { - val answerExpression = parsePolynomial(answer) ?: return false - val inputExpression = parsePolynomial(input) ?: return false - return answerExpression.approximatelyEquals(inputExpression) + val answerValue = evaluateNumericExpression(answer) ?: return false + val inputValue = evaluateNumericExpression(input) ?: return false + return answerValue.isApproximatelyEqualTo(inputValue) } - private fun parsePolynomial(rawExpression: String): Polynomial? { + private fun evaluateNumericExpression(rawExpression: String): Real? { return when (val expResult = MathExpressionParser.parseNumericExpression(rawExpression)) { is MathParsingResult.Success -> { - expResult.result.toPolynomial().also { + expResult.result.evaluateAsNumericExpression().also { if (it == null) { consoleLogger.w( - "NumericExpEquivalent", "Expression is not a supported polynomial: $rawExpression." + "NumericExpEquivalent", "Expression failed to evaluate: $rawExpression." ) } } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProvider.kt index fe58ff2dca8..da2c5ce46c8 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProvider.kt @@ -10,8 +10,9 @@ import org.oppia.android.util.logging.ConsoleLogger import org.oppia.android.util.math.MathExpressionParser import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult import javax.inject.Inject -import org.oppia.android.util.math.approximatelyEquals +import org.oppia.android.util.math.isApproximatelyEqualTo +// TODO: add tests. class NumericExpressionInputMatchesExactlyWithRuleClassifierProvider @Inject constructor( private val classifierFactory: GenericRuleClassifier.Factory, private val consoleLogger: ConsoleLogger @@ -31,7 +32,7 @@ class NumericExpressionInputMatchesExactlyWithRuleClassifierProvider @Inject con ): Boolean { val answerExpression = parseNumericExpression(answer) ?: return false val inputExpression = parseNumericExpression(input) ?: return false - return answerExpression.approximatelyEquals(inputExpression) + return answerExpression.isApproximatelyEqualTo(inputExpression) } private fun parseNumericExpression(rawExpression: String): MathExpression? { diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt index 5e17c0c4173..34c94f3f489 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt @@ -1,6 +1,6 @@ package org.oppia.android.domain.classify.rules.numericexpressioninput -import org.oppia.android.app.model.ComparableOperationList +import org.oppia.android.app.model.ComparableOperation import org.oppia.android.app.model.InteractionObject import org.oppia.android.app.model.WrittenTranslationContext import org.oppia.android.domain.classify.RuleClassifier @@ -9,10 +9,11 @@ import org.oppia.android.domain.classify.rules.RuleClassifierProvider import org.oppia.android.util.logging.ConsoleLogger import org.oppia.android.util.math.MathExpressionParser import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult -import org.oppia.android.util.math.toComparableOperationList +import org.oppia.android.util.math.toComparableOperation import javax.inject.Inject -import org.oppia.android.util.math.approximatelyEquals +import org.oppia.android.util.math.isApproximatelyEqualTo +// TODO: add tests. class NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider @Inject constructor( private val classifierFactory: GenericRuleClassifier.Factory, @@ -33,12 +34,12 @@ class NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvide ): Boolean { val answerExpression = parseComparableOperationList(answer) ?: return false val inputExpression = parseComparableOperationList(input) ?: return false - return answerExpression.approximatelyEquals(inputExpression) + return answerExpression.isApproximatelyEqualTo(inputExpression) } - private fun parseComparableOperationList(rawExpression: String): ComparableOperationList? { + private fun parseComparableOperationList(rawExpression: String): ComparableOperation? { return when (val expResult = MathExpressionParser.parseNumericExpression(rawExpression)) { - is MathParsingResult.Success -> expResult.result.toComparableOperationList() + is MathParsingResult.Success -> expResult.result.toComparableOperation() is MathParsingResult.Failure -> { consoleLogger.e( "NumericExpTrivialManips", diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputModule.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputModule.kt index ca42ea19de0..6a2cf9b50cb 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputModule.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputModule.kt @@ -7,6 +7,7 @@ import dagger.multibindings.StringKey import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.NumericExpressionInputRules +// TODO: add tests. /** Module that binds rule classifiers corresponding to the numeric expression input interaction. */ @Module class NumericExpressionInputModule { diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputEqualsRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputEqualsRuleClassifierProvider.kt index 2c7a6dc5212..9468881f022 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputEqualsRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputEqualsRuleClassifierProvider.kt @@ -5,7 +5,7 @@ import org.oppia.android.app.model.WrittenTranslationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.GenericRuleClassifier import org.oppia.android.domain.classify.rules.RuleClassifierProvider -import org.oppia.android.util.math.approximatelyEquals +import org.oppia.android.util.math.isApproximatelyEqualTo import javax.inject.Inject /** @@ -30,5 +30,5 @@ class NumericInputEqualsRuleClassifierProvider @Inject constructor( answer: Double, input: Double, writtenTranslationContext: WrittenTranslationContext - ): Boolean = input.approximatelyEquals(answer) + ): Boolean = input.isApproximatelyEqualTo(answer) } diff --git a/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel b/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel index 8fa89d72797..bc975383271 100644 --- a/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel @@ -10,7 +10,7 @@ android_library( "//:oppia_api_visibility", ], exports = [ - ":comparable_operation_list_extensions", + ":comparable_operation_extensions", ":comparator_extensions", ":float_extensions", ":fraction_extensions", @@ -88,9 +88,9 @@ kt_android_library( ) kt_android_library( - name = "comparable_operation_list_extensions", + name = "comparable_operation_extensions", srcs = [ - "ComparableOperationListExtensions.kt", + "ComparableOperationExtensions.kt", ], deps = [ ":real_extensions", diff --git a/utility/src/main/java/org/oppia/android/util/math/ComparableOperationExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/ComparableOperationExtensions.kt new file mode 100644 index 00000000000..f66b5aeb8c8 --- /dev/null +++ b/utility/src/main/java/org/oppia/android/util/math/ComparableOperationExtensions.kt @@ -0,0 +1,63 @@ +package org.oppia.android.util.math + +import org.oppia.android.app.model.ComparableOperation +import org.oppia.android.app.model.ComparableOperation.CommutativeAccumulation +import org.oppia.android.app.model.ComparableOperation.ComparisonTypeCase.COMMUTATIVE_ACCUMULATION +import org.oppia.android.app.model.ComparableOperation.ComparisonTypeCase.COMPARISONTYPE_NOT_SET +import org.oppia.android.app.model.ComparableOperation.ComparisonTypeCase.CONSTANT_TERM +import org.oppia.android.app.model.ComparableOperation.ComparisonTypeCase.NON_COMMUTATIVE_OPERATION +import org.oppia.android.app.model.ComparableOperation.ComparisonTypeCase.VARIABLE_TERM +import org.oppia.android.app.model.ComparableOperation.NonCommutativeOperation +import org.oppia.android.app.model.ComparableOperation.NonCommutativeOperation.OperationTypeCase.EXPONENTIATION +import org.oppia.android.app.model.ComparableOperation.NonCommutativeOperation.OperationTypeCase.OPERATIONTYPE_NOT_SET +import org.oppia.android.app.model.ComparableOperation.NonCommutativeOperation.OperationTypeCase.SQUARE_ROOT + +// TODO: add tests. +/** + * Returns whether this [ComparableOperation] is approximately equal to another, that is, + * whether it exactly matches the other except for constants (which instead utilize + * [org.oppia.android.app.model.Real.isApproximatelyEqualTo]). + * + * This function assumes that both this [ComparableOperation] and [other] are sorted prior to + * equality checking. + */ +fun ComparableOperation.isApproximatelyEqualTo(other: ComparableOperation): Boolean { + return when { + isNegated != other.isNegated -> false + isInverted != other.isInverted -> false + comparisonTypeCase != other.comparisonTypeCase -> false + else -> when (comparisonTypeCase) { + COMMUTATIVE_ACCUMULATION -> + commutativeAccumulation.isApproximatelyEqualTo(other.commutativeAccumulation) + NON_COMMUTATIVE_OPERATION -> + nonCommutativeOperation.isApproximatelyEqualTo(other.nonCommutativeOperation) + CONSTANT_TERM -> constantTerm.isApproximatelyEqualTo(other.constantTerm) + VARIABLE_TERM -> variableTerm == other.variableTerm + COMPARISONTYPE_NOT_SET, null -> true + } + } +} + +private fun CommutativeAccumulation.isApproximatelyEqualTo( + other: CommutativeAccumulation +): Boolean { + if (accumulationType != other.accumulationType) return false + if (combinedOperationsCount != other.combinedOperationsCount) return false + return combinedOperationsList.zip(other.combinedOperationsList).all { (first, second) -> + first.isApproximatelyEqualTo(second) + } +} + +private fun NonCommutativeOperation.isApproximatelyEqualTo( + other: NonCommutativeOperation +): Boolean { + if (operationTypeCase != other.operationTypeCase) return false + return when (operationTypeCase) { + EXPONENTIATION -> { + exponentiation.leftOperand.isApproximatelyEqualTo(other.exponentiation.leftOperand) + && exponentiation.rightOperand.isApproximatelyEqualTo(other.exponentiation.rightOperand) + } + SQUARE_ROOT -> squareRoot.isApproximatelyEqualTo(other.squareRoot) + OPERATIONTYPE_NOT_SET, null -> true + } +} diff --git a/utility/src/main/java/org/oppia/android/util/math/ComparableOperationListExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/ComparableOperationListExtensions.kt deleted file mode 100644 index 32af4ff4acb..00000000000 --- a/utility/src/main/java/org/oppia/android/util/math/ComparableOperationListExtensions.kt +++ /dev/null @@ -1,52 +0,0 @@ -package org.oppia.android.util.math - -import org.oppia.android.app.model.ComparableOperationList -import org.oppia.android.app.model.ComparableOperationList.CommutativeAccumulation -import org.oppia.android.app.model.ComparableOperationList.ComparableOperation -import org.oppia.android.app.model.ComparableOperationList.NonCommutativeOperation - -/** - * Returns whether this [ComparableOperationList] is approximately equal to another, that is, - * whether it exactly matches the other except for constants (which instead utilize - * [Real.approximatelyEquals]). - */ -fun ComparableOperationList.approximatelyEquals(other: ComparableOperationList): Boolean { - return rootOperation.approximatelyEquals(other.rootOperation) -} - -private fun ComparableOperation.approximatelyEquals(other: ComparableOperation): Boolean { - if (isNegated != other.isNegated) return false - if (isInverted != other.isInverted) return false - if (comparisonTypeCase != other.comparisonTypeCase) return false - return when (comparisonTypeCase) { - ComparableOperation.ComparisonTypeCase.COMMUTATIVE_ACCUMULATION -> - commutativeAccumulation.approximatelyEquals(other.commutativeAccumulation) - ComparableOperation.ComparisonTypeCase.NON_COMMUTATIVE_OPERATION -> - nonCommutativeOperation.approximatelyEquals(other.nonCommutativeOperation) - ComparableOperation.ComparisonTypeCase.CONSTANT_TERM -> - constantTerm.approximatelyEquals(other.constantTerm) - ComparableOperation.ComparisonTypeCase.VARIABLE_TERM -> variableTerm == other.variableTerm - ComparableOperation.ComparisonTypeCase.COMPARISONTYPE_NOT_SET, null -> true - } -} - -private fun CommutativeAccumulation.approximatelyEquals(other: CommutativeAccumulation): Boolean { - if (accumulationType != other.accumulationType) return false - if (combinedOperationsCount != other.combinedOperationsCount) return false - return combinedOperationsList.zip(other.combinedOperationsList).all { (first, second) -> - first.approximatelyEquals(second) - } -} - -private fun NonCommutativeOperation.approximatelyEquals(other: NonCommutativeOperation): Boolean { - if (operationTypeCase != other.operationTypeCase) return false - return when (operationTypeCase) { - NonCommutativeOperation.OperationTypeCase.EXPONENTIATION -> { - exponentiation.leftOperand.approximatelyEquals(other.exponentiation.leftOperand) - && exponentiation.rightOperand.approximatelyEquals(other.exponentiation.rightOperand) - } - NonCommutativeOperation.OperationTypeCase.SQUARE_ROOT -> - squareRoot.approximatelyEquals(other.squareRoot) - NonCommutativeOperation.OperationTypeCase.OPERATIONTYPE_NOT_SET, null -> true - } -} diff --git a/utility/src/main/java/org/oppia/android/util/math/FloatExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/FloatExtensions.kt index 96d072a669c..0be3d2ee3c3 100644 --- a/utility/src/main/java/org/oppia/android/util/math/FloatExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/FloatExtensions.kt @@ -14,14 +14,14 @@ const val FLOAT_EQUALITY_INTERVAL = 1e-5 * Returns whether this float approximately equals another based on a consistent epsilon value * ([FLOAT_EQUALITY_INTERVAL]). */ -fun Float.approximatelyEquals(other: Float): Boolean { +fun Float.isApproximatelyEqualTo(other: Float): Boolean { return abs(this - other) < FLOAT_EQUALITY_INTERVAL } /** Returns whether this double approximately equals another based on a consistent epsilon value * ([FLOAT_EQUALITY_INTERVAL]). */ -fun Double.approximatelyEquals(other: Double): Boolean { +fun Double.isApproximatelyEqualTo(other: Double): Boolean { return abs(this - other) < FLOAT_EQUALITY_INTERVAL } diff --git a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt index 1c53036902b..14ff73f7bc2 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt @@ -54,31 +54,32 @@ fun MathExpression.toComparableOperation(): ComparableOperation = convertToCompa */ fun MathExpression.toPolynomial(): Polynomial? = reduceToPolynomial() +// TODO: add tests. /** * Returns whether this [MathExpression] approximately equals another, that is, that it fully * matches in its AST representation but all constants are compared using - * [Real.approximatelyEquals]. Further, this does not check parser markers when considering - * equivalence. + * [Real.isApproximatelyEqualTo]. Further, this does not check parser markers when considering + * equality. */ -fun MathExpression.approximatelyEquals(other: MathExpression): Boolean { +fun MathExpression.isApproximatelyEqualTo(other: MathExpression): Boolean { if (expressionTypeCase != other.expressionTypeCase) return false return when (expressionTypeCase) { - CONSTANT -> constant.approximatelyEquals(other.constant) + CONSTANT -> constant.isApproximatelyEqualTo(other.constant) VARIABLE -> variable == other.variable BINARY_OPERATION -> { binaryOperation.operator == other.binaryOperation.operator - && binaryOperation.leftOperand.approximatelyEquals(other.binaryOperation.leftOperand) - && binaryOperation.rightOperand.approximatelyEquals(other.binaryOperation.rightOperand) + && binaryOperation.leftOperand.isApproximatelyEqualTo(other.binaryOperation.leftOperand) + && binaryOperation.rightOperand.isApproximatelyEqualTo(other.binaryOperation.rightOperand) } UNARY_OPERATION -> { unaryOperation.operator == other.unaryOperation.operator - && unaryOperation.operand.approximatelyEquals(other.unaryOperation.operand) + && unaryOperation.operand.isApproximatelyEqualTo(other.unaryOperation.operand) } FUNCTION_CALL -> { functionCall.functionType == other.functionCall.functionType - && functionCall.argument.approximatelyEquals(other.functionCall.argument) + && functionCall.argument.isApproximatelyEqualTo(other.functionCall.argument) } - GROUP -> group.approximatelyEquals(other.group) + GROUP -> group.isApproximatelyEqualTo(other.group) EXPRESSIONTYPE_NOT_SET, null -> true } } diff --git a/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt b/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt index 20b44cee192..e85107864c8 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt @@ -1128,7 +1128,7 @@ class MathExpressionParser private constructor(private val parseContext: ParseCo binaryOperation.operator == DIVIDE && binaryOperation.rightOperand.expressionTypeCase == CONSTANT && binaryOperation.rightOperand.constant - .toDouble().absoluteValue.approximatelyEquals(0.0) + .toDouble().absoluteValue.isApproximatelyEqualTo(0.0) } ?: binaryOperation.leftOperand.findNextDivisionByZero() ?: binaryOperation.rightOperand.findNextDivisionByZero() } diff --git a/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt index 4f76b82de58..6c41d9cd16f 100644 --- a/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt @@ -18,20 +18,21 @@ private val POLYNOMIAL_TERM_COMPARATOR by lazy { createTermComparator() } /** Returns whether this polynomial is a constant-only polynomial (contains no variables). */ fun Polynomial.isConstant(): Boolean = termCount == 1 && getTerm(0).variableCount == 0 +// TODO: add tests. /** * Returns whether this [Polynomial] approximately equals an other, that is, that the polynomial has - * the exact same terms and approximately equal coefficients (see [Real.approximatelyEquals]). + * the exact same terms and approximately equal coefficients (see [Real.isApproximatelyEqualTo]). + * + * This function assumes that both this and the other [Polynomial] are sorted before checking for + * equality. */ -fun Polynomial.approximatelyEquals(other: Polynomial): Boolean { +fun Polynomial.isApproximatelyEqualTo(other: Polynomial): Boolean { if (termCount != other.termCount) return false // Terms can be zipped since they should be sorted prior to checking equivalence. - return termList.zip(other.termList).all { (first, second) -> first.approximatelyEquals(second) } -} - -private fun Term.approximatelyEquals(other: Term): Boolean { - // The variable lists can be exactly matched since they're sorted. - return coefficient.approximatelyEquals(other.coefficient) && variableList == other.variableList + return termList.zip(other.termList).all { (first, second) -> + first.isApproximatelyEqualTo(second) + } } /** @@ -296,6 +297,11 @@ private fun Polynomial.combineLikeTerms(): Polynomial { }.build().ensureAtLeastConstant() } +private fun Term.isApproximatelyEqualTo(other: Term): Boolean { + // The variable lists can be exactly matched since they're sorted. + return coefficient.isApproximatelyEqualTo(other.coefficient) && variableList == other.variableList +} + private fun Polynomial.pow(exp: Real): Polynomial? { val shouldBeInverted = exp.isNegative() val positivePower = if (shouldBeInverted) -exp else exp diff --git a/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt index 06c29559363..9c451c65d34 100644 --- a/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt @@ -73,23 +73,24 @@ fun Real.isNegative(): Boolean = when (realTypeCase) { REALTYPE_NOT_SET, null -> throw IllegalStateException("Invalid real: $this.") } +// TODO: add tests. /** * Returns whether this [Real] approximately equals another, that is, if they evaluate to - * approximately the same value (see [Double.approximatelyEquals]). + * approximately the same value (see [Double.isApproximatelyEqualTo]). */ -fun Real.approximatelyEquals(other: Real): Boolean { - return isApproximatelyEqualTo(other.toDouble()) +fun Real.isApproximatelyEqualTo(other: Real): Boolean { + return this@isApproximatelyEqualTo.isApproximatelyEqualTo(other.toDouble()) } /** * Returns whether this [Real] is approximately equal to the specified [Double] per - * [Double.approximatelyEquals]. + * [Double.isApproximatelyEqualTo]. */ fun Real.isApproximatelyEqualTo(value: Double): Boolean { - return toDouble().approximatelyEquals(value) + return toDouble().isApproximatelyEqualTo(value) } -/** Returns whether this [Real] is approximately zero per [Double.approximatelyEquals]. */ +/** Returns whether this [Real] is approximately zero per [Double.isApproximatelyEqualTo]. */ fun Real.isApproximatelyZero(): Boolean = isApproximatelyEqualTo(0.0) /** diff --git a/utility/src/test/java/org/oppia/android/util/math/FloatExtensionsTest.kt b/utility/src/test/java/org/oppia/android/util/math/FloatExtensionsTest.kt index 6e1896902e6..c1fdc74d655 100644 --- a/utility/src/test/java/org/oppia/android/util/math/FloatExtensionsTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/FloatExtensionsTest.kt @@ -18,7 +18,7 @@ class FloatExtensionsTest { val leftFloat = 0f val rightFloat = 0f - val result = leftFloat.approximatelyEquals(rightFloat) + val result = leftFloat.isApproximatelyEqualTo(rightFloat) assertThat(result).isTrue() } @@ -28,7 +28,7 @@ class FloatExtensionsTest { val leftFloat = 1.2f val rightFloat = 1.2f - val result = leftFloat.approximatelyEquals(rightFloat) + val result = leftFloat.isApproximatelyEqualTo(rightFloat) assertThat(result).isTrue() } @@ -38,7 +38,7 @@ class FloatExtensionsTest { val leftFloat = 1.2f val rightFloat = leftFloat + FLOAT_EQUALITY_INTERVAL.toFloat() / 10f - val result = leftFloat.approximatelyEquals(rightFloat) + val result = leftFloat.isApproximatelyEqualTo(rightFloat) // Verify that they are approximately equal, but not actually the same float. assertThat(result).isTrue() @@ -50,7 +50,7 @@ class FloatExtensionsTest { val leftFloat = 0f val rightFloat = 7.3f - val result = leftFloat.approximatelyEquals(rightFloat) + val result = leftFloat.isApproximatelyEqualTo(rightFloat) assertThat(result).isFalse() } @@ -60,7 +60,7 @@ class FloatExtensionsTest { val leftFloat = 1.2f val rightFloat = leftFloat + FLOAT_EQUALITY_INTERVAL.toFloat() * 2f - val result = leftFloat.approximatelyEquals(rightFloat) + val result = leftFloat.isApproximatelyEqualTo(rightFloat) assertThat(result).isFalse() } @@ -70,7 +70,7 @@ class FloatExtensionsTest { val leftFloat = 1.2f val rightFloat = 7.3f - val result = leftFloat.approximatelyEquals(rightFloat) + val result = leftFloat.isApproximatelyEqualTo(rightFloat) assertThat(result).isFalse() } @@ -80,7 +80,7 @@ class FloatExtensionsTest { val leftDouble = 0.0 val rightDouble = 0.0 - val result = leftDouble.approximatelyEquals(rightDouble) + val result = leftDouble.isApproximatelyEqualTo(rightDouble) assertThat(result).isTrue() } @@ -90,7 +90,7 @@ class FloatExtensionsTest { val leftDouble = 1.2 val rightDouble = 1.2 - val result = leftDouble.approximatelyEquals(rightDouble) + val result = leftDouble.isApproximatelyEqualTo(rightDouble) assertThat(result).isTrue() } @@ -100,7 +100,7 @@ class FloatExtensionsTest { val leftDouble = 1.2 val rightDouble = leftDouble + FLOAT_EQUALITY_INTERVAL / 10.0 - val result = leftDouble.approximatelyEquals(rightDouble) + val result = leftDouble.isApproximatelyEqualTo(rightDouble) // Verify that they are approximately equal, but not actually the same double. assertThat(result).isTrue() @@ -112,7 +112,7 @@ class FloatExtensionsTest { val leftDouble = 1.2 val rightDouble = leftDouble + FLOAT_EQUALITY_INTERVAL * 2 - val result = leftDouble.approximatelyEquals(rightDouble) + val result = leftDouble.isApproximatelyEqualTo(rightDouble) assertThat(result).isFalse() } @@ -122,7 +122,7 @@ class FloatExtensionsTest { val leftDouble = 1.2 val rightDouble = 7.3 - val result = leftDouble.approximatelyEquals(rightDouble) + val result = leftDouble.isApproximatelyEqualTo(rightDouble) assertThat(result).isFalse() } From 468b565377dfc25afa4fd6f8182a5f1690bd0a64 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 3 Feb 2022 20:56:39 -0800 Subject: [PATCH 095/162] Add extension tests. --- .../NumericExpressionInputModule.kt | 1 - .../rules/numericexpressioninput/BUILD.bazel | 99 +++ ...sEquivalentToRuleClassifierProviderTest.kt | 102 +++ ...esExactlyWithRuleClassifierProviderTest.kt | 115 +++ ...ManipulationsRuleClassifierProviderTest.kt | 104 +++ .../NumericExpressionInputModuleTest.kt | 106 +++ .../math/ComparableOperationExtensions.kt | 1 - .../util/math/MathExpressionExtensions.kt | 1 - .../android/util/math/PolynomialExtensions.kt | 33 +- .../oppia/android/util/math/RealExtensions.kt | 1 - .../org/oppia/android/util/math/BUILD.bazel | 22 +- .../math/ComparableOperationExtensionsTest.kt | 770 +++++++++++++++++ .../util/math/MathExpressionExtensionsTest.kt | 151 +++- .../util/math/PolynomialExtensionsTest.kt | 808 ++++++++++++++++++ .../android/util/math/RealExtensionsTest.kt | 288 +++++++ 15 files changed, 2576 insertions(+), 26 deletions(-) create mode 100644 domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/BUILD.bazel create mode 100644 domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProviderTest.kt create mode 100644 domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest.kt create mode 100644 domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt create mode 100644 domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputModuleTest.kt create mode 100644 utility/src/test/java/org/oppia/android/util/math/ComparableOperationExtensionsTest.kt diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputModule.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputModule.kt index 6a2cf9b50cb..ca42ea19de0 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputModule.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputModule.kt @@ -7,7 +7,6 @@ import dagger.multibindings.StringKey import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.domain.classify.rules.NumericExpressionInputRules -// TODO: add tests. /** Module that binds rule classifiers corresponding to the numeric expression input interaction. */ @Module class NumericExpressionInputModule { diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/BUILD.bazel b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/BUILD.bazel new file mode 100644 index 00000000000..a83a60b85cf --- /dev/null +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/BUILD.bazel @@ -0,0 +1,99 @@ +""" +Tests for numeric expression input classifiers. +""" + +load("@dagger//:workspace_defs.bzl", "dagger_rules") +load("//:oppia_android_test.bzl", "oppia_android_test") + +oppia_android_test( + name = "NumericExpressionInputIsEquivalentToRuleClassifierProviderTest", + srcs = ["NumericExpressionInputIsEquivalentToRuleClassifierProviderTest.kt"], + custom_package = "org.oppia.android.domain.classify.rules.numericexpressioninput", + test_class = "org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputIsEquivalentToRuleClassifierProviderTest", + test_manifest = "//domain:test_manifest", + deps = [ + ":dagger", + "//domain", + "//model/src/main/proto:interaction_object_java_proto_lite", + "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module", + "//testing/src/main/java/org/oppia/android/testing/threading:test_module", + "//testing/src/main/java/org/oppia/android/testing/time:test_module", + "//third_party:androidx_test_ext_junit", + "//third_party:com_google_truth_truth", + "//third_party:junit_junit", + "//third_party:org_robolectric_robolectric", + "//third_party:robolectric_android-all", + "//utility/src/main/java/org/oppia/android/util/locale:prod_module", + "//utility/src/main/java/org/oppia/android/util/logging:prod_module", + ], +) + +oppia_android_test( + name = "NumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest", + srcs = ["NumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest.kt"], + custom_package = "org.oppia.android.domain.classify.rules.numericexpressioninput", + test_class = "org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest", + test_manifest = "//domain:test_manifest", + deps = [ + ":dagger", + "//domain", + "//model/src/main/proto:interaction_object_java_proto_lite", + "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module", + "//testing/src/main/java/org/oppia/android/testing/threading:test_module", + "//testing/src/main/java/org/oppia/android/testing/time:test_module", + "//third_party:androidx_test_ext_junit", + "//third_party:com_google_truth_truth", + "//third_party:junit_junit", + "//third_party:org_robolectric_robolectric", + "//third_party:robolectric_android-all", + "//utility/src/main/java/org/oppia/android/util/locale:prod_module", + "//utility/src/main/java/org/oppia/android/util/logging:prod_module", + ], +) + +oppia_android_test( + name = "NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest", + srcs = ["NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt"], + custom_package = "org.oppia.android.domain.classify.rules.numericexpressioninput", + test_class = "org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest", + test_manifest = "//domain:test_manifest", + deps = [ + ":dagger", + "//domain", + "//model/src/main/proto:interaction_object_java_proto_lite", + "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module", + "//testing/src/main/java/org/oppia/android/testing/threading:test_module", + "//testing/src/main/java/org/oppia/android/testing/time:test_module", + "//third_party:androidx_test_ext_junit", + "//third_party:com_google_truth_truth", + "//third_party:junit_junit", + "//third_party:org_robolectric_robolectric", + "//third_party:robolectric_android-all", + "//utility/src/main/java/org/oppia/android/util/locale:prod_module", + "//utility/src/main/java/org/oppia/android/util/logging:prod_module", + ], +) + +oppia_android_test( + name = "NumericExpressionInputModuleTest", + srcs = ["NumericExpressionInputModuleTest.kt"], + custom_package = "org.oppia.android.domain.classify.rules.numericexpressioninput", + test_class = "org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModuleTest", + test_manifest = "//domain:test_manifest", + deps = [ + ":dagger", + "//domain", + "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module", + "//testing/src/main/java/org/oppia/android/testing/threading:test_module", + "//testing/src/main/java/org/oppia/android/testing/time:test_module", + "//third_party:androidx_test_ext_junit", + "//third_party:com_google_truth_truth", + "//third_party:junit_junit", + "//third_party:org_robolectric_robolectric", + "//third_party:robolectric_android-all", + "//utility/src/main/java/org/oppia/android/util/locale:prod_module", + "//utility/src/main/java/org/oppia/android/util/logging:prod_module", + ], +) + +dagger_rules() diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProviderTest.kt new file mode 100644 index 00000000000..4f71926b3d4 --- /dev/null +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProviderTest.kt @@ -0,0 +1,102 @@ +package org.oppia.android.domain.classify.rules.numericexpressioninput + +import android.app.Application +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import dagger.BindsInstance +import dagger.Component +import dagger.Module +import dagger.Provides +import javax.inject.Inject +import javax.inject.Singleton +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.oppia.android.app.model.InteractionObject +import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.RuleClassifier +import org.oppia.android.testing.robolectric.RobolectricModule +import org.oppia.android.testing.threading.TestDispatcherModule +import org.oppia.android.testing.time.FakeOppiaClockModule +import org.oppia.android.util.locale.LocaleProdModule +import org.oppia.android.util.logging.LoggerModule +import org.robolectric.annotation.Config +import org.robolectric.annotation.LooperMode + +/** Tests for [NumericExpressionInputIsEquivalentToRuleClassifierProvider]. */ +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") +@RunWith(AndroidJUnit4::class) +@LooperMode(LooperMode.Mode.PAUSED) +@Config(manifest = Config.NONE) +class NumericExpressionInputIsEquivalentToRuleClassifierProviderTest { + @Inject + internal lateinit var provider: NumericExpressionInputIsEquivalentToRuleClassifierProvider + + private lateinit var classifier: RuleClassifier + + @Before + fun setUp() { + setUpTestApplicationComponent() + classifier = provider.createRuleClassifier() + } + + // TODO: finish tests. + + @Test + fun test() { + val answerExpression = createMathExpression("0") + val inputExpression = createMathExpression("1") + + val matches = + classifier.matches( + answerExpression, + inputs = mapOf("x" to inputExpression), + writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + ) + + assertThat(matches).isTrue() + } + + private fun setUpTestApplicationComponent() { + DaggerNumericExpressionInputIsEquivalentToRuleClassifierProviderTest_TestApplicationComponent + .builder() + .setApplication(ApplicationProvider.getApplicationContext()).build().inject(this) + } + + private fun createMathExpression(rawExpression: String) = InteractionObject.newBuilder().apply { + mathExpression = rawExpression + }.build() + + // TODO(#89): Move this to a common test application component. + @Module + class TestModule { + @Provides + @Singleton + fun provideContext(application: Application): Context { + return application + } + } + + // TODO(#89): Move this to a common test application component. + @Singleton + @Component( + modules = [ + TestModule::class, LocaleProdModule::class, FakeOppiaClockModule::class, + TestDispatcherModule::class, LoggerModule::class, RobolectricModule::class + ] + ) + interface TestApplicationComponent { + @Component.Builder + interface Builder { + @BindsInstance + fun setApplication(application: Application): Builder + + fun build(): TestApplicationComponent + } + + fun inject(test: NumericExpressionInputIsEquivalentToRuleClassifierProviderTest) + } +} diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest.kt new file mode 100644 index 00000000000..f69864d89b6 --- /dev/null +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest.kt @@ -0,0 +1,115 @@ +package org.oppia.android.domain.classify.rules.numericexpressioninput + +import android.app.Application +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import dagger.BindsInstance +import dagger.Component +import dagger.Module +import dagger.Provides +import javax.inject.Inject +import javax.inject.Singleton +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.oppia.android.app.model.InteractionObject +import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.RuleClassifier +import org.oppia.android.testing.robolectric.RobolectricModule +import org.oppia.android.testing.threading.TestDispatcherModule +import org.oppia.android.testing.time.FakeOppiaClockModule +import org.oppia.android.util.locale.LocaleProdModule +import org.oppia.android.util.logging.LoggerModule +import org.robolectric.annotation.Config +import org.robolectric.annotation.LooperMode + +/** Tests for [NumericExpressionInputMatchesExactlyWithRuleClassifierProvider]. */ +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") +@RunWith(AndroidJUnit4::class) +@LooperMode(LooperMode.Mode.PAUSED) +@Config(manifest = Config.NONE) +class NumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest { + @Inject + internal lateinit var provider: NumericExpressionInputMatchesExactlyWithRuleClassifierProvider + + private lateinit var classifier: RuleClassifier + + @Before + fun setUp() { + setUpTestApplicationComponent() + classifier = provider.createRuleClassifier() + } + + // TODO: finish tests. + + // testMatches_zeroAnswer_zeroInput_returnsTrue + // testMatches_zeroAnswer_oneInput_returnsFalse + // testMatches_oneAnswer_zeroInput_returnsFalse + // testMatches_oneAnswer_oneInput_returnsTrue + // testMatches_answerAndInput_sameOperationParameters_returnsTrue + // testMatches_answerAndInput_same_returnsTrue + // testMatches_answerAndInput_differentByCommutativity_returnsFalse + // testMatches_answerAndInput_differentByAssociativity_returnsFalse + // testMatches_answerAndInput_differentByDistributivity_returnsFalse + // testMatches_answerAndInput_differentByNonCommutativeReordering_returnsFalse + // testMatches_answerAndInput_similarInMultipleWays_returnsTrue + // testMatches_answerAndInput_varyIncorrectlyInMultipleWays_returnsFalse + + @Test + fun test() { + val answerExpression = createMathExpression("0") + val inputExpression = createMathExpression("1") + + val matches = + classifier.matches( + answerExpression, + inputs = mapOf("x" to inputExpression), + writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + ) + + assertThat(matches).isTrue() + } + + private fun setUpTestApplicationComponent() { + DaggerNumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest_TestApplicationComponent + .builder() + .setApplication(ApplicationProvider.getApplicationContext()).build().inject(this) + } + + private fun createMathExpression(rawExpression: String) = InteractionObject.newBuilder().apply { + mathExpression = rawExpression + }.build() + + // TODO(#89): Move this to a common test application component. + @Module + class TestModule { + @Provides + @Singleton + fun provideContext(application: Application): Context { + return application + } + } + + // TODO(#89): Move this to a common test application component. + @Singleton + @Component( + modules = [ + TestModule::class, LocaleProdModule::class, FakeOppiaClockModule::class, + TestDispatcherModule::class, LoggerModule::class, RobolectricModule::class + ] + ) + interface TestApplicationComponent { + @Component.Builder + interface Builder { + @BindsInstance + fun setApplication(application: Application): Builder + + fun build(): TestApplicationComponent + } + + fun inject(test: NumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest) + } +} diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt new file mode 100644 index 00000000000..508a6243b80 --- /dev/null +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt @@ -0,0 +1,104 @@ +package org.oppia.android.domain.classify.rules.numericexpressioninput + +import android.app.Application +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import dagger.BindsInstance +import dagger.Component +import dagger.Module +import dagger.Provides +import javax.inject.Inject +import javax.inject.Singleton +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.oppia.android.app.model.InteractionObject +import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.RuleClassifier +import org.oppia.android.testing.robolectric.RobolectricModule +import org.oppia.android.testing.threading.TestDispatcherModule +import org.oppia.android.testing.time.FakeOppiaClockModule +import org.oppia.android.util.locale.LocaleProdModule +import org.oppia.android.util.logging.LoggerModule +import org.robolectric.annotation.Config +import org.robolectric.annotation.LooperMode + +/** Tests for [NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider]. */ +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") +@RunWith(AndroidJUnit4::class) +@LooperMode(LooperMode.Mode.PAUSED) +@Config(manifest = Config.NONE) +class NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest { + @Inject + internal lateinit var provider: NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider + + private lateinit var classifier: RuleClassifier + + @Before + fun setUp() { + setUpTestApplicationComponent() + classifier = provider.createRuleClassifier() + } + + // TODO: finish tests. + + @Test + fun test() { + val answerExpression = createMathExpression("0") + val inputExpression = createMathExpression("1") + + val matches = + classifier.matches( + answerExpression, + inputs = mapOf("x" to inputExpression), + writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + ) + + assertThat(matches).isTrue() + } + + private fun setUpTestApplicationComponent() { + DaggerNumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest_TestApplicationComponent + .builder() + .setApplication(ApplicationProvider.getApplicationContext()).build().inject(this) + } + + private fun createMathExpression(rawExpression: String) = InteractionObject.newBuilder().apply { + mathExpression = rawExpression + }.build() + + // TODO(#89): Move this to a common test application component. + @Module + class TestModule { + @Provides + @Singleton + fun provideContext(application: Application): Context { + return application + } + } + + // TODO(#89): Move this to a common test application component. + @Singleton + @Component( + modules = [ + TestModule::class, LocaleProdModule::class, FakeOppiaClockModule::class, + TestDispatcherModule::class, LoggerModule::class, RobolectricModule::class + ] + ) + interface TestApplicationComponent { + @Component.Builder + interface Builder { + @BindsInstance + fun setApplication(application: Application): Builder + + fun build(): TestApplicationComponent + } + + fun inject( + test: NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest + ) + } +} diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputModuleTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputModuleTest.kt new file mode 100644 index 00000000000..3ad0eed220e --- /dev/null +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputModuleTest.kt @@ -0,0 +1,106 @@ +package org.oppia.android.domain.classify.rules.numericexpressioninput + +import android.app.Application +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import dagger.BindsInstance +import dagger.Component +import dagger.Module +import dagger.Provides +import javax.inject.Inject +import javax.inject.Singleton +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.oppia.android.domain.classify.RuleClassifier +import org.oppia.android.domain.classify.rules.NumericExpressionInputRules +import org.oppia.android.testing.robolectric.RobolectricModule +import org.oppia.android.testing.threading.TestDispatcherModule +import org.oppia.android.testing.time.FakeOppiaClockModule +import org.oppia.android.util.locale.LocaleProdModule +import org.oppia.android.util.logging.LoggerModule +import org.robolectric.annotation.Config +import org.robolectric.annotation.LooperMode + +/** Tests for [NumericExpressionInputModule]. */ +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") +@RunWith(AndroidJUnit4::class) +@LooperMode(LooperMode.Mode.PAUSED) +@Config(manifest = Config.NONE) +class NumericExpressionInputModuleTest { + @Inject + @NumericExpressionInputRules + lateinit var numericExpressionInputClassifiers: Map + + @Before + fun setUp() { + setUpTestApplicationComponent() + } + + @Test + fun testModule_hasAtLeastOneClassifier() { + assertThat(numericExpressionInputClassifiers).isNotEmpty() + } + + @Test + fun testModule_hasNoDuplicateClassifiers() { + assertThat(numericExpressionInputClassifiers.values.toSet()).hasSize( + numericExpressionInputClassifiers.size + ) + } + + @Test + fun testModule_providesMatchesExactlyWithClassifier() { + assertThat(numericExpressionInputClassifiers).containsKey("MatchesExactlyWith") + } + + @Test + fun testModule_providesMatchesUpToTrivialManipulationsClassifier() { + assertThat(numericExpressionInputClassifiers).containsKey("MatchesUpToTrivialManipulations") + } + + @Test + fun testModule_providesIsEquivalentToClassifier() { + assertThat(numericExpressionInputClassifiers).containsKey("MatchesExactlyWith") + } + + private fun setUpTestApplicationComponent() { + DaggerNumericExpressionInputModuleTest_TestApplicationComponent + .builder() + .setApplication(ApplicationProvider.getApplicationContext()).build().inject(this) + } + + // TODO(#89): Move this to a common test application component. + @Module + class TestModule { + @Provides + @Singleton + fun provideContext(application: Application): Context { + return application + } + } + + // TODO(#89): Move this to a common test application component. + @Singleton + @Component( + modules = [ + TestModule::class, LocaleProdModule::class, FakeOppiaClockModule::class, + TestDispatcherModule::class, LoggerModule::class, RobolectricModule::class, + NumericExpressionInputModule::class + ] + ) + interface TestApplicationComponent { + @Component.Builder + interface Builder { + @BindsInstance + fun setApplication(application: Application): Builder + + fun build(): TestApplicationComponent + } + + fun inject(test: NumericExpressionInputModuleTest) + } +} diff --git a/utility/src/main/java/org/oppia/android/util/math/ComparableOperationExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/ComparableOperationExtensions.kt index f66b5aeb8c8..249bbea8394 100644 --- a/utility/src/main/java/org/oppia/android/util/math/ComparableOperationExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/ComparableOperationExtensions.kt @@ -12,7 +12,6 @@ import org.oppia.android.app.model.ComparableOperation.NonCommutativeOperation.O import org.oppia.android.app.model.ComparableOperation.NonCommutativeOperation.OperationTypeCase.OPERATIONTYPE_NOT_SET import org.oppia.android.app.model.ComparableOperation.NonCommutativeOperation.OperationTypeCase.SQUARE_ROOT -// TODO: add tests. /** * Returns whether this [ComparableOperation] is approximately equal to another, that is, * whether it exactly matches the other except for constants (which instead utilize diff --git a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt index 14ff73f7bc2..ab2cdd1d5c2 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt @@ -54,7 +54,6 @@ fun MathExpression.toComparableOperation(): ComparableOperation = convertToCompa */ fun MathExpression.toPolynomial(): Polynomial? = reduceToPolynomial() -// TODO: add tests. /** * Returns whether this [MathExpression] approximately equals another, that is, that it fully * matches in its AST representation but all constants are compared using diff --git a/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt index 6c41d9cd16f..b1d215cd436 100644 --- a/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/PolynomialExtensions.kt @@ -18,23 +18,6 @@ private val POLYNOMIAL_TERM_COMPARATOR by lazy { createTermComparator() } /** Returns whether this polynomial is a constant-only polynomial (contains no variables). */ fun Polynomial.isConstant(): Boolean = termCount == 1 && getTerm(0).variableCount == 0 -// TODO: add tests. -/** - * Returns whether this [Polynomial] approximately equals an other, that is, that the polynomial has - * the exact same terms and approximately equal coefficients (see [Real.isApproximatelyEqualTo]). - * - * This function assumes that both this and the other [Polynomial] are sorted before checking for - * equality. - */ -fun Polynomial.isApproximatelyEqualTo(other: Polynomial): Boolean { - if (termCount != other.termCount) return false - - // Terms can be zipped since they should be sorted prior to checking equivalence. - return termList.zip(other.termList).all { (first, second) -> - first.isApproximatelyEqualTo(second) - } -} - /** * Returns the first term coefficient from this polynomial. This corresponds to the whole value of * the polynomial iff isConstant() returns true, otherwise this value isn't useful. @@ -123,6 +106,22 @@ fun Polynomial.sort(): Polynomial = Polynomial.newBuilder().apply { ) }.build() +/** + * Returns whether this [Polynomial] approximately equals an other, that is, that the polynomial has + * the exact same terms and approximately equal coefficients (see [Real.isApproximatelyEqualTo]). + * + * This function assumes that both this and the other [Polynomial] are sorted before checking for + * equality (i.e. via [sort]). + */ +fun Polynomial.isApproximatelyEqualTo(other: Polynomial): Boolean { + if (termCount != other.termCount) return false + + // Terms can be zipped since they should be sorted prior to checking equivalence. + return termList.zip(other.termList).all { (first, second) -> + first.isApproximatelyEqualTo(second) + } +} + /** * Returns the negated version of this [Polynomial] such that the original polynomial plus the * negative version would yield zero. diff --git a/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt index 9c451c65d34..344a5f95273 100644 --- a/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/RealExtensions.kt @@ -73,7 +73,6 @@ fun Real.isNegative(): Boolean = when (realTypeCase) { REALTYPE_NOT_SET, null -> throw IllegalStateException("Invalid real: $this.") } -// TODO: add tests. /** * Returns whether this [Real] approximately equals another, that is, if they evaluate to * approximately the same value (see [Double.isApproximatelyEqualTo]). diff --git a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel index 7a7aed1e686..f3ebddf37d7 100644 --- a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel @@ -42,6 +42,24 @@ oppia_android_test( ], ) +oppia_android_test( + name = "ComparableOperationExtensionsTest", + srcs = ["ComparableOperationExtensionsTest.kt"], + custom_package = "org.oppia.android.util.math", + test_class = "org.oppia.android.util.math.ComparableOperationExtensionsTest", + test_manifest = "//utility:test_manifest", + deps = [ + "//model/src/main/proto:math_java_proto_lite", + "//third_party:androidx_test_ext_junit", + "//third_party:com_google_truth_truth", + "//third_party:junit_junit", + "//third_party:org_robolectric_robolectric", + "//third_party:robolectric_android-all", + "//utility/src/main/java/org/oppia/android/util/math:extensions", + "//utility/src/main/java/org/oppia/android/util/math:fraction_parser", + ], +) + oppia_android_test( name = "ComparatorExtensionsTest", srcs = ["ComparatorExtensionsTest.kt"], @@ -178,9 +196,11 @@ oppia_android_test( test_manifest = "//utility:test_manifest", deps = [ "//model/src/main/proto:math_java_proto_lite", + "//testing", + "//testing/src/main/java/org/oppia/android/testing/junit:oppia_parameterized_test_runner", + "//testing/src/main/java/org/oppia/android/testing/junit:parameterized_robolectric_test_runner", "//testing/src/main/java/org/oppia/android/testing/math:polynomial_subject", "//testing/src/main/java/org/oppia/android/testing/math:real_subject", - "//third_party:androidx_test_ext_junit", "//third_party:com_google_truth_truth", "//third_party:junit_junit", "//third_party:org_robolectric_robolectric", diff --git a/utility/src/test/java/org/oppia/android/util/math/ComparableOperationExtensionsTest.kt b/utility/src/test/java/org/oppia/android/util/math/ComparableOperationExtensionsTest.kt new file mode 100644 index 00000000000..0e277196fa3 --- /dev/null +++ b/utility/src/test/java/org/oppia/android/util/math/ComparableOperationExtensionsTest.kt @@ -0,0 +1,770 @@ +package org.oppia.android.util.math + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.oppia.android.app.model.ComparableOperation +import org.oppia.android.app.model.ComparableOperation.CommutativeAccumulation +import org.oppia.android.app.model.ComparableOperation.NonCommutativeOperation +import org.oppia.android.app.model.Real +import org.robolectric.annotation.LooperMode + +/** Tests for [ComparableOperation] extensions. */ +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") +@RunWith(AndroidJUnit4::class) +@LooperMode(LooperMode.Mode.PAUSED) +class ComparableOperationExtensionsTest { + private val fractionParser by lazy { FractionParser() } + + @Test + fun testIsApproximatelyEqualTo_firstIsDefault_secondIsDefault_returnTrue() { + val first = ComparableOperation.getDefaultInstance() + val second = ComparableOperation.getDefaultInstance() + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isTrue() + assertThat(result2).isTrue() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsDefault_secondIsConstantInt2_returnFalse() { + val first = ComparableOperation.getDefaultInstance() + val second = createConstantOp(constant = 2) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsConstantInt2_secondIsConstantInt2_bothOrders_returnTrue() { + val first = createConstantOp(constant = 2) + val second = createConstantOp(constant = 2) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isTrue() + assertThat(result2).isTrue() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsConstantInt2_secondIsConstantInt3_returnsFalse() { + val first = createConstantOp(constant = 2) + val second = createConstantOp(constant = 3) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsConstantInt2_secondIsConstantFraction2_returnsTrue() { + val first = createConstantOp(constant = 2) + val second = createConstantOp(constantFraction = "2") + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isTrue() + assertThat(result2).isTrue() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsConstInt2_secondIsConstFraction3Halves_returnsFalse() { + val first = createConstantOp(constant = 2) + val second = createConstantOp(constantFraction = "3/2") + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsConstantInt3_secondIsConstantFraction3Ones_returnsTrue() { + val first = createConstantOp(constant = 3) + val second = createConstantOp(constantFraction = "3/1") + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isTrue() + assertThat(result2).isTrue() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsConstantInt2_secondIsConstantFraction3_returnsFalse() { + val first = createConstantOp(constant = 2) + val second = createConstantOp(constantFraction = "3") + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsConstantInt2_secondIsConstantDouble2_returnsTrue() { + val first = createConstantOp(constant = 2) + val second = createConstantOp(constant = 2.0) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isTrue() + assertThat(result2).isTrue() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsConstInt2_secondIsConstDouble2PlusMargin_returnsTrue() { + val first = createConstantOp(constant = 2) + val second = createConstantOp(constant = 2.0000001) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isTrue() + assertThat(result2).isTrue() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsConstantInt3_secondIsConstantPi_returnsFalse() { + val first = createConstantOp(constant = 3) + val second = createConstantOp(constant = 3.14) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsDoubleOnePointFive_secondIsFracThreeHalves_returnsTrue() { + val first = createConstantOp(constant = 1.5) + val second = createConstantOp(constantFraction = "3/2") + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isTrue() + assertThat(result2).isTrue() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsVariableX_secondIsVariableX_returnsTrue() { + val first = createVariableOp(name = "x") + val second = createVariableOp(name = "x") + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isTrue() + assertThat(result2).isTrue() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsVariableX_secondIsVariableY_returnsFalse() { + val first = createVariableOp(name = "x") + val second = createVariableOp(name = "y") + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsNegatedInt2_secondIsNegatedInt2_returnsTrue() { + val first = createConstantOp(constant = 2).toNegated() + val second = createConstantOp(constant = 2).toNegated() + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isTrue() + assertThat(result2).isTrue() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsNegatedInt2_secondIsNotNegatedInt2_returnsFalse() { + val first = createConstantOp(constant = 2).toNegated() + val second = createConstantOp(constant = 2) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsInvertedInt2_secondIsInvertedInt2_returnsTrue() { + val first = createConstantOp(constant = 2).toInverted() + val second = createConstantOp(constant = 2).toInverted() + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isTrue() + assertThat(result2).isTrue() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsInvertedInt2_secondIsNotInvertedInt2_returnsFalse() { + val first = createConstantOp(constant = 2).toInverted() + val second = createConstantOp(constant = 2) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsConstantInt2_secondIsSumOfInt2And3_returnFalse() { + val first = createConstantOp(constant = 2) + val second = createSumOp( + createConstantOp(constant = 2), + createConstantOp(constant = 3) + ) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsVariableX_secondIsSumOfInt2And3_returnFalse() { + val first = createVariableOp(name = "x") + val second = createSumOp( + createConstantOp(constant = 2), + createConstantOp(constant = 3) + ) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsConstantInt2_secondIsProductOfInt2And3_returnFalse() { + val first = createConstantOp(constant = 2) + val second = createProductOp( + createConstantOp(constant = 2), + createConstantOp(constant = 3) + ) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsVariableX_secondIsProductOfInt2And3_returnFalse() { + val first = createVariableOp(name = "x") + val second = createProductOp( + createConstantOp(constant = 2), + createConstantOp(constant = 3) + ) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsSumOfInt2And3_secondIsSumOfInt2And3_returnsTrue() { + val first = createSumOp( + createConstantOp(constant = 2), + createConstantOp(constant = 3) + ) + val second = createSumOp( + createConstantOp(constant = 2), + createConstantOp(constant = 3) + ) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isTrue() + assertThat(result2).isTrue() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsSumOfInt2And3_secondIsSumOfInt3And2_returnsFalse() { + val first = createSumOp( + createConstantOp(constant = 2), + createConstantOp(constant = 3) + ) + val second = createSumOp( + createConstantOp(constant = 3), + createConstantOp(constant = 2) + ) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + // Order matters. + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsSumOfInt2And3_secondIsProductOfInt2And3_returnsFalse() { + val first = createSumOp( + createConstantOp(constant = 2), + createConstantOp(constant = 3) + ) + val second = createProductOp( + createConstantOp(constant = 2), + createConstantOp(constant = 3) + ) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + // The accumulation type must match. Since this check is symmetric, it's also verifying the case + // when the left-hand side is a product. + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsProductOfInt2And3_secondIsProductOfInt2And3_returnsTrue() { + val first = createProductOp( + createConstantOp(constant = 2), + createConstantOp(constant = 3) + ) + val second = createProductOp( + createConstantOp(constant = 2), + createConstantOp(constant = 3) + ) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isTrue() + assertThat(result2).isTrue() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsProductOfInt2And3_secondIsProductOfInt3And2_returnsFalse() { + val first = createProductOp( + createConstantOp(constant = 2), + createConstantOp(constant = 3) + ) + val second = createProductOp( + createConstantOp(constant = 3), + createConstantOp(constant = 2) + ) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + // Order matters. + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsConstantInt2_secondIsSquareRootOfInt2_returnsFalse() { + val first = createConstantOp(constant = 2) + val second = createSquareRootOp(arg = createConstantOp(constant = 2)) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsVariableX_secondIsSquareRootOfIntX_returnsFalse() { + val first = createVariableOp(name = "x") + val second = createSquareRootOp(arg = createVariableOp(name = "x")) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsSumOfInt2And3_secondIsSquareRootOfInt2_returnsFalse() { + val first = createSumOp( + createConstantOp(constant = 2), + createConstantOp(constant = 3) + ) + val second = createSquareRootOp(arg = createConstantOp(constant = 2)) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsProductOfInt2And3_secondIsSquareRootOfInt2_returnsFalse() { + val first = createProductOp( + createConstantOp(constant = 2), + createConstantOp(constant = 3) + ) + val second = createSquareRootOp(arg = createConstantOp(constant = 2)) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsConstantInt2_secondIsExpOfXAnd2_returnsFalse() { + val first = createConstantOp(constant = 2) + val second = createExpOp( + lhs = createVariableOp(name = "x"), + rhs = createConstantOp(constant = 2) + ) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsVariableX_secondIsExpOfXAnd2_returnsFalse() { + val first = createVariableOp(name = "x") + val second = createExpOp( + lhs = createVariableOp(name = "x"), + rhs = createConstantOp(constant = 2) + ) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsSumOfInt2And3_secondIsExpOfInt2And3_returnsFalse() { + val first = createSumOp( + createConstantOp(constant = 2), + createConstantOp(constant = 3) + ) + val second = createExpOp( + lhs = createConstantOp(constant = 2), + rhs = createConstantOp(constant = 3) + ) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsProductOfInt2And3_secondIsExpOfInt2And3_returnsFalse() { + val first = createProductOp( + createConstantOp(constant = 2), + createConstantOp(constant = 3) + ) + val second = createExpOp( + lhs = createConstantOp(constant = 2), + rhs = createConstantOp(constant = 3) + ) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsExpOfXAnd2_secondIsExpOfXAnd2_returnsTrue() { + val first = createExpOp( + lhs = createVariableOp(name = "x"), + rhs = createConstantOp(constant = 2) + ) + val second = createExpOp( + lhs = createVariableOp(name = "x"), + rhs = createConstantOp(constant = 2) + ) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isTrue() + assertThat(result2).isTrue() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsExpOfXAnd2_secondIsExpOfXAnd3_returnsFalse() { + val first = createExpOp( + lhs = createVariableOp(name = "x"), + rhs = createConstantOp(constant = 2) + ) + val second = createExpOp( + lhs = createVariableOp(name = "x"), + rhs = createConstantOp(constant = 3) + ) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsExpOfXAnd2_secondIsExpOfYAnd2_returnsFalse() { + val first = createExpOp( + lhs = createVariableOp(name = "x"), + rhs = createConstantOp(constant = 2) + ) + val second = createExpOp( + lhs = createVariableOp(name = "y"), + rhs = createConstantOp(constant = 2) + ) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsExpOfInt2AndThree_secondIsSquareRootOfInt2_returnsFalse() { + val first = createExpOp( + lhs = createConstantOp(constant = 2), + rhs = createConstantOp(constant = 3) + ) + val second = createSquareRootOp(arg = createConstantOp(constant = 2)) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsExpOfInt2AndOneHalf_secondIsSqRootOfInt2_returnsFalse() { + val first = createExpOp( + lhs = createConstantOp(constant = 2), + rhs = createConstantOp(constantFraction = "1/2") + ) + val second = createSquareRootOp(arg = createConstantOp(constant = 2)) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + // The two expressions are technically numerically equal, but they don't pass the equality check + // for comparable operations since exponentiation and square roots aren't simplified. + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsSquareRootOfInt2_secondIsSquareRootOfInt2_returnsTrue() { + val first = createSquareRootOp(arg = createConstantOp(constant = 2)) + val second = createSquareRootOp(arg = createConstantOp(constant = 2)) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isTrue() + assertThat(result2).isTrue() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsSquareRootOfInt2_secondIsSquareRootOfInt3_returnsFalse() { + val first = createSquareRootOp(arg = createConstantOp(constant = 2)) + val second = createSquareRootOp(arg = createConstantOp(constant = 3)) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_fullOperation_withNesting_allMatching_returnsTrue() { + val complexOp = createSumOp( + createProductOp( + createSumOp( + createVariableOp(name = "x"), + createConstantOp(constant = 3.14) + ), + createExpOp( + lhs = createConstantOp(constant = 3).toNegated(), + rhs = createSquareRootOp(arg = createConstantOp(3)) + ).toInverted() + ) + ) + + val result = complexOp.isApproximatelyEqualTo(complexOp) + + assertThat(result).isTrue() + } + + @Test + fun testIsApproximatelyEqualTo_fullOperation_withNesting_innerDifference_returnsFalse() { + val first = createSumOp( + createProductOp( + createSumOp( + createVariableOp(name = "x"), + createConstantOp(constant = 3.14) + ), + createExpOp( + lhs = createConstantOp(constant = 3).toNegated(), + rhs = createSquareRootOp(arg = createConstantOp(3)) + ).toInverted() + ) + ) + val second = createSumOp( + createProductOp( + createSumOp( + createVariableOp(name = "x"), + createConstantOp(constant = 3.14) + ), + createExpOp( + lhs = createConstantOp(constant = 2).toNegated(), + rhs = createSquareRootOp(arg = createConstantOp(3)) + ).toInverted() + ) + ) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_fullOperation_comparedToDefault_returnsFalse() { + val first = createSumOp( + createProductOp( + createSumOp( + createVariableOp(name = "x"), + createConstantOp(constant = 3.14) + ), + createExpOp( + lhs = createConstantOp(constant = 3).toNegated(), + rhs = createSquareRootOp(arg = createConstantOp(3)) + ).toInverted() + ) + ) + val second = ComparableOperation.getDefaultInstance() + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + private fun createConstantOp(constant: Int) = ComparableOperation.newBuilder().apply { + constantTerm = createIntegerReal(constant) + }.build() + + private fun createConstantOp(constantFraction: String) = ComparableOperation.newBuilder().apply { + constantTerm = createRationalReal(rawFractionExpression = constantFraction) + }.build() + + private fun createConstantOp(constant: Double) = ComparableOperation.newBuilder().apply { + constantTerm = createIrrationalReal(constant) + }.build() + + private fun createVariableOp(name: String) = ComparableOperation.newBuilder().apply { + variableTerm = name + }.build() + + private fun createSumOp( + vararg ops: ComparableOperation + ) = ComparableOperation.newBuilder().apply { + commutativeAccumulation = CommutativeAccumulation.newBuilder().apply { + accumulationType = CommutativeAccumulation.AccumulationType.SUMMATION + addAllCombinedOperations(ops.asIterable()) + }.build() + }.build() + + private fun createProductOp( + vararg ops: ComparableOperation + ) = ComparableOperation.newBuilder().apply { + commutativeAccumulation = CommutativeAccumulation.newBuilder().apply { + accumulationType = CommutativeAccumulation.AccumulationType.PRODUCT + addAllCombinedOperations(ops.asIterable()) + }.build() + }.build() + + private fun createSquareRootOp( + arg: ComparableOperation + ) = ComparableOperation.newBuilder().apply { + nonCommutativeOperation = NonCommutativeOperation.newBuilder().apply { + squareRoot = arg + }.build() + }.build() + + private fun createExpOp( + lhs: ComparableOperation, rhs: ComparableOperation + ) = ComparableOperation.newBuilder().apply { + nonCommutativeOperation = NonCommutativeOperation.newBuilder().apply { + exponentiation = NonCommutativeOperation.BinaryOperation.newBuilder().apply { + leftOperand = lhs + rightOperand = rhs + }.build() + }.build() + }.build() + + private fun ComparableOperation.toNegated() = toBuilder().apply { + isNegated = true + }.build() + + private fun ComparableOperation.toInverted() = toBuilder().apply { + isInverted = true + }.build() + + private fun createIntegerReal(value: Int) = Real.newBuilder().apply { + integer = value + }.build() + + private fun createRationalReal(rawFractionExpression: String) = Real.newBuilder().apply { + rational = fractionParser.parseFractionFromString(rawFractionExpression) + }.build() + + private fun createIrrationalReal(value: Double) = Real.newBuilder().apply { + irrational = value + }.build() +} diff --git a/utility/src/test/java/org/oppia/android/util/math/MathExpressionExtensionsTest.kt b/utility/src/test/java/org/oppia/android/util/math/MathExpressionExtensionsTest.kt index 7a52039d973..abc2888f94e 100644 --- a/utility/src/test/java/org/oppia/android/util/math/MathExpressionExtensionsTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/MathExpressionExtensionsTest.kt @@ -1,14 +1,21 @@ package org.oppia.android.util.math -import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat +import java.lang.IllegalStateException import org.junit.Test import org.junit.runner.RunWith import org.oppia.android.app.model.MathEquation import org.oppia.android.app.model.MathExpression +import org.oppia.android.testing.assertThrows +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Iteration +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Parameter +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.RunParameterized import org.oppia.android.testing.math.PolynomialSubject.Companion.assertThat import org.oppia.android.testing.math.RealSubject.Companion.assertThat +import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode.ALL_ERRORS +import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode.REQUIRED_ONLY import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult import org.oppia.android.util.math.MathExpressionParser.Companion.parseAlgebraicExpression import org.oppia.android.util.math.MathExpressionParser.Companion.parseNumericExpression @@ -27,9 +34,12 @@ import org.robolectric.annotation.LooperMode // FunctionName: test names are conventionally named with underscores. // SameParameterValue: tests should have specific context included/excluded for readability. @Suppress("FunctionName", "SameParameterValue") -@RunWith(AndroidJUnit4::class) +@RunWith(OppiaParameterizedTestRunner::class) @LooperMode(LooperMode.Mode.PAUSED) class MathExpressionExtensionsTest { + @Parameter lateinit var exp1: String + @Parameter lateinit var exp2: String + @Test fun testToRawLatex_algebraicExpression_divNotAsFraction_returnsLatexStringWithDivision() { val expression = parseAlgebraicExpression("(x^2+7x-y)/2") @@ -134,14 +144,147 @@ class MathExpressionExtensionsTest { } } + /* Equality checks. Note that these are symmetrical to reduce the number of needed test cases. */ + + @Test + fun testIsApproximatelyEqualTo_oneIsDefault_otherIsConstInt2_returnsFalse() { + val first = MathExpression.getDefaultInstance() + val second = parseNumericExpression("2") + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_oneIsConstInt2_otherIsDefault_returnsFalse() { + val first = parseNumericExpression("2") + val second = MathExpression.getDefaultInstance() + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + @RunParameterized( + Iteration("2==2", "exp1=2", "exp2=2"), + Iteration("2==2.000000001", "exp1=2", "exp2=2.000000001"), + Iteration("x+1==x+1", "exp1=x+1", "exp2=x+1"), + Iteration("x-1==x-1", "exp1=x-1", "exp2=x-1"), + Iteration("x*2==x*2", "exp1=x*2", "exp2=x*2"), + Iteration("x/2==x/2", "exp1=x/2", "exp2=x/2"), + Iteration("x^2==x^2", "exp1=x^2", "exp2=x^2"), + Iteration("-x==-x", "exp1=-x", "exp2=-x"), + Iteration("sqrt(x)==sqrt(x)", "exp1=sqrt(x)", "exp2=sqrt(x)") + ) + fun testIsApproximatelyEqualTo_bothAreSingleTermsOrOperations_andSame_returnsTrue() { + val first = parseAlgebraicExpression(exp1) + val second = parseAlgebraicExpression(exp2) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isTrue() + assertThat(result2).isTrue() + } + + @Test + @RunParameterized( + Iteration("2!=3", "exp1=2", "exp2=3"), + Iteration("2!=3/2", "exp1=2", "exp2=3/2"), + Iteration("2!=3.14", "exp1=2", "exp2=3.14"), + Iteration("x!=y", "exp1=x", "exp2=y"), + Iteration("x!=2", "exp1=x", "exp2=2"), + // The number of terms must match. + Iteration("1+x!=1", "exp1=1+x", "exp2=1"), + Iteration("1+x!=x", "exp1=1+x", "exp2=x"), + Iteration("1+1+x!=2+x", "exp1=1+1+x", "exp2=2+x"), + // Term order must match. + Iteration("1+x!=2+x", "exp1=1+x", "exp2=2+x"), + Iteration("1+x!=x+1", "exp1=1+x", "exp2=x+1"), + Iteration("1-x!=2-x", "exp1=1-x", "exp2=2-x"), + Iteration("1-x!=x-1", "exp1=1-x", "exp2=x-1"), + Iteration("2*x!=3*x", "exp1=2*x", "exp2=3*x"), + Iteration("2*x!=x*2", "exp1=2*x", "exp2=x*2"), + Iteration("x/2!=x/3", "exp1=x/2", "exp2=x/3"), + Iteration("x/2!=2/x", "exp1=x/2", "exp2=2/x"), + Iteration("x^2!=x^3", "exp1=x^2", "exp2=x^3"), + Iteration("x^2!=2^x", "exp1=x^2", "exp2=2^x"), + Iteration("x!=-2", "exp1=x", "exp2=-2"), + Iteration("x!=-x", "exp1=x", "exp2=-x"), + Iteration("sqrt(x)!=sqrt(2)", "exp1=sqrt(x)", "exp2=sqrt(2)"), + // These checks are numerically equivalent but fail due to the expression structure not + // matching. + Iteration("2==2/1", "exp1=2", "exp2=2/1"), + Iteration("1/3==0.33333333", "exp1=1/3", "exp2=0.33333333"), + Iteration("1.5==3/2", "exp1=1.5", "exp2=3/2") + ) + fun testIsApproximatelyEqualTo_bothAreSingleTermsOrOperations_butDifferent_returnsFalse() { + // Some expressions may attempt normally disallowed expressions (such as '2^x'). + val first = parseAlgebraicExpression(exp1, errorCheckingMode = REQUIRED_ONLY) + val second = parseAlgebraicExpression(exp2, errorCheckingMode = REQUIRED_ONLY) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_complexExpressionsWithNesting_allTermsMatch_returnsTrue() { + val expression = "x+2/x-(-7*8-9)+sqrt((x+2)+1)+3xy^2" + val first = parseAlgebraicExpression(expression) + val second = parseAlgebraicExpression(expression) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isTrue() + assertThat(result2).isTrue() + } + + @Test + fun testIsApproximatelyEqualTo_complexExpressionsWithNesting_oneDifferent_returnsFalse() { + // One difference in operations, but otherwise the same values. This equality check demonstrates + // that the check is inherently recursive & properly checks for nested expression equality. + val first = parseAlgebraicExpression("x+2/x-(-7*8-9)+sqrt((x+2)+1)+3xy^2") + val second = parseAlgebraicExpression("x+2/x-(-7*8-9)+sqrt((x+1+1)+1)+3xy^2") + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_complexExpressionsWithNesting_comparedWithDefault_returnsFalse() { + val first = parseAlgebraicExpression("x+2/x-(-7*8-9)+sqrt((x+2)+1)+3xy^2") + val second = MathExpression.getDefaultInstance() + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + private companion object { private fun parseNumericExpression(expression: String): MathExpression { return parseNumericExpression(expression, ALL_ERRORS).retrieveExpectedSuccessfulResult() } - private fun parseAlgebraicExpression(expression: String): MathExpression { + private fun parseAlgebraicExpression( + expression: String, errorCheckingMode: ErrorCheckingMode = ALL_ERRORS + ): MathExpression { return parseAlgebraicExpression( - expression, allowedVariables = listOf("x", "y", "z"), ALL_ERRORS + expression, allowedVariables = listOf("x", "y", "z"), errorCheckingMode ).retrieveExpectedSuccessfulResult() } diff --git a/utility/src/test/java/org/oppia/android/util/math/PolynomialExtensionsTest.kt b/utility/src/test/java/org/oppia/android/util/math/PolynomialExtensionsTest.kt index 36b1aaa5224..00da8684164 100644 --- a/utility/src/test/java/org/oppia/android/util/math/PolynomialExtensionsTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/PolynomialExtensionsTest.kt @@ -80,6 +80,14 @@ class PolynomialExtensionsTest { rational = THREE_FRACTION }.build() + private val ONE_POINT_FIVE_REAL = Real.newBuilder().apply { + irrational = 1.5 + }.build() + + private val TWO_DOUBLE_REAL = Real.newBuilder().apply { + irrational = 2.0 + }.build() + private val PI_REAL = Real.newBuilder().apply { irrational = 3.14 }.build() @@ -1250,6 +1258,806 @@ class PolynomialExtensionsTest { } } + /* Equality checks. Note that these are symmetrical to reduce the number of needed test cases. */ + + @Test + fun testIsApproximatelyEqualTo_firstIsDefault_secondIsDefault_returnsTrue() { + val first = Polynomial.getDefaultInstance() + val second = Polynomial.getDefaultInstance() + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isTrue() + assertThat(result2).isTrue() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsDefault_secondIsConstPolyOfInt2_returnsFalse() { + val first = Polynomial.getDefaultInstance() + val second = createPolynomial(createTerm(coefficient = TWO_REAL)) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsConstPolyOfInt2_secondIsDefault_returnsFalse() { + val first = createPolynomial(createTerm(coefficient = TWO_REAL)) + val second = Polynomial.getDefaultInstance() + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsPolyOfInt2_secondIsPolyOfInt2_returnsTrue() { + val first = createPolynomial(createTerm(coefficient = TWO_REAL)) + val second = createPolynomial(createTerm(coefficient = TWO_REAL)) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isTrue() + assertThat(result2).isTrue() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsPolyOfInt2_secondIsPolyOfInt3_returnsFalse() { + val first = createPolynomial(createTerm(coefficient = TWO_REAL)) + val second = createPolynomial(createTerm(coefficient = THREE_REAL)) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsPolyOfInt3_secondIsPolyOfFrac3_returnsTrue() { + val first = createPolynomial(createTerm(coefficient = THREE_REAL)) + val second = createPolynomial(createTerm(coefficient = THREE_FRACTION_REAL)) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + // These are equal since reals are fully evaluated for polynomials. + assertThat(result1).isTrue() + assertThat(result2).isTrue() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsPolyOfInt3_secondIsPolyOfFrac3Ones_returnsTrue() { + val first = createPolynomial(createTerm(coefficient = THREE_REAL)) + val second = createPolynomial(createTerm(coefficient = THREE_ONES_REAL)) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + // These are equal since reals are fully evaluated for polynomials. + assertThat(result1).isTrue() + assertThat(result2).isTrue() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsPolyOfInt3_secondIsPolyOfFracOneAndOneHalf_returnsFalse() { + val first = createPolynomial(createTerm(coefficient = THREE_REAL)) + val second = createPolynomial(createTerm(coefficient = ONE_AND_ONE_HALF_REAL)) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsPolyOfInt2_secondIsPolyOfDouble2_returnsTrue() { + val first = createPolynomial(createTerm(coefficient = TWO_REAL)) + val second = createPolynomial(createTerm(coefficient = TWO_DOUBLE_REAL)) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + // These are equal since reals are fully evaluated for polynomials. + assertThat(result1).isTrue() + assertThat(result2).isTrue() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsPolyOfInt2_secondIsPolyOfDouble2PlusMargin_returnsTrue() { + val first = createPolynomial(createTerm(coefficient = TWO_REAL)) + val second = createPolynomial( + createTerm(coefficient = Real.newBuilder().apply { + irrational = 2.00000001 + }.build()) + ) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + // These are equal since reals are fully evaluated with a margin check. + assertThat(result1).isTrue() + assertThat(result2).isTrue() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsPolyOfInt3_secondIsPolyOfDoublePi_returnsFalse() { + val first = createPolynomial(createTerm(coefficient = THREE_REAL)) + val second = createPolynomial(createTerm(coefficient = PI_REAL)) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsDoubleOnePointFive_secondIsFracOneAndOneHalf_returnsTrue() { + val first = createPolynomial(createTerm(coefficient = ONE_POINT_FIVE_REAL)) + val second = createPolynomial(createTerm(coefficient = ONE_AND_ONE_HALF_REAL)) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + // These are equal since reals are fully evaluated for polynomials. + assertThat(result1).isTrue() + assertThat(result2).isTrue() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsDoublePointThrees_secondIsFracOneThird_returnsTrue() { + val first = createPolynomial( + createTerm(coefficient = Real.newBuilder().apply { + irrational = 0.33333333333 + }.build()) + ) + val second = createPolynomial(createTerm(coefficient = ONE_THIRD_REAL)) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + // These are equal since reals are fully evaluated with a margin check. + assertThat(result1).isTrue() + assertThat(result2).isTrue() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsPolyOfVarX_secondIsPolyOfVarX_returnsTrue() { + val first = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)) + ) + val second = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)) + ) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isTrue() + assertThat(result2).isTrue() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsPolyOfVarX_secondIsPolyOfVarY_returnsFalse() { + val first = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)) + ) + val second = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "y", power = 1)) + ) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsPolyOfVarX_secondIsPolyOfInt2_returnsFalse() { + val first = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)) + ) + val second = createPolynomial(createTerm(coefficient = TWO_REAL)) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsPolyOfVarsXy_secondIsPolyOfVarX_returnsFalse() { + val first = createPolynomial( + createTerm( + coefficient = ONE, + createVariable(name = "x", power = 1), + createVariable(name = "y", power = 1) + ) + ) + val second = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)) + ) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + // A variable is missing. + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsPolyOfVarsXy_secondIsPolyOfVarY_returnsFalse() { + val first = createPolynomial( + createTerm( + coefficient = ONE, + createVariable(name = "x", power = 1), + createVariable(name = "y", power = 1) + ) + ) + val second = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "y", power = 1)) + ) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + // A variable is missing. + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsPolyOfVarsXy_secondIsPolyOfVarsXy_returnsTrue() { + val first = createPolynomial( + createTerm( + coefficient = ONE, + createVariable(name = "x", power = 1), + createVariable(name = "y", power = 1) + ) + ) + val second = createPolynomial( + createTerm( + coefficient = ONE, + createVariable(name = "x", power = 1), + createVariable(name = "y", power = 1) + ) + ) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isTrue() + assertThat(result2).isTrue() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsPolyOfVarsXy_secondIsPolyOfVarsYx_returnsFalse() { + val first = createPolynomial( + createTerm( + coefficient = ONE, + createVariable(name = "x", power = 1), + createVariable(name = "y", power = 1) + ) + ) + val second = createPolynomial( + createTerm( + coefficient = ONE, + createVariable(name = "y", power = 1), + createVariable(name = "x", power = 1) + ) + ) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + // Order matters (which is why the function recommends only comparing sorted polynomials). + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsXSquared_secondIsXSquared_returnsTrue() { + val first = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 2)) + ) + val second = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 2)) + ) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isTrue() + assertThat(result2).isTrue() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsXSquared_secondIsNegativeXSquared_returnsFalse() { + val first = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 2)) + ) + val second = createPolynomial( + createTerm(coefficient = -ONE, createVariable(name = "x", power = 2)) + ) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + // Coefficient sign differs. + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsXSquared_secondIsTwoXSquared_returnsFalse() { + val first = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 2)) + ) + val second = createPolynomial( + createTerm(coefficient = TWO_REAL, createVariable(name = "x", power = 2)) + ) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + // Coefficient value is different. + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsXSquared_secondIsX_returnsFalse() { + val first = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 2)) + ) + val second = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)) + ) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + // The powers don't match. + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsXSquared_secondIsXCubed_returnsFalse() { + val first = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 2)) + ) + val second = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 3)) + ) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + // The powers don't match. + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsXSquared_secondIsXSquaredY_returnsFalse() { + val first = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 2)) + ) + val second = createPolynomial( + createTerm( + coefficient = ONE, + createVariable(name = "x", power = 2), + createVariable(name = "y", power = 1) + ) + ) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + // There's an extra variable in one of the polynomials. + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsXSquaredY_secondIsXSquaredY_returnsTrue() { + val first = createPolynomial( + createTerm( + coefficient = ONE, + createVariable(name = "x", power = 2), + createVariable(name = "y", power = 1) + ) + ) + val second = createPolynomial( + createTerm( + coefficient = ONE, + createVariable(name = "x", power = 2), + createVariable(name = "y", power = 1) + ) + ) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isTrue() + assertThat(result2).isTrue() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsXSquaredY_secondIsXy_returnsFalse() { + val first = createPolynomial( + createTerm( + coefficient = ONE, + createVariable(name = "x", power = 2), + createVariable(name = "y", power = 1) + ) + ) + val second = createPolynomial( + createTerm( + coefficient = ONE, + createVariable(name = "x", power = 1), + createVariable(name = "y", power = 1) + ) + ) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + // x's power isn't correct. + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsXSquaredY_secondIsXYSquared_returnsFalse() { + val first = createPolynomial( + createTerm( + coefficient = ONE, + createVariable(name = "x", power = 2), + createVariable(name = "y", power = 1) + ) + ) + val second = createPolynomial( + createTerm( + coefficient = ONE, + createVariable(name = "x", power = 1), + createVariable(name = "y", power = 2) + ) + ) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + // The wrong variable is squared. + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsXSquaredY_secondIsXSquaredYSquared_returnsFalse() { + val first = createPolynomial( + createTerm( + coefficient = ONE, + createVariable(name = "x", power = 2), + createVariable(name = "y", power = 1) + ) + ) + val second = createPolynomial( + createTerm( + coefficient = ONE, + createVariable(name = "x", power = 2), + createVariable(name = "y", power = 2) + ) + ) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + // y is incorrectly also squared. + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsXSquaredY_secondIsYXSquared_returnsFalse() { + val first = createPolynomial( + createTerm( + coefficient = ONE, + createVariable(name = "x", power = 2), + createVariable(name = "y", power = 1) + ) + ) + val second = createPolynomial( + createTerm( + coefficient = ONE, + createVariable(name = "y", power = 1), + createVariable(name = "x", power = 2) + ) + ) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + // The terms are out of order. + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsXSquaredY_secondIsNegativeXSquaredY_returnsFalse() { + val first = createPolynomial( + createTerm( + coefficient = ONE, + createVariable(name = "x", power = 2), + createVariable(name = "y", power = 1) + ) + ) + val second = createPolynomial( + createTerm( + coefficient = -ONE, + createVariable(name = "x", power = 2), + createVariable(name = "y", power = 1) + ) + ) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + // The sign is incorrect on the second polynomial. + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsXSquaredY_secondIsTwoXSquaredY_returnsFalse() { + val first = createPolynomial( + createTerm( + coefficient = ONE, + createVariable(name = "x", power = 2), + createVariable(name = "y", power = 1) + ) + ) + val second = createPolynomial( + createTerm( + coefficient = TWO_REAL, + createVariable(name = "x", power = 2), + createVariable(name = "y", power = 1) + ) + ) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + // The coefficient is incorrect on the second polynomial. + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsXPlusY_secondIsXPlusY_returnsTrue() { + val first = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)), + createTerm(coefficient = ONE, createVariable(name = "y", power = 1)) + ) + val second = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)), + createTerm(coefficient = ONE, createVariable(name = "y", power = 1)) + ) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isTrue() + assertThat(result2).isTrue() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsXPlusY_secondIsXPlusYSquared_returnsFalse() { + val first = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)), + createTerm(coefficient = ONE, createVariable(name = "y", power = 1)) + ) + val second = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)), + createTerm(coefficient = ONE, createVariable(name = "y", power = 2)) + ) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + // The second polynomial's y power doesn't match. + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsXPlusY_secondIsXPlusFiveY_returnsFalse() { + val first = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)), + createTerm(coefficient = ONE, createVariable(name = "y", power = 1)) + ) + val second = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)), + createTerm(coefficient = FIVE_REAL, createVariable(name = "y", power = 1)) + ) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + // The second polynomial's y coefficient doesn't match. + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_firstIsXPlusY_secondIsNegativeXPlusY_returnsFalse() { + val first = createPolynomial( + createTerm(coefficient = ONE, createVariable(name = "x", power = 1)), + createTerm(coefficient = ONE, createVariable(name = "y", power = 1)) + ) + val second = createPolynomial( + createTerm(coefficient = -ONE, createVariable(name = "x", power = 1)), + createTerm(coefficient = ONE, createVariable(name = "y", power = 1)) + ) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + // The second polynomial's x coefficient negativity doesn't match. + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_complexMultiTermMultiVarPolys_allTermsSame_returnsTrue() { + val polynomial = createPolynomial( + createTerm(coefficient = -ONE_AND_ONE_HALF_REAL, createVariable(name = "x", power = 1)), + createTerm( + coefficient = FIVE_REAL, + createVariable(name = "x", power = 2), + createVariable(name = "y", power = 3), + createVariable(name = "z", power = 1) + ), + createTerm( + coefficient = -PI_REAL, + createVariable(name = "x", power = 2), + createVariable(name = "y", power = 1) + ), + createTerm(coefficient = ONE, createVariable(name = "z", power = 1)), + createTerm(coefficient = SEVEN_REAL) + ) + + val result = polynomial.isApproximatelyEqualTo(polynomial) + + assertThat(result).isTrue() + } + + @Test + fun testIsApproximatelyEqualTo_complexMultiTermMultiVarPolys_oneItemOutOfOrder_returnsFalse() { + val first = createPolynomial( + createTerm(coefficient = -ONE_AND_ONE_HALF_REAL, createVariable(name = "x", power = 1)), + createTerm( + coefficient = FIVE_REAL, + createVariable(name = "x", power = 2), + createVariable(name = "y", power = 3), + createVariable(name = "z", power = 1) + ), + createTerm( + coefficient = -PI_REAL, + createVariable(name = "x", power = 2), + createVariable(name = "y", power = 1) + ), + createTerm(coefficient = ONE, createVariable(name = "z", power = 1)), + createTerm(coefficient = SEVEN_REAL) + ) + val second = createPolynomial( + createTerm(coefficient = -ONE_AND_ONE_HALF_REAL, createVariable(name = "x", power = 1)), + createTerm( + coefficient = FIVE_REAL, + createVariable(name = "x", power = 2), + createVariable(name = "y", power = 3), + createVariable(name = "z", power = 1) + ), + createTerm( + coefficient = -PI_REAL, + createVariable(name = "y", power = 1), + createVariable(name = "x", power = 2) + ), + createTerm(coefficient = ONE, createVariable(name = "z", power = 1)), + createTerm(coefficient = SEVEN_REAL) + ) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + // One element is out of order in the second polynomial. + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_complexMultiTermMultiVarPolys_oneItemDifferent_returnsFalse() { + val first = createPolynomial( + createTerm(coefficient = -ONE_AND_ONE_HALF_REAL, createVariable(name = "x", power = 1)), + createTerm( + coefficient = FIVE_REAL, + createVariable(name = "x", power = 2), + createVariable(name = "y", power = 3), + createVariable(name = "z", power = 1) + ), + createTerm( + coefficient = -PI_REAL, + createVariable(name = "x", power = 2), + createVariable(name = "y", power = 1) + ), + createTerm(coefficient = ONE, createVariable(name = "z", power = 1)), + createTerm(coefficient = SEVEN_REAL) + ) + val second = createPolynomial( + createTerm(coefficient = -ONE_AND_ONE_HALF_REAL, createVariable(name = "x", power = 1)), + createTerm( + coefficient = FIVE_REAL - ONE, + createVariable(name = "x", power = 2), + createVariable(name = "y", power = 3), + createVariable(name = "z", power = 1) + ), + createTerm( + coefficient = -PI_REAL, + createVariable(name = "x", power = 2), + createVariable(name = "y", power = 1) + ), + createTerm(coefficient = ONE, createVariable(name = "z", power = 1)), + createTerm(coefficient = SEVEN_REAL) + ) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + // One coefficient is different in the second polynomial. + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + fun testIsApproximatelyEqualTo_complexMultiTermMultiVarPolys_compareToDefault_returnsFalse() { + val first = createPolynomial( + createTerm(coefficient = -ONE_AND_ONE_HALF_REAL, createVariable(name = "x", power = 1)), + createTerm( + coefficient = FIVE_REAL, + createVariable(name = "x", power = 2), + createVariable(name = "y", power = 3), + createVariable(name = "z", power = 1) + ), + createTerm( + coefficient = -PI_REAL, + createVariable(name = "x", power = 2), + createVariable(name = "y", power = 1) + ), + createTerm(coefficient = ONE, createVariable(name = "z", power = 1)), + createTerm(coefficient = SEVEN_REAL) + ) + val second = Polynomial.getDefaultInstance() + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + /* Operator tests. */ @Test diff --git a/utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt b/utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt index 842bd80f18c..2f8c7cd7bd9 100644 --- a/utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt @@ -296,6 +296,294 @@ class RealExtensionsTest { assertThat(result).isTrue() } + /* + * Approximate equality checks between reals. Note that all of these tests are symmetrical to + * reduce the number of test cases. + */ + + @Test + fun testIsApproximatelyEqualTo_firstIsDefault_secondIsInt2_throwsException() { + val first = Real.getDefaultInstance() + val second = TWO_REAL + + val exception = assertThrows(IllegalStateException::class) { + first.isApproximatelyEqualTo(second) + } + + assertThat(exception).hasMessageThat().contains("Invalid real") + } + + @Test + fun testIsApproximatelyEqualTo_firstIsInt2_secondIsDefault_throwsException() { + val first = TWO_REAL + val second = Real.getDefaultInstance() + + val exception = assertThrows(IllegalStateException::class) { + first.isApproximatelyEqualTo(second) + } + + assertThat(exception).hasMessageThat().contains("Invalid real") + } + + @Test + @RunParameterized( + Iteration("0==0", "lhsInt=0", "rhsInt=0"), + Iteration("1==1", "lhsInt=1", "rhsInt=1"), + Iteration("2==2", "lhsInt=2", "rhsInt=2"), + Iteration("-2==-2", "lhsInt=-2", "rhsInt=-2") + ) + fun testIsApproximatelyEqualTo_oneIsInt_otherIsSameInt_returnsTrue() { + val first = createIntegerReal(lhsInt) + val second = createIntegerReal(rhsInt) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + // Verify both correctness and the symmetric of equality. + assertThat(result1).isTrue() + assertThat(result2).isTrue() + } + + @Test + @RunParameterized( + Iteration("0!=1", "lhsInt=0", "rhsInt=1"), + Iteration("0!=2", "lhsInt=0", "rhsInt=2"), + Iteration("-2!=2", "lhsInt=-2", "rhsInt=2"), + Iteration("-2!=-1", "lhsInt=-2", "rhsInt=-1") + ) + fun testIsApproximatelyEqualTo_oneIsInt_otherIsDifferentInt_returnsFalse() { + val first = createIntegerReal(lhsInt) + val second = createIntegerReal(rhsInt) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = first.isApproximatelyEqualTo(second) + + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + @RunParameterized( + Iteration("0==0", "lhsInt=0", "rhsFrac=0"), + Iteration("2==2", "lhsInt=2", "rhsFrac=2"), + Iteration("2==2/1", "lhsInt=2", "rhsFrac=2/1"), + Iteration("2==4/2", "lhsInt=2", "rhsFrac=4/2"), + Iteration("-2==-2", "lhsInt=-2", "rhsFrac=-2"), + Iteration("-2==-2/1", "lhsInt=-2", "rhsFrac=-2/1"), + Iteration("-2==-4/2", "lhsInt=-2", "rhsFrac=-4/2") + ) + fun testIsApproximatelyEqualTo_oneIsInt_otherIsSameFraction_returnsTrue() { + val first = createIntegerReal(lhsInt) + val second = createRationalReal(rhsFrac) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + // Verify both correctness and the symmetric of equality. + assertThat(result1).isTrue() + assertThat(result2).isTrue() + } + + @Test + @RunParameterized( + Iteration("0!=2", "lhsInt=0", "rhsFrac=2"), + Iteration("2!=4", "lhsInt=2", "rhsFrac=4"), + Iteration("2!=3/2", "lhsInt=2", "rhsFrac=3/2"), + Iteration("2!=-2", "lhsInt=2", "rhsFrac=-2"), + ) + fun testIsApproximatelyEqualTo_oneIsInt_otherIsDifferentFraction_returnsFalse() { + val first = createIntegerReal(lhsInt) + val second = createRationalReal(rhsFrac) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = first.isApproximatelyEqualTo(second) + + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + @RunParameterized( + Iteration("0==0.0", "lhsInt=0", "rhsDouble=0.0"), + Iteration("1==1.0", "lhsInt=1", "rhsDouble=1.0"), + Iteration("2==2.0", "lhsInt=2", "rhsDouble=2.0"), + Iteration("2==2.0000001", "lhsInt=2", "rhsDouble=2.0000001"), + Iteration("2==1.9999999", "lhsInt=2", "rhsDouble=1.9999999"), + Iteration("-2==-2.0", "lhsInt=-2", "rhsDouble=-2.0"), + Iteration("-2==-2.0000001", "lhsInt=-2", "rhsDouble=-2.0000001"), + Iteration("-2==-1.9999999", "lhsInt=-2", "rhsDouble=-1.9999999") + ) + fun testIsApproximatelyEqualTo_oneIsInt_otherIsSimilarDouble_returnsTrue() { + val first = createIntegerReal(lhsInt) + val second = createIrrationalReal(rhsDouble) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + // Verify both correctness and the symmetric of equality. + assertThat(result1).isTrue() + assertThat(result2).isTrue() + } + + @Test + @RunParameterized( + Iteration("0!=2.0", "lhsInt=0", "rhsDouble=2.0"), + Iteration("2!=0.0", "lhsInt=2", "rhsDouble=0.0"), + Iteration("2!=4.0", "lhsInt=2", "rhsDouble=4.0"), + Iteration("3!=3.14", "lhsInt=3", "rhsDouble=3.14"), + Iteration("2!=2.001", "lhsInt=2", "rhsDouble=2.001"), + Iteration("2!=1.999", "lhsInt=2", "rhsDouble=1.999"), + Iteration("2!=-2.0", "lhsInt=2", "rhsDouble=-2.0"), + Iteration("-2!=2.0", "lhsInt=-2", "rhsDouble=2.0"), + Iteration("-2!=-2.001", "lhsInt=-2", "rhsDouble=-2.001"), + Iteration("-2!=-1.999", "lhsInt=-2", "rhsDouble=-1.999") + ) + fun testIsApproximatelyEqualTo_oneIsInt_otherIsDifferentDouble_returnsFalse() { + val first = createIntegerReal(lhsInt) + val second = createIrrationalReal(rhsDouble) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = first.isApproximatelyEqualTo(second) + + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + @RunParameterized( + Iteration("0==0", "lhsFrac=0", "rhsFrac=0"), + Iteration("2==2", "lhsFrac=2", "rhsFrac=2"), + Iteration("2==4/2", "lhsFrac=2", "rhsFrac=4/2"), + Iteration("3/2==1 1/2", "lhsFrac=3/2", "rhsFrac=1 1/2"), + Iteration("-2==-2", "lhsFrac=-2", "rhsFrac=-2"), + Iteration("-2==-4/2", "lhsFrac=-2", "rhsFrac=-4/2"), + Iteration("-3/2==-1 1/2", "lhsFrac=-3/2", "rhsFrac=-1 1/2"), + Iteration("1/3==3/9", "lhsFrac=1/3", "rhsFrac=3/9") + ) + fun testIsApproximatelyEqualTo_oneIsFraction_otherIsSameFraction_returnsTrue() { + val first = createRationalReal(lhsFrac) + val second = createRationalReal(rhsFrac) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + // Verify both correctness and the symmetric of equality. + assertThat(result1).isTrue() + assertThat(result2).isTrue() + } + + @Test + @RunParameterized( + Iteration("0!=2", "lhsFrac=0", "rhsFrac=2"), + Iteration("3/2!=1/2", "lhsFrac=3/2", "rhsFrac=1/2"), + Iteration("3/2!=1", "lhsFrac=3/2", "rhsFrac=1"), + Iteration("3/2!=-1 1/2", "lhsFrac=3/2", "rhsFrac=-1 1/2"), + Iteration("-3/2!=1 1/2", "lhsFrac=-3/2", "rhsFrac=1 1/2"), + Iteration("-3/2!=-1/2", "lhsFrac=-3/2", "rhsFrac=-1/2"), + Iteration("1/3!=2/3", "lhsFrac=1/3", "rhsFrac=2/3") + ) + fun testIsApproximatelyEqualTo_oneIsFraction_otherIsDifferentFraction_returnsFalse() { + val first = createRationalReal(lhsFrac) + val second = createRationalReal(rhsFrac) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = first.isApproximatelyEqualTo(second) + + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + @RunParameterized( + Iteration("0==0.0", "lhsFrac=0", "rhsDouble=0.0"), + Iteration("2==2.0", "lhsFrac=2", "rhsDouble=2.0"), + Iteration("2/1==2.0", "lhsFrac=2/1", "rhsDouble=2.0"), + Iteration("3/2==1.5", "lhsFrac=3/2", "rhsDouble=1.5"), + Iteration("1/3==0.33333333333", "lhsFrac=1/3", "rhsDouble=0.33333333333"), + Iteration("1 2/3==1.66666666666", "lhsFrac=1 2/3", "rhsDouble=1.66666666666"), + Iteration("-2==-2.0", "lhsFrac=-2", "rhsDouble=-2.0"), + Iteration("-3/2==-1.5", "lhsFrac=-3/2", "rhsDouble=-1.5") + ) + fun testIsApproximatelyEqualTo_oneIsFraction_otherIsSimilarDouble_returnsTrue() { + val first = createRationalReal(lhsFrac) + val second = createIrrationalReal(rhsDouble) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + // Verify both correctness and the symmetric of equality. + assertThat(result1).isTrue() + assertThat(result2).isTrue() + } + + @Test + @RunParameterized( + Iteration("0!=2.0", "lhsFrac=0", "rhsDouble=2.0"), + Iteration("2!=0.0", "lhsFrac=2", "rhsDouble=0.0"), + Iteration("2/2!=2.0", "lhsFrac=2/2", "rhsDouble=2.0"), + Iteration("1/3!=0.333", "lhsFrac=1/3", "rhsDouble=0.333"), + Iteration("1 2/3!=1.667", "lhsFrac=1 2/3", "rhsDouble=1.667"), + Iteration("22/7!=3.14", "lhsFrac=22/7", "rhsDouble=3.14"), + Iteration("-2!=2.0", "lhsFrac=-2", "rhsDouble=2.0"), + Iteration("2!=-2.0", "lhsFrac=2", "rhsDouble=-2.0"), + Iteration("-2/2!=-2.0", "lhsFrac=-2/2", "rhsDouble=-2.0"), + Iteration("-1/3!=-0.333", "lhsFrac=-1/3", "rhsDouble=-0.333"), + Iteration("-1 2/3!=-1.667", "lhsFrac=-1 2/3", "rhsDouble=-1.667") + ) + fun testIsApproximatelyEqualTo_oneIsFraction_firstIsDifferentDouble_returnsFalse() { + val first = createRationalReal(lhsFrac) + val second = createIrrationalReal(rhsDouble) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = first.isApproximatelyEqualTo(second) + + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + + @Test + @RunParameterized( + Iteration("0.0==0.0", "lhsDouble=0.0", "rhsDouble=0.0"), + Iteration("2.0==2.0", "lhsDouble=2.0", "rhsDouble=2.0"), + Iteration("2.0000000001==1.9999999999", "lhsDouble=2.0000000001", "rhsDouble=1.9999999999"), + Iteration("3.14==3.14", "lhsDouble=3.14", "rhsDouble=3.14"), + Iteration("-2.0==-2.0", "lhsDouble=-2.0", "rhsDouble=-2.0"), + Iteration("-2.0000000001==-1.9999999999", "lhsDouble=-2.0000000001", "rhsDouble=-1.9999999999"), + Iteration("-3.14==-3.14", "lhsDouble=-3.14", "rhsDouble=-3.14") + ) + fun testIsApproximatelyEqualTo_oneIsDouble_otherIsSimilarDouble_returnsTrue() { + val first = createIrrationalReal(lhsDouble) + val second = createIrrationalReal(rhsDouble) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = second.isApproximatelyEqualTo(first) + + // Verify both correctness and the symmetric of equality. + assertThat(result1).isTrue() + assertThat(result2).isTrue() + } + + @Test + @RunParameterized( + Iteration("0.0!=2.0", "lhsDouble=0.0", "rhsDouble=2.0"), + Iteration("2.001!=1.999", "lhsDouble=2.001", "rhsDouble=1.999"), + Iteration("2.7!=3.14", "lhsDouble=2.7", "rhsDouble=3.14"), + Iteration("2.7!=-3.14", "lhsDouble=2.7", "rhsDouble=-3.14"), + Iteration("-2.7!=3.14", "lhsDouble=-2.7", "rhsDouble=3.14"), + Iteration("-2.0!=2.0", "lhsDouble=-2.0", "rhsDouble=2.0"), + Iteration("-3.14!=3.14", "lhsDouble=-3.14", "rhsDouble=3.14") + ) + fun testIsApproximatelyEqualTo_oneIsDouble_otherIsDifferentDouble_returnsFalse() { + val first = createIrrationalReal(lhsDouble) + val second = createIrrationalReal(rhsDouble) + + val result1 = first.isApproximatelyEqualTo(second) + val result2 = first.isApproximatelyEqualTo(second) + + assertThat(result1).isFalse() + assertThat(result2).isFalse() + } + @Test fun testIsApproximatelyEqualTo_zeroAndOne_returnsFalse() { val result = ZERO_REAL.isApproximatelyEqualTo(1.0) From 4030aa34d8f73d3f027d47839c42a2269cc180c8 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 4 Feb 2022 15:10:03 -0800 Subject: [PATCH 096/162] Add classifier tests. --- ...putIsEquivalentToRuleClassifierProvider.kt | 1 - ...atchesExactlyWithRuleClassifierProvider.kt | 1 - ...vialManipulationsRuleClassifierProvider.kt | 1 - .../rules/numericexpressioninput/BUILD.bazel | 12 +- ...sEquivalentToRuleClassifierProviderTest.kt | 318 ++++++++++++++++- ...esExactlyWithRuleClassifierProviderTest.kt | 323 +++++++++++++++-- ...ManipulationsRuleClassifierProviderTest.kt | 332 +++++++++++++++++- 7 files changed, 921 insertions(+), 67 deletions(-) diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProvider.kt index 9e533395999..5d041e3e2ce 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProvider.kt @@ -13,7 +13,6 @@ import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingRes import org.oppia.android.util.math.evaluateAsNumericExpression import org.oppia.android.util.math.isApproximatelyEqualTo -// TODO: add tests. class NumericExpressionInputIsEquivalentToRuleClassifierProvider @Inject constructor( private val classifierFactory: GenericRuleClassifier.Factory, private val consoleLogger: ConsoleLogger diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProvider.kt index da2c5ce46c8..a62e75d9acb 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProvider.kt @@ -12,7 +12,6 @@ import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingRes import javax.inject.Inject import org.oppia.android.util.math.isApproximatelyEqualTo -// TODO: add tests. class NumericExpressionInputMatchesExactlyWithRuleClassifierProvider @Inject constructor( private val classifierFactory: GenericRuleClassifier.Factory, private val consoleLogger: ConsoleLogger diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt index 34c94f3f489..b4f4b074099 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt @@ -13,7 +13,6 @@ import org.oppia.android.util.math.toComparableOperation import javax.inject.Inject import org.oppia.android.util.math.isApproximatelyEqualTo -// TODO: add tests. class NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider @Inject constructor( private val classifierFactory: GenericRuleClassifier.Factory, diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/BUILD.bazel b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/BUILD.bazel index a83a60b85cf..f757f37b29e 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/BUILD.bazel +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/BUILD.bazel @@ -15,10 +15,12 @@ oppia_android_test( ":dagger", "//domain", "//model/src/main/proto:interaction_object_java_proto_lite", + "//testing/src/main/java/org/oppia/android/testing/junit:oppia_parameterized_test_runner", + "//testing/src/main/java/org/oppia/android/testing/junit:parameterized_robolectric_test_runner", "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module", "//testing/src/main/java/org/oppia/android/testing/threading:test_module", "//testing/src/main/java/org/oppia/android/testing/time:test_module", - "//third_party:androidx_test_ext_junit", + "//third_party:androidx_test_core", "//third_party:com_google_truth_truth", "//third_party:junit_junit", "//third_party:org_robolectric_robolectric", @@ -38,10 +40,12 @@ oppia_android_test( ":dagger", "//domain", "//model/src/main/proto:interaction_object_java_proto_lite", + "//testing/src/main/java/org/oppia/android/testing/junit:oppia_parameterized_test_runner", + "//testing/src/main/java/org/oppia/android/testing/junit:parameterized_robolectric_test_runner", "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module", "//testing/src/main/java/org/oppia/android/testing/threading:test_module", "//testing/src/main/java/org/oppia/android/testing/time:test_module", - "//third_party:androidx_test_ext_junit", + "//third_party:androidx_test_core", "//third_party:com_google_truth_truth", "//third_party:junit_junit", "//third_party:org_robolectric_robolectric", @@ -61,10 +65,12 @@ oppia_android_test( ":dagger", "//domain", "//model/src/main/proto:interaction_object_java_proto_lite", + "//testing/src/main/java/org/oppia/android/testing/junit:oppia_parameterized_test_runner", + "//testing/src/main/java/org/oppia/android/testing/junit:parameterized_robolectric_test_runner", "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module", "//testing/src/main/java/org/oppia/android/testing/threading:test_module", "//testing/src/main/java/org/oppia/android/testing/time:test_module", - "//third_party:androidx_test_ext_junit", + "//third_party:androidx_test_core", "//third_party:com_google_truth_truth", "//third_party:junit_junit", "//third_party:org_robolectric_robolectric", diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProviderTest.kt index 4f71926b3d4..3f9b00babce 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProviderTest.kt @@ -3,7 +3,6 @@ package org.oppia.android.domain.classify.rules.numericexpressioninput import android.app.Application import android.content.Context import androidx.test.core.app.ApplicationProvider -import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat import dagger.BindsInstance import dagger.Component @@ -17,6 +16,10 @@ import org.junit.runner.RunWith import org.oppia.android.app.model.InteractionObject import org.oppia.android.app.model.WrittenTranslationContext import org.oppia.android.domain.classify.RuleClassifier +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Iteration +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Parameter +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.RunParameterized import org.oppia.android.testing.robolectric.RobolectricModule import org.oppia.android.testing.threading.TestDispatcherModule import org.oppia.android.testing.time.FakeOppiaClockModule @@ -28,13 +31,18 @@ import org.robolectric.annotation.LooperMode /** Tests for [NumericExpressionInputIsEquivalentToRuleClassifierProvider]. */ // FunctionName: test names are conventionally named with underscores. @Suppress("FunctionName") -@RunWith(AndroidJUnit4::class) +@RunWith(OppiaParameterizedTestRunner::class) @LooperMode(LooperMode.Mode.PAUSED) @Config(manifest = Config.NONE) class NumericExpressionInputIsEquivalentToRuleClassifierProviderTest { + // TODO: add details about the sheet to this test's KDoc. + @Inject internal lateinit var provider: NumericExpressionInputIsEquivalentToRuleClassifierProvider + @Parameter lateinit var answer: String + @Parameter lateinit var input: String + private lateinit var classifier: RuleClassifier @Before @@ -43,33 +51,311 @@ class NumericExpressionInputIsEquivalentToRuleClassifierProviderTest { classifier = provider.createRuleClassifier() } - // TODO: finish tests. + @Test + @RunParameterized( + Iteration("0==0", "answer=0", "input=0"), + Iteration("1==1", "answer=1", "input=1"), + Iteration("2==2", "answer=2", "input=2") + ) + fun testMatches_sameSingleTerms_returnsTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // If the two expressions are exactly the same, the classifier should match. + assertThat(matches).isTrue() + } @Test - fun test() { - val answerExpression = createMathExpression("0") - val inputExpression = createMathExpression("1") + @RunParameterized( + Iteration("-2==-2", "answer=-2", "input=-2"), + Iteration("1+3.14==1+3.14", "answer=1+3.14", "input=1+3.14"), + Iteration(" 1 + 3.14 ==1+3.14", "answer= 1 + 3.14 ", "input=1+3.14"), + Iteration("1+2+3==1+2+3", "answer=1+2+3", "input=1+2+3"), + Iteration("1-3.14==1-3.14", "answer=1-3.14", "input=1-3.14"), + Iteration("2*3.14==2*3.14", "answer=2*3.14", "input=2*3.14"), + Iteration("2/3==2/3", "answer=2/3", "input=2/3"), + Iteration("2/3.14==2/3.14", "answer=2/3.14", "input=2/3.14"), + Iteration("2^3==2^3", "answer=2^3", "input=2^3"), + Iteration("2^3.14==2^3.14", "answer=2^3.14", "input=2^3.14"), + Iteration("sqrt(2)==sqrt(2)", "answer=sqrt(2)", "input=sqrt(2)") + ) + fun testMatches_sameSingleOperations_returnsTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) - val matches = - classifier.matches( - answerExpression, - inputs = mapOf("x" to inputExpression), - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() - ) + val matches = matchesClassifier(answerExpression, inputExpression) + // If the two expressions are exactly the same, the classifier should match. assertThat(matches).isTrue() } - private fun setUpTestApplicationComponent() { - DaggerNumericExpressionInputIsEquivalentToRuleClassifierProviderTest_TestApplicationComponent - .builder() - .setApplication(ApplicationProvider.getApplicationContext()).build().inject(this) + @Test + @RunParameterized( + Iteration("1!=0", "answer=1", "input=0"), + Iteration("0!=1", "answer=0", "input=1"), + Iteration("3.14!=1", "answer=3.14", "input=1"), + Iteration("1!=3.14", "answer=1", "input=3.14") + ) + fun testMatches_differentSingleTerms_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // If the two expressions don't evaluate to the same value then the classifier won't match them. + assertThat(matches).isFalse() + } + + @Test + @RunParameterized( + Iteration("3.14+1==1+3.14", "answer=3.14+1", "input=1+3.14"), + Iteration("3+2+1==1+2+3", "answer=3+2+1", "input=1+2+3"), + Iteration("-3.14+1==1-3.14", "answer=-3.14+1", "input=1-3.14"), + Iteration("3.14*2==2*3.14", "answer=3.14*2", "input=2*3.14") + ) + fun testMatches_operationsDiffer_byCommutativity_returnsTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // Reordering terms by commutativity is allowed by this classifier. + assertThat(matches).isTrue() + } + + @Test + @RunParameterized( + Iteration("1+(2+3)==(1+2)+3", "answer=1+(2+3)", "input=(1+2)+3"), + Iteration("2*(3*4)==(2*3)*4", "answer=2*(3*4)", "input=(2*3)*4") + ) + fun testMatches_operationsDiffer_byAssociativity_returnsTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // Changing operation associativity is allowed by this classifier. + assertThat(matches).isTrue() + } + + @Test + @RunParameterized( + Iteration("3.14-1!=1-3.14", "answer=3.14-1", "input=1-3.14"), + Iteration("1-(2-3)!=(1-2)-3", "answer=1-(2-3)", "input=(1-2)-3"), + Iteration("3.14/2!=2/3.14", "answer=3.14/2", "input=2/3.14"), + Iteration("2/(3/4)!=(2/3)/4", "answer=2/(3/4)", "input=(2/3)/4"), + Iteration("3.14^2!=2^3.14", "answer=3.14^2", "input=2^3.14"), + Iteration("3.14-x!=x-3.14", "answer=3.14-x", "input=x-3.14"), + Iteration("x-(y-z)!=(x-y)-z", "answer=x-(y-z)", "input=(x-y)-z"), + Iteration("3.14/x!=x/3.14", "answer=3.14/x", "input=x/3.14"), + Iteration("x/(y/z)!=(x/y)/z", "answer=x/(y/z)", "input=(x/y)/z") + ) + fun testMatches_operationsDiffer_byNonCommutativeOrAssociativeReordering_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // Non-commutative and non-associative reordering generally results in a different value, so the + // classifier will fail to match. + assertThat(matches).isFalse() + } + + @Test + @RunParameterized( + Iteration("1-2==-(2-1)", "answer=1-2", "input=-(2-1)"), + Iteration("1+2==1+1+1", "answer=1+2", "input=1+1+1"), + Iteration("1+2==1-(-2)", "answer=1+2", "input=1-(-2)"), + Iteration("4-6==1-2-1", "answer=4-6", "input=1-2-1"), + Iteration("2*3*2*2==2*3*4", "answer=2*3*2*2", "input=2*3*4"), + Iteration("-6-2==2*-(3+1)", "answer=-6-2", "input=2*-(3+1)"), + Iteration("2/3/2/2==2/3/4", "answer=2/3/2/2", "input=2/3/4"), + Iteration("2^(2+1)==2^3", "answer=2^(2+1)", "input=2^3"), + Iteration("2^(-1)==1/2", "answer=2^(-1)", "input=1/2") + ) + fun testMatches_operationsDiffer_byDistributionAndCombining_returnTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // This classifier supports any distribution or combining of terms. + assertThat(matches).isTrue() + } + + @Test + @RunParameterized( +// Iteration("2*(2+6+3+4)==2*(2+6+3+4)", "answer=2*(2+6+3+4)", "input=2*(2+6+3+4)"), +// Iteration("2 × (2+6+3+4)==2*(2+6+3+4)", "answer=2 × (2+6+3+4)", "input=2*(2+6+3+4)"), +// Iteration( +// "15 - (6 × 2) + 3==15 - (6 × 2) + 3", "answer=15 - (6 × 2) + 3", "input=15 - (6 × 2) + 3" +// ), +// Iteration( +// "2 × (50 + 150 + 100 + 25) ==(50 + 150 + 100 + 25) × 2", +// "answer=2 × (50 + 150 + 100 + 25) ", +// "input=(50 + 150 + 100 + 25) × 2" +// ), +// Iteration( +// "2 * (50 + 150 + 100 + 25) ==2 × (50 + 150 + 100 + 25)", +// "answer=2 * (50 + 150 + 100 + 25) ", +// "input=2 × (50 + 150 + 100 + 25)" +// ), +// Iteration("2+5==5+2", "answer=2+5", "input=5+2"), +// Iteration("5+2==5+2", "answer=5+2", "input=5+2"), +// Iteration("6 − (− 4)==6 − (− 4)", "answer=6 − (− 4)", "input=6 − (− 4)"), +// Iteration("6-(-4)==6 − (− 4)", "answer=6-(-4)", "input=6 − (− 4)"), +// Iteration("− (− 4) + 6==6 − (− 4)", "answer=− (− 4) + 6", "input=6 − (− 4)"), +// Iteration("10==6 − (− 4)", "answer=10", "input=6 − (− 4)"), +// Iteration("6 + 4==6 − (− 4)", "answer=6 + 4", "input=6 − (− 4)"), +// Iteration("6 + 2^2==6 − (− 4)", "answer=6 + 2^2", "input=6 − (− 4)"), +// Iteration("3 * 2 − (− 4)==6 − (− 4)", "answer=3 * 2 − (− 4)", "input=6 − (− 4)"), +// Iteration("100/10==6 − (− 4)", "answer=100/10", "input=6 − (− 4)"), +// Iteration("3 * 10^-5==3 * 10^-5", "answer=3 * 10^-5", "input=3 * 10^-5"), +// Iteration("3/(10 * 10^4)==3 * 10^-5", "answer=3/(10 * 10^4)", "input=3 * 10^-5"), +// Iteration( +// "1000 + 200 + 30 + 4 + 0.5 + 0.06==1000 + 200 + 30 + 4 + 0.5 + 0.06", +// "answer=1000 + 200 + 30 + 4 + 0.5 + 0.06", +// "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" +// ), +// Iteration( +// "200 + 30 + 4 + 0.5 + 0.06 + 1000==1000 + 200 + 30 + 4 + 0.5 + 0.06", +// "answer=200 + 30 + 4 + 0.5 + 0.06 + 1000", +// "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" +// ), +// Iteration( +// "0.06 + 0.5 + 4 + 30 + 200 + 1000==1000 + 200 + 30 + 4 + 0.5 + 0.06", +// "answer=0.06 + 0.5 + 4 + 30 + 200 + 1000", +// "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" +// ), +// Iteration( +// "1234.56==1000 + 200 + 30 + 4 + 0.5 + 0.06", +// "answer=1234.56", +// "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" +// ), +// Iteration( +// "123456/100==1000 + 200 + 30 + 4 + 0.5 + 0.06", +// "answer=123456/100", +// "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" +// ), +// Iteration( +// "61728/50==1000 + 200 + 30 + 4 + 0.5 + 0.06", +// "answer=61728/50", +// "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" +// ), +// Iteration( +// "1234 + 56/100==1000 + 200 + 30 + 4 + 0.5 + 0.06", +// "answer=1234 + 56/100", +// "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" +// ), +// Iteration( +// "1230 + 4.56==1000 + 200 + 30 + 4 + 0.5 + 0.06", +// "answer=1230 + 4.56", +// "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" +// ), +// Iteration("2 * 2 * 3 * 3==2 * 2 * 3 * 3", "answer=2 * 2 * 3 * 3", "input=2 * 2 * 3 * 3"), +// Iteration( +// "2 * 2 * 3 * 3 * 1==2 * 2 * 3 * 3", "answer=2 * 2 * 3 * 3 * 1", "input=2 * 2 * 3 * 3" +// ), +// Iteration("2 * 2 * 9==2 * 2 * 3 * 3", "answer=2 * 2 * 9", "input=2 * 2 * 3 * 3"), +// Iteration("4 * 3^2==2 * 2 * 3 * 3", "answer=4 * 3^2", "input=2 * 2 * 3 * 3"), +// Iteration("8/2 * 3 * 3==2 * 2 * 3 * 3", "answer=8/2 * 3 * 3", "input=2 * 2 * 3 * 3"), +// Iteration("36==2 * 2 * 3 * 3", "answer=36", "input=2 * 2 * 3 * 3"), +// Iteration("sqrt(4-2)==sqrt(2)", "answer=sqrt(4-2)", "input=sqrt(2)"), +// Iteration("2*(6+3+4) + 4==2*(2+6+3+4)", "answer=2*(6+3+4) + 4", "input=2*(2+6+3+4)"), +// Iteration("2*(2+6+3) + 8==2*(2+6+3+4)", "answer=2*(2+6+3) + 8", "input=2*(2+6+3+4)"), +// Iteration("(2+6+3+4)*2==2*(2+6+3+4)", "answer=(2+6+3+4)*2", "input=2*(2+6+3+4)"), +// Iteration("(2+6+3+4) × 2==2*(2+6+3+4)", "answer=(2+6+3+4) × 2", "input=2*(2+6+3+4)"), +// Iteration("15 - 12 + 3==15 - (6 × 2) + 3", "answer=15 - 12 + 3", "input=15 - (6 × 2) + 3"), +// Iteration( +// "3 - (6 * 2) + 15==15 - (6 × 2) + 3", "answer=3 - (6 * 2) + 15", "input=15 - (6 × 2) + 3" +// ), +// Iteration( +// "15 - (2 × 6) + 3==15 - (6 × 2) + 3", "answer=15 - (2 × 6) + 3", "input=15 - (6 × 2) + 3" +// ), +// Iteration( +// "2 *(50 + 150) + 2*(100 + 25)==(50 + 150 + 100 + 25) × 2", +// "answer=2 *(50 + 150) + 2*(100 + 25)", +// "input=(50 + 150 + 100 + 25) × 2" +// ), +// Iteration( +// "2* ( 25+50+100+150)==(50 + 150 + 100 + 25) × 2", +// "answer=2* ( 25+50+100+150)", +// "input=(50 + 150 + 100 + 25) × 2" +// ), +// Iteration("10^−5 * 3==3 * 10^-5", "answer=10^−5 * 3", "input=3 * 10^-5"), +// Iteration("30 * 10^−6==3 * 10^-5", "answer=30 * 10^−6", "input=3 * 10^-5"), +// Iteration("0.00003==3 * 10^-5", "answer=0.00003", "input=3 * 10^-5"), +// Iteration("3/10^5==3 * 10^-5", "answer=3/10^5", "input=3 * 10^-5"), + Iteration("3 *2 – (− 4)==6 − (− 4)", "answer=3 *2 – (− 4)", "input=6 − (− 4)"), +// Iteration("7==5+2", "answer=7", "input=5+2"), +// Iteration("3+4==5+2", "answer=3+4", "input=5+2") + ) + fun testMatches_assortedExpressions_withMatchingCharacteristics_returnsTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // This verifies a variety of expressions that per the PRD and technical specification should + // pass for this classifier. + assertThat(matches).isTrue() + } + + @Test + @RunParameterized( + Iteration("3 * 10^5!=3 * 10^-5", "answer=3 * 10^5", "input=3 * 10^-5"), + Iteration("2 * 10^−5!=3 * 10^-5", "answer=2 * 10^−5", "input=3 * 10^-5"), + Iteration("5 * 10^−3!=3 * 10^-5", "answer=5 * 10^−3", "input=3 * 10^-5"), + Iteration( + "123456!=1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=123456", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration( + "1000 + 200 + 30!=1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=1000 + 200 + 30", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration("6 − 4!=6 − (− 4)", "answer=6 − 4", "input=6 − (− 4)"), + Iteration("6 + (− 4)!=6 − (− 4)", "answer=6 + (− 4)", "input=6 − (− 4)"), + Iteration("100!=6 − (− 4)", "answer=100", "input=6 − (− 4)"), + Iteration("2 * 2 * 3!=2 * 2 * 3 * 3", "answer=2 * 2 * 3", "input=2 * 2 * 3 * 3"), + Iteration("2 * 3 * 3 * 3!=2 * 2 * 3 * 3", "answer=2 * 3 * 3 * 3", "input=2 * 2 * 3 * 3") + ) + fun testMatches_assortedExpressions_withoutMatchingCharacteristics_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // This verifies a variety of expressions that per the PRD and technical specification should + // not pass for this classifier. + assertThat(matches).isFalse() + } + + private fun matchesClassifier( + answerExpression: InteractionObject, + inputExpression: InteractionObject + ): Boolean { + return classifier.matches( + answerExpression, + inputs = mapOf("x" to inputExpression), + writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + ) } private fun createMathExpression(rawExpression: String) = InteractionObject.newBuilder().apply { mathExpression = rawExpression }.build() + private fun setUpTestApplicationComponent() { + DaggerNumericExpressionInputIsEquivalentToRuleClassifierProviderTest_TestApplicationComponent + .builder() + .setApplication(ApplicationProvider.getApplicationContext()).build().inject(this) + } + // TODO(#89): Move this to a common test application component. @Module class TestModule { diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest.kt index f69864d89b6..42b4c7e2b99 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest.kt @@ -3,7 +3,6 @@ package org.oppia.android.domain.classify.rules.numericexpressioninput import android.app.Application import android.content.Context import androidx.test.core.app.ApplicationProvider -import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat import dagger.BindsInstance import dagger.Component @@ -17,6 +16,10 @@ import org.junit.runner.RunWith import org.oppia.android.app.model.InteractionObject import org.oppia.android.app.model.WrittenTranslationContext import org.oppia.android.domain.classify.RuleClassifier +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Iteration +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Parameter +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.RunParameterized import org.oppia.android.testing.robolectric.RobolectricModule import org.oppia.android.testing.threading.TestDispatcherModule import org.oppia.android.testing.time.FakeOppiaClockModule @@ -28,13 +31,18 @@ import org.robolectric.annotation.LooperMode /** Tests for [NumericExpressionInputMatchesExactlyWithRuleClassifierProvider]. */ // FunctionName: test names are conventionally named with underscores. @Suppress("FunctionName") -@RunWith(AndroidJUnit4::class) +@RunWith(OppiaParameterizedTestRunner::class) @LooperMode(LooperMode.Mode.PAUSED) @Config(manifest = Config.NONE) class NumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest { + // TODO: add details about the sheet to this test's KDoc. + @Inject internal lateinit var provider: NumericExpressionInputMatchesExactlyWithRuleClassifierProvider + @Parameter lateinit var answer: String + @Parameter lateinit var input: String + private lateinit var classifier: RuleClassifier @Before @@ -43,46 +51,303 @@ class NumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest { classifier = provider.createRuleClassifier() } - // TODO: finish tests. - - // testMatches_zeroAnswer_zeroInput_returnsTrue - // testMatches_zeroAnswer_oneInput_returnsFalse - // testMatches_oneAnswer_zeroInput_returnsFalse - // testMatches_oneAnswer_oneInput_returnsTrue - // testMatches_answerAndInput_sameOperationParameters_returnsTrue - // testMatches_answerAndInput_same_returnsTrue - // testMatches_answerAndInput_differentByCommutativity_returnsFalse - // testMatches_answerAndInput_differentByAssociativity_returnsFalse - // testMatches_answerAndInput_differentByDistributivity_returnsFalse - // testMatches_answerAndInput_differentByNonCommutativeReordering_returnsFalse - // testMatches_answerAndInput_similarInMultipleWays_returnsTrue - // testMatches_answerAndInput_varyIncorrectlyInMultipleWays_returnsFalse + @Test + @RunParameterized( + Iteration("0==0", "answer=0", "input=0"), + Iteration("1==1", "answer=1", "input=1"), + Iteration("2==2", "answer=2", "input=2") + ) + fun testMatches_sameSingleTerms_returnsTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // If the two expressions are exactly the same, the classifier should match. + assertThat(matches).isTrue() + } @Test - fun test() { - val answerExpression = createMathExpression("0") - val inputExpression = createMathExpression("1") + @RunParameterized( + Iteration("-2==-2", "answer=-2", "input=-2"), + Iteration("1+3.14==1+3.14", "answer=1+3.14", "input=1+3.14"), + Iteration(" 1 + 3.14 ==1+3.14", "answer= 1 + 3.14 ", "input=1+3.14"), + Iteration("1+2+3==1+2+3", "answer=1+2+3", "input=1+2+3"), + Iteration("1-3.14==1-3.14", "answer=1-3.14", "input=1-3.14"), + Iteration("2*3.14==2*3.14", "answer=2*3.14", "input=2*3.14"), + Iteration("2/3==2/3", "answer=2/3", "input=2/3"), + Iteration("2/3.14==2/3.14", "answer=2/3.14", "input=2/3.14"), + Iteration("2^3==2^3", "answer=2^3", "input=2^3"), + Iteration("2^3.14==2^3.14", "answer=2^3.14", "input=2^3.14"), + Iteration("sqrt(2)==sqrt(2)", "answer=sqrt(2)", "input=sqrt(2)") + ) + fun testMatches_sameSingleOperations_returnsTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) - val matches = - classifier.matches( - answerExpression, - inputs = mapOf("x" to inputExpression), - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() - ) + val matches = matchesClassifier(answerExpression, inputExpression) + // If the two expressions are exactly the same, the classifier should match. assertThat(matches).isTrue() } - private fun setUpTestApplicationComponent() { - DaggerNumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest_TestApplicationComponent - .builder() - .setApplication(ApplicationProvider.getApplicationContext()).build().inject(this) + @Test + @RunParameterized( + Iteration("1!=0", "answer=1", "input=0"), + Iteration("0!=1", "answer=0", "input=1"), + Iteration("3.14!=1", "answer=3.14", "input=1"), + Iteration("1!=3.14", "answer=1", "input=3.14") + ) + fun testMatches_differentSingleTerms_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // If the two expressions aren't exactly the same (minus whitespace), they won't match. + assertThat(matches).isFalse() + } + + @Test + @RunParameterized( + Iteration("3.14+1!=1+3.14", "answer=3.14+1", "input=1+3.14"), + Iteration("3+2+1!=1+2+3", "answer=3+2+1", "input=1+2+3"), + Iteration("-3.14+1!=1-3.14", "answer=-3.14+1", "input=1-3.14"), + Iteration("3.14*2!=2*3.14", "answer=3.14*2", "input=2*3.14") + ) + fun testMatches_operationsDiffer_byCommutativity_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // This classifier expects commutativity to be retained. + assertThat(matches).isFalse() + } + + @Test + @RunParameterized( + Iteration("1+(2+3)!=(1+2)+3", "answer=1+(2+3)", "input=(1+2)+3"), + Iteration("2*(3*4)!=(2*3)*4", "answer=2*(3*4)", "input=(2*3)*4") + ) + fun testMatches_operationsDiffer_byAssociativity_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // This classifier expects associativity to be retained. + assertThat(matches).isFalse() + } + + @Test + @RunParameterized( + Iteration("3.14-1!=1-3.14", "answer=3.14-1", "input=1-3.14"), + Iteration("1-(2-3)!=(1-2)-3", "answer=1-(2-3)", "input=(1-2)-3"), + Iteration("3.14/2!=2/3.14", "answer=3.14/2", "input=2/3.14"), + Iteration("2/(3/4)!=(2/3)/4", "answer=2/(3/4)", "input=(2/3)/4"), + Iteration("3.14^2!=2^3.14", "answer=3.14^2", "input=2^3.14"), + Iteration("3.14-x!=x-3.14", "answer=3.14-x", "input=x-3.14"), + Iteration("x-(y-z)!=(x-y)-z", "answer=x-(y-z)", "input=(x-y)-z"), + Iteration("3.14/x!=x/3.14", "answer=3.14/x", "input=x/3.14"), + Iteration("x/(y/z)!=(x/y)/z", "answer=x/(y/z)", "input=(x/y)/z") + ) + fun testMatches_operationsDiffer_byNonCommutativeOrAssociativeReordering_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // Non-commutative and non-associative reordering generally results in a different value, so the + // classifier will fail to match. + assertThat(matches).isFalse() + } + + @Test + @RunParameterized( + Iteration("1-2!=-(2-1)", "answer=1-2", "input=-(2-1)"), + Iteration("1+2!=1+1+1", "answer=1+2", "input=1+1+1"), + Iteration("1+2!=1-(-2)", "answer=1+2", "input=1-(-2)"), + Iteration("4-6!=1-2-1", "answer=4-6", "input=1-2-1"), + Iteration("2*3*2*2!=2*3*4", "answer=2*3*2*2", "input=2*3*4"), + Iteration("-6-2!=2*-(3+1)", "answer=-6-2", "input=2*-(3+1)"), + Iteration("2/3/2/2!=2/3/4", "answer=2/3/2/2", "input=2/3/4"), + Iteration("2^(2+1)!=2^3", "answer=2^(2+1)", "input=2^3"), + Iteration("2^(-1)!=1/2", "answer=2^(-1)", "input=1/2") + ) + fun testMatches_operationsDiffer_byDistributionAndCombining_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // This classifier doesn't support distributing or combining terms. + assertThat(matches).isFalse() + } + + @Test + @RunParameterized( + Iteration("2*(2+6+3+4)==2*(2+6+3+4)", "answer=2*(2+6+3+4)", "input=2*(2+6+3+4)"), + Iteration("2 × (2+6+3+4)==2*(2+6+3+4)", "answer=2 × (2+6+3+4)", "input=2*(2+6+3+4)"), + Iteration( + "15 - (6 × 2) + 3==15 - (6 × 2) + 3", "answer=15 - (6 × 2) + 3", "input=15 - (6 × 2) + 3" + ), + Iteration( + "2 * (50 + 150 + 100 + 25) ==2 × (50 + 150 + 100 + 25)", + "answer=2 * (50 + 150 + 100 + 25) ", + "input=2 × (50 + 150 + 100 + 25)" + ), + Iteration("5+2==5+2", "answer=5+2", "input=5+2"), + Iteration("6 − (− 4)==6 − (− 4)", "answer=6 − (− 4)", "input=6 − (− 4)"), + Iteration("6-(-4)==6 − (− 4)", "answer=6-(-4)", "input=6 − (− 4)"), + Iteration("3 * 10^-5==3 * 10^-5", "answer=3 * 10^-5", "input=3 * 10^-5"), + Iteration( + "1000 + 200 + 30 + 4 + 0.5 + 0.06==1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=1000 + 200 + 30 + 4 + 0.5 + 0.06", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration("2 * 2 * 3 * 3==2 * 2 * 3 * 3", "answer=2 * 2 * 3 * 3", "input=2 * 2 * 3 * 3") + ) + fun testMatches_assortedExpressions_withMatchingCharacteristics_returnsTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // This verifies a variety of expressions that per the PRD and technical specification should + // pass for this classifier. + assertThat(matches).isTrue() + } + + @Test + @RunParameterized( + Iteration( + "2 × (50 + 150 + 100 + 25) !=(50 + 150 + 100 + 25) × 2", + "answer=2 × (50 + 150 + 100 + 25) ", + "input=(50 + 150 + 100 + 25) × 2" + ), + Iteration("− (− 4) + 6!=6 − (− 4)", "answer=− (− 4) + 6", "input=6 − (− 4)"), + Iteration("10!=6 − (− 4)", "answer=10", "input=6 − (− 4)"), + Iteration("6 + 4!=6 − (− 4)", "answer=6 + 4", "input=6 − (− 4)"), + Iteration("6 + 2^2!=6 − (− 4)", "answer=6 + 2^2", "input=6 − (− 4)"), + Iteration("3 * 2 − (− 4)!=6 − (− 4)", "answer=3 * 2 − (− 4)", "input=6 − (− 4)"), + Iteration("100/10!=6 − (− 4)", "answer=100/10", "input=6 − (− 4)"), + Iteration("2+5!=5+2", "answer=2+5", "input=5+2"), + Iteration("3/(10 * 10^4)!=3 * 10^-5", "answer=3/(10 * 10^4)", "input=3 * 10^-5"), + Iteration( + "200 + 30 + 4 + 0.5 + 0.06 + 1000!=1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=200 + 30 + 4 + 0.5 + 0.06 + 1000", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration( + "0.06 + 0.5 + 4 + 30 + 200 + 1000!=1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=0.06 + 0.5 + 4 + 30 + 200 + 1000", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06"), + Iteration( + "1234.56!=1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=1234.56", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06"), + Iteration( + "123456/100!=1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=123456/100", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06"), + Iteration( + "61728/50!=1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=61728/50", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06"), + Iteration( + "1234 + 56/10!=1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=1234 + 56/10", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06"), + Iteration( + "1230 + 4.56!=1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=1230 + 4.56", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06"), + Iteration( + "2 * 2 * 3 * 3 * 1!=2 * 2 * 3 * 3", "answer=2 * 2 * 3 * 3 * 1", "input=2 * 2 * 3 * 3" + ), + Iteration("2 * 2 * 9!=2 * 2 * 3 * 3", "answer=2 * 2 * 9", "input=2 * 2 * 3 * 3"), + Iteration("4 * 3^2!=2 * 2 * 3 * 3", "answer=4 * 3^2", "input=2 * 2 * 3 * 3"), + Iteration("8/2 * 3 * 3!=2 * 2 * 3 * 3", "answer=8/2 * 3 * 3", "input=2 * 2 * 3 * 3"), + Iteration("36!=2 * 2 * 3 * 3", "answer=36", "input=2 * 2 * 3 * 3"), + Iteration("sqrt(4-2)!=sqrt(2)", "answer=sqrt(4-2)", "input=sqrt(2)"), + Iteration("2*(6+3+4) + 4!=2*(2+6+3+4)", "answer=2*(6+3+4) + 4", "input=2*(2+6+3+4)"), + Iteration("2*(2+6+3) + 8!=2*(2+6+3+4)", "answer=2*(2+6+3) + 8", "input=2*(2+6+3+4)"), + Iteration("(2+6+3+4)*2!=2*(2+6+3+4)", "answer=(2+6+3+4)*2", "input=2*(2+6+3+4)"), + Iteration("(2+6+3+4) × 2!=2*(2+6+3+4)", "answer=(2+6+3+4) × 2", "input=2*(2+6+3+4)"), + Iteration("15 - 12 + 3!=15 - (6 × 2) + 3", "answer=15 - 12 + 3", "input=15 - (6 × 2) + 3"), + Iteration( + "3 - (6 * 2) + 15!=15 - (6 × 2) + 3", "answer=3 - (6 * 2) + 15", "input=15 - (6 × 2) + 3" + ), + Iteration( + "15 - (2 × 6) + 3!=15 - (6 × 2) + 3", "answer=15 - (2 × 6) + 3", "input=15 - (6 × 2) + 3" + ), + Iteration( + "2 *(50 + 150) + 2*(100 + 25)!=(50 + 150 + 100 + 25) × 2", + "answer=2 *(50 + 150) + 2*(100 + 25)", + "input=(50 + 150 + 100 + 25) × 2" + ), + Iteration( + "2* ( 25+50+100+150)!=(50 + 150 + 100 + 25) × 2", + "answer=2* ( 25+50+100+150)", + "input=(50 + 150 + 100 + 25) × 2" + ), + Iteration("10^−5 * 3!=3 * 10^-5", "answer=10^−5 * 3", "input=3 * 10^-5"), + Iteration("3 * 10^5!=3 * 10^-5", "answer=3 * 10^5", "input=3 * 10^-5"), + Iteration("2 * 10^−5!=3 * 10^-5", "answer=2 * 10^−5", "input=3 * 10^-5"), + Iteration("5 * 10^−3!=3 * 10^-5", "answer=5 * 10^−3", "input=3 * 10^-5"), + Iteration("30 * 10^−6!=3 * 10^-5", "answer=30 * 10^−6", "input=3 * 10^-5"), + Iteration("0.00003!=3 * 10^-5", "answer=0.00003", "input=3 * 10^-5"), + Iteration("3/10^5!=3 * 10^-5", "answer=3/10^5", "input=3 * 10^-5"), + Iteration( + "123456!=1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=123456", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06"), + Iteration( + "1000 + 200 + 30!=1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=1000 + 200 + 30", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06"), + Iteration("3 *2 – (− 4)!=6 − (− 4)", "answer=3 *2 – (− 4)", "input=6 − (− 4)"), + Iteration("6 − 4!=6 − (− 4)", "answer=6 − 4", "input=6 − (− 4)"), + Iteration("6 + (− 4)!=6 − (− 4)", "answer=6 + (− 4)", "input=6 − (− 4)"), + Iteration("100!=6 − (− 4)", "answer=100", "input=6 − (− 4)"), + Iteration("7!=5+2", "answer=7", "input=5+2"), + Iteration("3+4!=5+2", "answer=3+4", "input=5+2"), + Iteration("2 * 2 * 3!=2 * 2 * 3 * 3", "answer=2 * 2 * 3", "input=2 * 2 * 3 * 3"), + Iteration("2 * 3 * 3 * 3!=2 * 2 * 3 * 3", "answer=2 * 3 * 3 * 3", "input=2 * 2 * 3 * 3") + ) + fun testMatches_assortedExpressions_withoutMatchingCharacteristics_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // This verifies a variety of expressions that per the PRD and technical specification should + // not pass for this classifier. + assertThat(matches).isFalse() + } + + private fun matchesClassifier( + answerExpression: InteractionObject, + inputExpression: InteractionObject + ): Boolean { + return classifier.matches( + answerExpression, + inputs = mapOf("x" to inputExpression), + writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + ) } private fun createMathExpression(rawExpression: String) = InteractionObject.newBuilder().apply { mathExpression = rawExpression }.build() + private fun setUpTestApplicationComponent() { + DaggerNumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest_TestApplicationComponent + .builder() + .setApplication(ApplicationProvider.getApplicationContext()).build().inject(this) + } + // TODO(#89): Move this to a common test application component. @Module class TestModule { diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt index 508a6243b80..bbd4085d420 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt @@ -3,7 +3,6 @@ package org.oppia.android.domain.classify.rules.numericexpressioninput import android.app.Application import android.content.Context import androidx.test.core.app.ApplicationProvider -import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat import dagger.BindsInstance import dagger.Component @@ -17,6 +16,10 @@ import org.junit.runner.RunWith import org.oppia.android.app.model.InteractionObject import org.oppia.android.app.model.WrittenTranslationContext import org.oppia.android.domain.classify.RuleClassifier +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Iteration +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Parameter +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.RunParameterized import org.oppia.android.testing.robolectric.RobolectricModule import org.oppia.android.testing.threading.TestDispatcherModule import org.oppia.android.testing.time.FakeOppiaClockModule @@ -28,13 +31,18 @@ import org.robolectric.annotation.LooperMode /** Tests for [NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider]. */ // FunctionName: test names are conventionally named with underscores. @Suppress("FunctionName") -@RunWith(AndroidJUnit4::class) +@RunWith(OppiaParameterizedTestRunner::class) @LooperMode(LooperMode.Mode.PAUSED) @Config(manifest = Config.NONE) class NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest { + // TODO: add details about the sheet to this test's KDoc. + @Inject internal lateinit var provider: NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider + @Parameter lateinit var answer: String + @Parameter lateinit var input: String + private lateinit var classifier: RuleClassifier @Before @@ -43,33 +51,325 @@ class NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvide classifier = provider.createRuleClassifier() } - // TODO: finish tests. + @Test + @RunParameterized( + Iteration("0==0", "answer=0", "input=0"), + Iteration("1==1", "answer=1", "input=1"), + Iteration("2==2", "answer=2", "input=2") + ) + fun testMatches_sameSingleTerms_returnsTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // If the two expressions are exactly the same, the classifier should match. + assertThat(matches).isTrue() + } @Test - fun test() { - val answerExpression = createMathExpression("0") - val inputExpression = createMathExpression("1") + @RunParameterized( + Iteration("-2==-2", "answer=-2", "input=-2"), + Iteration("1+3.14==1+3.14", "answer=1+3.14", "input=1+3.14"), + Iteration(" 1 + 3.14 ==1+3.14", "answer= 1 + 3.14 ", "input=1+3.14"), + Iteration("1+2+3==1+2+3", "answer=1+2+3", "input=1+2+3"), + Iteration("1-3.14==1-3.14", "answer=1-3.14", "input=1-3.14"), + Iteration("2*3.14==2*3.14", "answer=2*3.14", "input=2*3.14"), + Iteration("2/3==2/3", "answer=2/3", "input=2/3"), + Iteration("2/3.14==2/3.14", "answer=2/3.14", "input=2/3.14"), + Iteration("2^3==2^3", "answer=2^3", "input=2^3"), + Iteration("2^3.14==2^3.14", "answer=2^3.14", "input=2^3.14"), + Iteration("sqrt(2)==sqrt(2)", "answer=sqrt(2)", "input=sqrt(2)") + ) + fun testMatches_sameSingleOperations_returnsTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) - val matches = - classifier.matches( - answerExpression, - inputs = mapOf("x" to inputExpression), - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() - ) + val matches = matchesClassifier(answerExpression, inputExpression) + // If the two expressions are exactly the same, the classifier should match. assertThat(matches).isTrue() } - private fun setUpTestApplicationComponent() { - DaggerNumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest_TestApplicationComponent - .builder() - .setApplication(ApplicationProvider.getApplicationContext()).build().inject(this) + @Test + @RunParameterized( + Iteration("1!=0", "answer=1", "input=0"), + Iteration("0!=1", "answer=0", "input=1"), + Iteration("3.14!=1", "answer=3.14", "input=1"), + Iteration("1!=3.14", "answer=1", "input=3.14") + ) + fun testMatches_differentSingleTerms_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // If the two expressions aren't exactly the same (minus whitespace and some minor term + // reordering), they won't match. + assertThat(matches).isFalse() + } + + @Test + @RunParameterized( + Iteration("3.14+1==1+3.14", "answer=3.14+1", "input=1+3.14"), + Iteration("3+2+1==1+2+3", "answer=3+2+1", "input=1+2+3"), + Iteration("-3.14+1==1-3.14", "answer=-3.14+1", "input=1-3.14"), + Iteration("3.14*2==2*3.14", "answer=3.14*2", "input=2*3.14") + ) + fun testMatches_operationsDiffer_byCommutativity_returnsTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // Reordering terms by commutativity is allowed by this classifier. + assertThat(matches).isTrue() + } + + @Test + @RunParameterized( + Iteration("1+(2+3)==(1+2)+3", "answer=1+(2+3)", "input=(1+2)+3"), + Iteration("2*(3*4)==(2*3)*4", "answer=2*(3*4)", "input=(2*3)*4") + ) + fun testMatches_operationsDiffer_byAssociativity_returnsTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // Changing operation associativity is allowed by this classifier. + assertThat(matches).isTrue() + } + + @Test + @RunParameterized( + Iteration("3.14-1!=1-3.14", "answer=3.14-1", "input=1-3.14"), + Iteration("1-(2-3)!=(1-2)-3", "answer=1-(2-3)", "input=(1-2)-3"), + Iteration("3.14/2!=2/3.14", "answer=3.14/2", "input=2/3.14"), + Iteration("2/(3/4)!=(2/3)/4", "answer=2/(3/4)", "input=(2/3)/4"), + Iteration("3.14^2!=2^3.14", "answer=3.14^2", "input=2^3.14"), + Iteration("3.14-x!=x-3.14", "answer=3.14-x", "input=x-3.14"), + Iteration("x-(y-z)!=(x-y)-z", "answer=x-(y-z)", "input=(x-y)-z"), + Iteration("3.14/x!=x/3.14", "answer=3.14/x", "input=x/3.14"), + Iteration("x/(y/z)!=(x/y)/z", "answer=x/(y/z)", "input=(x/y)/z") + ) + fun testMatches_operationsDiffer_byNonCommutativeOrAssociativeReordering_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // Non-commutative and non-associative reordering generally results in a different value, so the + // classifier will fail to match. + assertThat(matches).isFalse() + } + + @Test + @RunParameterized( + Iteration("1+2==1-(-2)", "answer=1+2", "input=1-(-2)") + ) + fun testMatches_operationsDiffer_byDistributingNegation_returnsTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // The classifier does support distributing negations (e.g. across groups). + assertThat(matches).isTrue() + } + + @Test + @RunParameterized( + Iteration("1-2==-(2-1)", "answer=1-2", "input=-(2-1)"), + Iteration("1+2!=1+1+1", "answer=1+2", "input=1+1+1"), + Iteration("4-6!=1-2-1", "answer=4-6", "input=1-2-1"), + Iteration("2*3*2*2!=2*3*4", "answer=2*3*2*2", "input=2*3*4"), + Iteration("-6-2!=2*-(3+1)", "answer=-6-2", "input=2*-(3+1)"), + Iteration("2/3/2/2!=2/3/4", "answer=2/3/2/2", "input=2/3/4"), + Iteration("2^(2+1)!=2^3", "answer=2^(2+1)", "input=2^3"), + Iteration("2^(-1)!=1/2", "answer=2^(-1)", "input=1/2") + ) + fun testMatches_operationsDiffer_byDistributionAndCombining_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // This classifier doesn't support broadly distributing or combining terms. + assertThat(matches).isFalse() + } + + @Test + @RunParameterized( + Iteration("2*(2+6+3+4)==2*(2+6+3+4)", "answer=2*(2+6+3+4)", "input=2*(2+6+3+4)"), + Iteration("2 × (2+6+3+4)==2*(2+6+3+4)", "answer=2 × (2+6+3+4)", "input=2*(2+6+3+4)"), + Iteration( + "15 - (6 × 2) + 3==15 - (6 × 2) + 3", "answer=15 - (6 × 2) + 3", "input=15 - (6 × 2) + 3" + ), + Iteration( + "2 × (50 + 150 + 100 + 25) ==(50 + 150 + 100 + 25) × 2", + "answer=2 × (50 + 150 + 100 + 25) ", + "input=(50 + 150 + 100 + 25) × 2" + ), + Iteration( + "2 * (50 + 150 + 100 + 25) ==2 × (50 + 150 + 100 + 25)", + "answer=2 * (50 + 150 + 100 + 25) ", + "input=2 × (50 + 150 + 100 + 25)" + ), + Iteration("2+5==5+2", "answer=2+5", "input=5+2"), + Iteration("5+2==5+2", "answer=5+2", "input=5+2"), + Iteration("6 − (− 4)==6 − (− 4)", "answer=6 − (− 4)", "input=6 − (− 4)"), + Iteration("6-(-4)==6 − (− 4)", "answer=6-(-4)", "input=6 − (− 4)"), + Iteration("− (− 4) + 6==6 − (− 4)", "answer=− (− 4) + 6", "input=6 − (− 4)"), + Iteration("6 + 4!=6 − (− 4)", "answer=6 + 4", "input=6 − (− 4)"), + Iteration("3 * 10^-5==3 * 10^-5", "answer=3 * 10^-5", "input=3 * 10^-5"), + Iteration("10^−5 * 3==3 * 10^-5", "answer=10^−5 * 3", "input=3 * 10^-5"), + Iteration( + "1000 + 200 + 30 + 4 + 0.5 + 0.06==1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=1000 + 200 + 30 + 4 + 0.5 + 0.06", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration( + "200 + 30 + 4 + 0.5 + 0.06 + 1000==1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=200 + 30 + 4 + 0.5 + 0.06 + 1000", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration( + "0.06 + 0.5 + 4 + 30 + 200 + 1000==1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=0.06 + 0.5 + 4 + 30 + 200 + 1000", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration("2 * 2 * 3 * 3==2 * 2 * 3 * 3", "answer=2 * 2 * 3 * 3", "input=2 * 2 * 3 * 3"), + Iteration("(2+6+3+4)*2==2*(2+6+3+4)", "answer=(2+6+3+4)*2", "input=2*(2+6+3+4)"), + Iteration("(2+6+3+4) × 2==2*(2+6+3+4)", "answer=(2+6+3+4) × 2", "input=2*(2+6+3+4)"), + Iteration( + "3 - (6 * 2) + 15==15 - (6 × 2) + 3", "answer=3 - (6 * 2) + 15", "input=15 - (6 × 2) + 3" + ), + Iteration( + "15 - (2 × 6) + 3==15 - (6 × 2) + 3", "answer=15 - (2 × 6) + 3", "input=15 - (6 × 2) + 3" + ), + Iteration( + "2* ( 25+50+100+150)==(50 + 150 + 100 + 25) × 2", + "answer=2* ( 25+50+100+150)", + "input=(50 + 150 + 100 + 25) × 2" + ) + ) + fun testMatches_assortedExpressions_withMatchingCharacteristics_returnsTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // This verifies a variety of expressions that per the PRD and technical specification should + // pass for this classifier. + assertThat(matches).isTrue() + } + + @Test + @RunParameterized( + Iteration("10!=6 − (− 4)", "answer=10", "input=6 − (− 4)"), + Iteration("6 + 2^2!=6 − (− 4)", "answer=6 + 2^2", "input=6 − (− 4)"), + Iteration("3 * 2 − (− 4)!=6 − (− 4)", "answer=3 * 2 − (− 4)", "input=6 − (− 4)"), + Iteration("100/10!=6 − (− 4)", "answer=100/10", "input=6 − (− 4)"), + Iteration("3/(10 * 10^4)!=3 * 10^-5", "answer=3/(10 * 10^4)", "input=3 * 10^-5"), + Iteration( + "1234.56!=1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=1234.56", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration( + "123456/100!=1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=123456/100", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration( + "61728/50!=1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=61728/50", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration( + "1234 + 56/10!=1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=1234 + 56/10", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration( + "1230 + 4.56!=1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=1230 + 4.56", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration( + "2 * 2 * 3 * 3 * 1!=2 * 2 * 3 * 3", "answer=2 * 2 * 3 * 3 * 1", "input=2 * 2 * 3 * 3" + ), + Iteration("2 * 2 * 9!=2 * 2 * 3 * 3", "answer=2 * 2 * 9", "input=2 * 2 * 3 * 3"), + Iteration("4 * 3^2!=2 * 2 * 3 * 3", "answer=4 * 3^2", "input=2 * 2 * 3 * 3"), + Iteration("8/2 * 3 * 3!=2 * 2 * 3 * 3", "answer=8/2 * 3 * 3", "input=2 * 2 * 3 * 3"), + Iteration("36!=2 * 2 * 3 * 3", "answer=36", "input=2 * 2 * 3 * 3"), + Iteration("sqrt(4-2)!=sqrt(2)", "answer=sqrt(4-2)", "input=sqrt(2)"), + Iteration("3 * 10^5!=3 * 10^-5", "answer=3 * 10^5", "input=3 * 10^-5"), + Iteration("2 * 10^−5!=3 * 10^-5", "answer=2 * 10^−5", "input=3 * 10^-5"), + Iteration("5 * 10^−3!=3 * 10^-5", "answer=5 * 10^−3", "input=3 * 10^-5"), + Iteration("30 * 10^−6!=3 * 10^-5", "answer=30 * 10^−6", "input=3 * 10^-5"), + Iteration("0.00003!=3 * 10^-5", "answer=0.00003", "input=3 * 10^-5"), + Iteration("3/10^5!=3 * 10^-5", "answer=3/10^5", "input=3 * 10^-5"), + Iteration( + "123456!=1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=123456", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration( + "1000 + 200 + 30!=1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=1000 + 200 + 30", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration("3 *2 – (− 4)!=6 − (− 4)", "answer=3 *2 – (− 4)", "input=6 − (− 4)"), + Iteration("6 − 4!=6 − (− 4)", "answer=6 − 4", "input=6 − (− 4)"), + Iteration("6 + (− 4)!=6 − (− 4)", "answer=6 + (− 4)", "input=6 − (− 4)"), + Iteration("100!=6 − (− 4)", "answer=100", "input=6 − (− 4)"), + Iteration("7!=5+2", "answer=7", "input=5+2"), + Iteration("3+4!=5+2", "answer=3+4", "input=5+2"), + Iteration("2 * 2 * 3!=2 * 2 * 3 * 3", "answer=2 * 2 * 3", "input=2 * 2 * 3 * 3"), + Iteration("2 * 3 * 3 * 3!=2 * 2 * 3 * 3", "answer=2 * 3 * 3 * 3", "input=2 * 2 * 3 * 3"), + Iteration( + "2 *(50 + 150) + 2*(100 + 25)!=(50 + 150 + 100 + 25) × 2", + "answer=2 *(50 + 150) + 2*(100 + 25)", + "input=(50 + 150 + 100 + 25) × 2" + ), + Iteration("15 - 12 + 3!=15 - (6 × 2) + 3", "answer=15 - 12 + 3", "input=15 - (6 × 2) + 3"), + Iteration("2*(6+3+4) + 4!=2*(2+6+3+4)", "answer=2*(6+3+4) + 4", "input=2*(2+6+3+4)"), + Iteration("2*(2+6+3) + 8!=2*(2+6+3+4)", "answer=2*(2+6+3) + 8", "input=2*(2+6+3+4)") + ) + fun testMatches_assortedExpressions_withoutMatchingCharacteristics_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // This verifies a variety of expressions that per the PRD and technical specification should + // not pass for this classifier. + assertThat(matches).isFalse() + } + + private fun matchesClassifier( + answerExpression: InteractionObject, + inputExpression: InteractionObject + ): Boolean { + return classifier.matches( + answerExpression, + inputs = mapOf("x" to inputExpression), + writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + ) } private fun createMathExpression(rawExpression: String) = InteractionObject.newBuilder().apply { mathExpression = rawExpression }.build() + private fun setUpTestApplicationComponent() { + DaggerNumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest_TestApplicationComponent + .builder() + .setApplication(ApplicationProvider.getApplicationContext()).build().inject(this) + } + // TODO(#89): Move this to a common test application component. @Module class TestModule { From d20256ce61cdf6807e9ab47f70430b830c256cfa Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 4 Feb 2022 15:29:49 -0800 Subject: [PATCH 097/162] Use more intentional epsilons for float comparing. --- ...icInputEqualsRuleClassifierProviderTest.kt | 10 +++---- .../android/util/math/FloatExtensions.kt | 26 ++++++++++++++----- .../android/util/math/FloatExtensionsTest.kt | 10 +++---- .../android/util/math/RealExtensionsTest.kt | 4 +-- 4 files changed, 32 insertions(+), 18 deletions(-) diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputEqualsRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputEqualsRuleClassifierProviderTest.kt index f7485b13545..506df4acebe 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputEqualsRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputEqualsRuleClassifierProviderTest.kt @@ -12,7 +12,7 @@ import org.junit.runner.RunWith import org.oppia.android.app.model.WrittenTranslationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder import org.oppia.android.testing.assertThrows -import org.oppia.android.util.math.FLOAT_EQUALITY_INTERVAL +import org.oppia.android.util.math.FLOAT_EQUALITY_EPSILON import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode import javax.inject.Inject @@ -33,13 +33,13 @@ class NumericInputEqualsRuleClassifierProviderTest { private val NEGATIVE_REAL_VALUE_3_5 = InteractionObjectTestBuilder.createReal(value = -3.5) private val FIVE_TIMES_FLOAT_EQUALITY_INTERVAL = - InteractionObjectTestBuilder.createReal(value = 5 * FLOAT_EQUALITY_INTERVAL) + InteractionObjectTestBuilder.createReal(value = 5 * FLOAT_EQUALITY_EPSILON) private val SIX_TIMES_FLOAT_EQUALITY_INTERVAL = - InteractionObjectTestBuilder.createReal(value = 6 * FLOAT_EQUALITY_INTERVAL) + InteractionObjectTestBuilder.createReal(value = 6 * FLOAT_EQUALITY_EPSILON) private val FIVE_POINT_ONE_TIMES_FLOAT_EQUALITY_INTERVAL = InteractionObjectTestBuilder.createReal( - value = 5 * FLOAT_EQUALITY_INTERVAL + - FLOAT_EQUALITY_INTERVAL / 10 + value = 5 * FLOAT_EQUALITY_EPSILON + + FLOAT_EQUALITY_EPSILON / 10 ) private val STRING_VALUE = InteractionObjectTestBuilder.createString(value = "test") diff --git a/utility/src/main/java/org/oppia/android/util/math/FloatExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/FloatExtensions.kt index 27ac002c08c..9062dfe6484 100644 --- a/utility/src/main/java/org/oppia/android/util/math/FloatExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/FloatExtensions.kt @@ -2,22 +2,36 @@ package org.oppia.android.util.math import kotlin.math.abs -/** The error margin used for approximately [Float] and [Double] equality checking. */ -const val FLOAT_EQUALITY_INTERVAL = 1e-5 +/** + * The error margin used for approximately [Float] equality checking. + * + * Note that the machine epsilon value from https://en.wikipedia.org/wiki/Machine_epsilon is defined + * defined as the smallest value that, when added to, or subtract from, 1, will result in a value + * that is exactly equal to 1. A slightly larger value is picked here for some allowance in + * variance. + */ +const val FLOAT_EQUALITY_EPSILON: Float = 1e-6f + +/** + * The error margin used for approximately [Double] equality checking. + * + * See [FLOAT_EQUALITY_EPSILON] for an explanation of this value. + */ +const val DOUBLE_EQUALITY_EPSILON: Double = 1e-15 /** * Returns whether this float approximately equals another based on a consistent epsilon value - * ([FLOAT_EQUALITY_INTERVAL]). + * ([FLOAT_EQUALITY_EPSILON]). */ fun Float.approximatelyEquals(other: Float): Boolean { - return abs(this - other) < FLOAT_EQUALITY_INTERVAL + return abs(this - other) < FLOAT_EQUALITY_EPSILON } /** Returns whether this double approximately equals another based on a consistent epsilon value - * ([FLOAT_EQUALITY_INTERVAL]). + * ([DOUBLE_EQUALITY_EPSILON]). */ fun Double.approximatelyEquals(other: Double): Boolean { - return abs(this - other) < FLOAT_EQUALITY_INTERVAL + return abs(this - other) < DOUBLE_EQUALITY_EPSILON } /** diff --git a/utility/src/test/java/org/oppia/android/util/math/FloatExtensionsTest.kt b/utility/src/test/java/org/oppia/android/util/math/FloatExtensionsTest.kt index 6e1896902e6..9f83b544a6d 100644 --- a/utility/src/test/java/org/oppia/android/util/math/FloatExtensionsTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/FloatExtensionsTest.kt @@ -36,7 +36,7 @@ class FloatExtensionsTest { @Test fun testFloat_approximatelyEquals_nonZeroValues_withinInterval_returnsTrue() { val leftFloat = 1.2f - val rightFloat = leftFloat + FLOAT_EQUALITY_INTERVAL.toFloat() / 10f + val rightFloat = leftFloat + FLOAT_EQUALITY_EPSILON / 10f val result = leftFloat.approximatelyEquals(rightFloat) @@ -58,7 +58,7 @@ class FloatExtensionsTest { @Test fun testFloat_approximatelyEquals_nonZeroValues_outsideInterval_returnsFalse() { val leftFloat = 1.2f - val rightFloat = leftFloat + FLOAT_EQUALITY_INTERVAL.toFloat() * 2f + val rightFloat = leftFloat + FLOAT_EQUALITY_EPSILON * 2f val result = leftFloat.approximatelyEquals(rightFloat) @@ -97,8 +97,8 @@ class FloatExtensionsTest { @Test fun testDouble_approximatelyEquals_nonZeroValues_withinInterval_returnsTrue() { - val leftDouble = 1.2 - val rightDouble = leftDouble + FLOAT_EQUALITY_INTERVAL / 10.0 + val leftDouble = 0.2 + val rightDouble = leftDouble + DOUBLE_EQUALITY_EPSILON / 10.0 val result = leftDouble.approximatelyEquals(rightDouble) @@ -110,7 +110,7 @@ class FloatExtensionsTest { @Test fun testDouble_approximatelyEquals_nonZeroValues_outsideInterval_returnsFalse() { val leftDouble = 1.2 - val rightDouble = leftDouble + FLOAT_EQUALITY_INTERVAL * 2 + val rightDouble = leftDouble + DOUBLE_EQUALITY_EPSILON * 2 val result = leftDouble.approximatelyEquals(rightDouble) diff --git a/utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt b/utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt index 2e13da959aa..6efbac11ed0 100644 --- a/utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt @@ -256,14 +256,14 @@ class RealExtensionsTest { @Test fun testIsApproximatelyEqualTo_twoAndTwoWithinThreshold_returnsTrue() { - val result = TWO_REAL.isApproximatelyEqualTo(2.0 + FLOAT_EQUALITY_INTERVAL / 2.0) + val result = TWO_REAL.isApproximatelyEqualTo(2.0 + DOUBLE_EQUALITY_EPSILON / 2.0) assertThat(result).isTrue() } @Test fun testIsApproximatelyEqualTo_twoAndTwoOutsideThreshold_returnsFalse() { - val result = TWO_REAL.isApproximatelyEqualTo(2.0 + FLOAT_EQUALITY_INTERVAL * 2.0) + val result = TWO_REAL.isApproximatelyEqualTo(2.0 + DOUBLE_EQUALITY_EPSILON * 2.0) assertThat(result).isFalse() } From 7e97d0b107294b64b48f510b9b9c325e51238bd3 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 4 Feb 2022 15:39:54 -0800 Subject: [PATCH 098/162] Treat en-dash as a subtraction symbol. --- .../java/org/oppia/android/util/math/MathTokenizer.kt | 2 +- .../org/oppia/android/util/math/MathTokenizerTest.kt | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/utility/src/main/java/org/oppia/android/util/math/MathTokenizer.kt b/utility/src/main/java/org/oppia/android/util/math/MathTokenizer.kt index d45ce14d571..3f378f5de7f 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathTokenizer.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathTokenizer.kt @@ -50,7 +50,7 @@ class MathTokenizer private constructor() { '+' -> tokenizeSymbol(chars) { startIndex, endIndex -> Token.PlusSymbol(startIndex, endIndex) } - '-', '−' -> tokenizeSymbol(chars) { startIndex, endIndex -> + '-', '−', '–' -> tokenizeSymbol(chars) { startIndex, endIndex -> Token.MinusSymbol(startIndex, endIndex) } '*', '×' -> tokenizeSymbol(chars) { startIndex, endIndex -> diff --git a/utility/src/test/java/org/oppia/android/util/math/MathTokenizerTest.kt b/utility/src/test/java/org/oppia/android/util/math/MathTokenizerTest.kt index a91ea971626..0f9abb08db1 100644 --- a/utility/src/test/java/org/oppia/android/util/math/MathTokenizerTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/MathTokenizerTest.kt @@ -369,6 +369,14 @@ class MathTokenizerTest { assertThat(tokens[0]).isMinusSymbol() } + @Test + fun testTokenize_enDashSymbol_producesMinusSymbol() { + val tokens = MathTokenizer.tokenize("–").toList() + + assertThat(tokens).hasSize(1) + assertThat(tokens[0]).isMinusSymbol() + } + @Test fun testTokenize_minusSymbol_withSpaces_tokenHasCorrectIndices() { val tokens = MathTokenizer.tokenize(" − ").toList() @@ -645,7 +653,7 @@ class MathTokenizerTest { // Build a large list of unicode characters minus those which are actually allowed. The ASCII // range is excluded from this list. val characters = ('\u007f'..'\uffff').filterNot { - it in listOf('×', '÷', '−', '√') + it in listOf('×', '÷', '−', '–', '√') } val charStr = characters.joinToString("") From a9c68b1ea76e7b7bb44fa9531b5fdfc48c4a70c3 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 4 Feb 2022 16:02:56 -0800 Subject: [PATCH 099/162] Add explicit platform selection for paramerized. This adds explicit platform selection support rather than it being automatic based on deps. While less flexible for shared tests, this offers better control for tests that don't want to to use Robolectric for local tests. This also adds a JUnit-only test runner, and updates MathTokenizerTest to use it (which led to an almost 40x decrease in runtime). --- .../oppia/android/testing/junit/BUILD.bazel | 14 +++++ .../junit/OppiaParameterizedBaseRunner.kt | 9 ++++ .../junit/OppiaParameterizedTestRunner.kt | 52 ++++++++----------- .../ParameterizedAndroidJUnit4ClassRunner.kt | 8 ++- .../junit/ParameterizedJunitTestRunner.kt | 45 ++++++++++++++++ .../ParameterizedRobolectricTestRunner.kt | 8 ++- .../org/oppia/android/util/math/BUILD.bazel | 2 +- .../android/util/math/MathTokenizerTest.kt | 3 ++ 8 files changed, 107 insertions(+), 34 deletions(-) create mode 100644 testing/src/main/java/org/oppia/android/testing/junit/OppiaParameterizedBaseRunner.kt create mode 100644 testing/src/main/java/org/oppia/android/testing/junit/ParameterizedJunitTestRunner.kt diff --git a/testing/src/main/java/org/oppia/android/testing/junit/BUILD.bazel b/testing/src/main/java/org/oppia/android/testing/junit/BUILD.bazel index d7e6d99dd25..555889937b9 100644 --- a/testing/src/main/java/org/oppia/android/testing/junit/BUILD.bazel +++ b/testing/src/main/java/org/oppia/android/testing/junit/BUILD.bazel @@ -61,6 +61,19 @@ kt_android_library( ], ) +kt_android_library( + name = "parameterized_junit_test_runner", + testonly = True, + srcs = [ + "ParameterizedJunitTestRunner.kt", + ], + visibility = ["//:oppia_testing_visibility"], + deps = [ + ":parameterized_runner_delegate_impl", + "//third_party:junit_junit", + ], +) + kt_android_library( name = "parameterized_robolectric_test_runner", testonly = True, @@ -79,6 +92,7 @@ kt_android_library( name = "parameterized_runner_delegate_impl", testonly = True, srcs = [ + "OppiaParameterizedBaseRunner.kt", "ParameterValue.kt", "ParameterizedMethod.kt", "ParameterizedRunnerDelegate.kt", diff --git a/testing/src/main/java/org/oppia/android/testing/junit/OppiaParameterizedBaseRunner.kt b/testing/src/main/java/org/oppia/android/testing/junit/OppiaParameterizedBaseRunner.kt new file mode 100644 index 00000000000..ff08c985400 --- /dev/null +++ b/testing/src/main/java/org/oppia/android/testing/junit/OppiaParameterizedBaseRunner.kt @@ -0,0 +1,9 @@ +package org.oppia.android.testing.junit + +/** + * This is a marker interface that's used to select a base runner to be used in conjunction with + * [OppiaParameterizedTestRunner]. + * + * See the KDoc for the test runner for more details on how to use this. + */ +interface OppiaParameterizedBaseRunner diff --git a/testing/src/main/java/org/oppia/android/testing/junit/OppiaParameterizedTestRunner.kt b/testing/src/main/java/org/oppia/android/testing/junit/OppiaParameterizedTestRunner.kt index 218dc1258aa..cb67956a553 100644 --- a/testing/src/main/java/org/oppia/android/testing/junit/OppiaParameterizedTestRunner.kt +++ b/testing/src/main/java/org/oppia/android/testing/junit/OppiaParameterizedTestRunner.kt @@ -11,6 +11,7 @@ import org.junit.runners.Suite import java.lang.annotation.Repeatable import java.lang.reflect.Field import java.lang.reflect.Method +import kotlin.reflect.KClass /** * JUnit test runner that enables support for parameterization, that is, running a single test @@ -22,9 +23,9 @@ import java.lang.reflect.Method * use regular explicit tests, instead (since parameterized tests can hurt test maintainability and * readability). * - * This runner behaves like AndroidJUnit4 in that it should work both locally (i.e. via Robolectric) - * and on a device (i.e. with Espresso), though the correct Bazel dependency needs to be added based - * on the environment in which the test is running. + * This runner behaves like AndroidJUnit4 in that it should work in different environments based on + * which base runner is configured using [SelectRunnerPlatform] (which automatically pulls in the + * necessary Bazel dependencies). However, it will only support the platform(s) selected. * * To introduce parameterized tests, add this runner along with one or more [Parameter]-annotated * fields and one or more [RunParameterized]-annotated methods (where each method should have @@ -32,6 +33,7 @@ import java.lang.reflect.Method * * ```kotlin * @RunWith(OppiaParameterizedTestRunner::class) + * @SelectRunnerPlatform(ParameterizedRobolectricTestRunner::class) * class ExampleParameterizedTest { * @Parameter lateinit var parameter: String * @@ -73,14 +75,19 @@ import java.lang.reflect.Method */ class OppiaParameterizedTestRunner(private val testClass: Class<*>) : Suite(testClass, listOf()) { private val parameterizedMethods = computeParameterizedMethods() + private val selectedRunnerClass by lazy { fetchSelectedRunnerPlatformClass() } private val childrenRunners by lazy { // Collect all parameterized methods (for each iteration they support) plus one test runner for // all non-parameterized methods. parameterizedMethods.flatMap { (methodName, method) -> method.iterationNames.map { iterationName -> - ProxyParameterizedTestRunner(testClass, parameterizedMethods, methodName, iterationName) + ProxyParameterizedTestRunner( + selectedRunnerClass, testClass, parameterizedMethods, methodName, iterationName + ) } - } + ProxyParameterizedTestRunner(testClass, parameterizedMethods, methodName = null) + } + ProxyParameterizedTestRunner( + selectedRunnerClass, testClass, parameterizedMethods, methodName = null + ) } override fun getChildren(): MutableList = childrenRunners.toMutableList() @@ -196,6 +203,16 @@ class OppiaParameterizedTestRunner(private val testClass: Class<*>) : Suite(test } } + private fun fetchSelectedRunnerPlatformClass(): Class<*> { + return checkNotNull(testClass.getDeclaredAnnotation(SelectRunnerPlatform::class.java)) { + "All suites using OppiaParameterizedTestRunner must declare their base platform runner" + + " using SelectRunnerPlatform." + }.runnerType.java + } + + @Target(AnnotationTarget.CLASS) + annotation class SelectRunnerPlatform(val runnerType: KClass) + /** * Defines a parameter that may have an injected value that comes from per-test [Iteration] * definitions. @@ -244,6 +261,7 @@ class OppiaParameterizedTestRunner(private val testClass: Class<*>) : Suite(test ) private class ProxyParameterizedTestRunner( + private val runnerClass: Class<*>, private val testClass: Class<*>, private val parameterizedMethods: Map, private val methodName: String?, @@ -269,30 +287,6 @@ class OppiaParameterizedTestRunner(private val testClass: Class<*>) : Suite(test override fun sort(sorter: Sorter?) = delegateSortable.sort(sorter) private fun constructDelegate(): Any { - System.getProperty("android.junit.runner").also { customRunner -> - check(customRunner == null) { - "Detected a custom runner ($customRunner) in a parameterized test. This isn't yet" + - " supported." - } - } - val runningOnAndroid = - System.getProperty("java.runtime.name")?.contains("android", ignoreCase = true) ?: false - - // Load the runner class using reflection since the Robolectric implementation relies on - // Robolectric (which can't be pulled into Espresso builds of shared tests). - val runnerClass = try { - if (runningOnAndroid) { - Class.forName("org.oppia.android.testing.junit.ParameterizedAndroidJUnit4ClassRunner") - } else Class.forName("org.oppia.android.testing.junit.ParameterizedRobolectricTestRunner") - } catch (e: Exception) { - throw IllegalStateException( - "Failed to load delegate test runner class. Did you forget to add either" + - " parameterized_android_junit4_class_runner or parameterized_robolectric_test_runner" + - " as a dependency?", - e - ) - } - val constructor = runnerClass.getConstructor( Class::class.java, Map::class.java, String::class.java, String::class.java diff --git a/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedAndroidJUnit4ClassRunner.kt b/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedAndroidJUnit4ClassRunner.kt index 8526c4b557d..09dded7eff8 100644 --- a/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedAndroidJUnit4ClassRunner.kt +++ b/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedAndroidJUnit4ClassRunner.kt @@ -7,14 +7,18 @@ import org.junit.runners.model.Statement /** * A [AndroidJUnit4ClassRunner] which supports [OppiaParameterizedTestRunner] when running on an * Espresso-driven platform. + * + * This should be selected as the base runner when the test author wishes to use Espresso. */ @Suppress("unused") // This class is constructed using reflection. -internal class ParameterizedAndroidJUnit4ClassRunner( +class ParameterizedAndroidJUnit4ClassRunner internal constructor( testClass: Class<*>, private val parameterizedMethods: Map, private val methodName: String?, private val iterationName: String? -) : AndroidJUnit4ClassRunner(testClass), ParameterizedRunnerOverrideMethods { +) : AndroidJUnit4ClassRunner(testClass), + OppiaParameterizedBaseRunner, + ParameterizedRunnerOverrideMethods { private val delegate by lazy { ParameterizedRunnerDelegate( parameterizedMethods, diff --git a/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedJunitTestRunner.kt b/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedJunitTestRunner.kt new file mode 100644 index 00000000000..8aa5fff8c84 --- /dev/null +++ b/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedJunitTestRunner.kt @@ -0,0 +1,45 @@ +package org.oppia.android.testing.junit + +import org.junit.runners.BlockJUnit4ClassRunner +import org.junit.runners.model.FrameworkMethod +import org.junit.runners.model.Statement + +/** + * A [BlockJUnit4ClassRunner] which supports [OppiaParameterizedTestRunner] when running on a local + * JVM using JUnit directly. + * + * This should be selected as the base runner when the test author wishes to use JUnit without + * Android dependencies. This should **not** be used for Robolectric (i.e. tests that require + * Android libraries) tests; use [ParameterizedRobolectricTestRunner] for those, instead. + * + * The main advantage that this runner provides beyond the Robolectric one is that it avoids + * initializing the Android shadows that Robolectric manages. + */ +@Suppress("unused") // This class is constructed using reflection. +class ParameterizedJunitTestRunner internal constructor( + testClass: Class<*>, + private val parameterizedMethods: Map, + private val methodName: String?, + private val iterationName: String? +) : BlockJUnit4ClassRunner(testClass), + OppiaParameterizedBaseRunner, + ParameterizedRunnerOverrideMethods { + private val delegate by lazy { + ParameterizedRunnerDelegate( + parameterizedMethods, + methodName, + iterationName + ).also { delegate -> + delegate.fetchChildrenFromParent = { super.getChildren() } + delegate.fetchTestNameFromParent = { method -> super.testName(method) } + delegate.fetchMethodInvokerFromParent = { method, test -> super.methodInvoker(method, test) } + } + } + + override fun getChildren(): MutableList = delegate.getChildren() + + override fun testName(method: FrameworkMethod?): String = delegate.testName(method) + + override fun methodInvoker(method: FrameworkMethod?, test: Any?): Statement = + delegate.methodInvoker(method, test) +} diff --git a/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedRobolectricTestRunner.kt b/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedRobolectricTestRunner.kt index 161a4bce0c2..8503933cd98 100644 --- a/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedRobolectricTestRunner.kt +++ b/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedRobolectricTestRunner.kt @@ -7,14 +7,18 @@ import org.robolectric.RobolectricTestRunner /** * A [RobolectricTestRunner] which supports [OppiaParameterizedTestRunner] when running on a local * JVM using Robolectric. + * + * This should be selected as the base runner when the test author wishes to use Robolectric. */ @Suppress("unused") // This class is constructed using reflection. -internal class ParameterizedRobolectricTestRunner( +class ParameterizedRobolectricTestRunner internal constructor( testClass: Class<*>, private val parameterizedMethods: Map, private val methodName: String?, private val iterationName: String? -) : RobolectricTestRunner(testClass), ParameterizedRunnerOverrideMethods { +) : RobolectricTestRunner(testClass), + OppiaParameterizedBaseRunner, + ParameterizedRunnerOverrideMethods { private val delegate by lazy { ParameterizedRunnerDelegate( parameterizedMethods, diff --git a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel index 079bf22babb..eb7b18b4b4f 100644 --- a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel @@ -68,7 +68,7 @@ oppia_android_test( "//model/src/main/proto:math_java_proto_lite", "//testing:assertion_helpers", "//testing/src/main/java/org/oppia/android/testing/junit:oppia_parameterized_test_runner", - "//testing/src/main/java/org/oppia/android/testing/junit:parameterized_robolectric_test_runner", + "//testing/src/main/java/org/oppia/android/testing/junit:parameterized_junit_test_runner", "//testing/src/main/java/org/oppia/android/testing/math:token_subject", "//third_party:com_google_truth_truth", "//third_party:junit_junit", diff --git a/utility/src/test/java/org/oppia/android/util/math/MathTokenizerTest.kt b/utility/src/test/java/org/oppia/android/util/math/MathTokenizerTest.kt index 0f9abb08db1..4cf512cb261 100644 --- a/utility/src/test/java/org/oppia/android/util/math/MathTokenizerTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/MathTokenizerTest.kt @@ -7,6 +7,8 @@ import org.oppia.android.testing.junit.OppiaParameterizedTestRunner import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Iteration import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Parameter import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.RunParameterized +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.SelectRunnerPlatform +import org.oppia.android.testing.junit.ParameterizedJunitTestRunner import org.oppia.android.testing.math.TokenSubject.Companion.assertThat import org.robolectric.annotation.LooperMode @@ -14,6 +16,7 @@ import org.robolectric.annotation.LooperMode // FunctionName: test names are conventionally named with underscores. @Suppress("FunctionName") @RunWith(OppiaParameterizedTestRunner::class) +@SelectRunnerPlatform(ParameterizedJunitTestRunner::class) @LooperMode(LooperMode.Mode.PAUSED) class MathTokenizerTest { @Parameter lateinit var variableName: String From a273ee694b623dee0753ab75f974f97e27038361 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 4 Feb 2022 16:08:27 -0800 Subject: [PATCH 100/162] Exemption fixes. Also, fix name for the AndroidJUnit4 runner. --- scripts/assets/test_file_exemptions.textproto | 4 +++- .../main/java/org/oppia/android/testing/junit/BUILD.bazel | 2 +- .../android/testing/junit/OppiaParameterizedTestRunner.kt | 6 ++++++ ...assRunner.kt => ParameterizedAndroidJunit4TestRunner.kt} | 2 +- 4 files changed, 11 insertions(+), 3 deletions(-) rename testing/src/main/java/org/oppia/android/testing/junit/{ParameterizedAndroidJUnit4ClassRunner.kt => ParameterizedAndroidJunit4TestRunner.kt} (95%) diff --git a/scripts/assets/test_file_exemptions.textproto b/scripts/assets/test_file_exemptions.textproto index db2bbb8edd7..2021184308f 100644 --- a/scripts/assets/test_file_exemptions.textproto +++ b/scripts/assets/test_file_exemptions.textproto @@ -646,8 +646,10 @@ exempted_file_path: "testing/src/main/java/org/oppia/android/testing/espresso/Ge exempted_file_path: "testing/src/main/java/org/oppia/android/testing/espresso/ImageViewMatcher.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/espresso/KonfettiViewMatcher.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/junit/DefineAppLanguageLocaleContext.kt" +exempted_file_path: "testing/src/main/java/org/oppia/android/testing/junit/OppiaParameterizedBaseRunner.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/junit/OppiaParameterizedTestRunner.kt" -exempted_file_path: "testing/src/main/java/org/oppia/android/testing/junit/ParameterizedAndroidJUnit4ClassRunner.kt" +exempted_file_path: "testing/src/main/java/org/oppia/android/testing/junit/ParameterizedAndroidJunit4TestRunner.kt" +exempted_file_path: "testing/src/main/java/org/oppia/android/testing/junit/ParameterizedJunitTestRunner.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/junit/ParameterizedMethod.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/junit/ParameterizedRobolectricTestRunner.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/junit/ParameterizedRunnerDelegate.kt" diff --git a/testing/src/main/java/org/oppia/android/testing/junit/BUILD.bazel b/testing/src/main/java/org/oppia/android/testing/junit/BUILD.bazel index 555889937b9..bfcac07ce7a 100644 --- a/testing/src/main/java/org/oppia/android/testing/junit/BUILD.bazel +++ b/testing/src/main/java/org/oppia/android/testing/junit/BUILD.bazel @@ -51,7 +51,7 @@ kt_android_library( name = "parameterized_android_junit4_class_runner", testonly = True, srcs = [ - "ParameterizedAndroidJUnit4ClassRunner.kt", + "ParameterizedAndroidJunit4TestRunner.kt", ], visibility = ["//:oppia_testing_visibility"], deps = [ diff --git a/testing/src/main/java/org/oppia/android/testing/junit/OppiaParameterizedTestRunner.kt b/testing/src/main/java/org/oppia/android/testing/junit/OppiaParameterizedTestRunner.kt index cb67956a553..6638a801db0 100644 --- a/testing/src/main/java/org/oppia/android/testing/junit/OppiaParameterizedTestRunner.kt +++ b/testing/src/main/java/org/oppia/android/testing/junit/OppiaParameterizedTestRunner.kt @@ -210,6 +210,12 @@ class OppiaParameterizedTestRunner(private val testClass: Class<*>) : Suite(test }.runnerType.java } + /** + * Defines which [OppiaParameterizedBaseRunner] should be used for running individual + * parameterized and non-parameterized test cases. + * + * See base classes for options. + */ @Target(AnnotationTarget.CLASS) annotation class SelectRunnerPlatform(val runnerType: KClass) diff --git a/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedAndroidJUnit4ClassRunner.kt b/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedAndroidJunit4TestRunner.kt similarity index 95% rename from testing/src/main/java/org/oppia/android/testing/junit/ParameterizedAndroidJUnit4ClassRunner.kt rename to testing/src/main/java/org/oppia/android/testing/junit/ParameterizedAndroidJunit4TestRunner.kt index 09dded7eff8..8ba4a5be2df 100644 --- a/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedAndroidJUnit4ClassRunner.kt +++ b/testing/src/main/java/org/oppia/android/testing/junit/ParameterizedAndroidJunit4TestRunner.kt @@ -11,7 +11,7 @@ import org.junit.runners.model.Statement * This should be selected as the base runner when the test author wishes to use Espresso. */ @Suppress("unused") // This class is constructed using reflection. -class ParameterizedAndroidJUnit4ClassRunner internal constructor( +class ParameterizedAndroidJunit4TestRunner internal constructor( testClass: Class<*>, private val parameterizedMethods: Map, private val methodName: String?, From 09e2aad0b81827cd6566f0883dc76ff81d0653da Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 4 Feb 2022 16:42:14 -0800 Subject: [PATCH 101/162] Remove failing test. --- ...icInputEqualsRuleClassifierProviderTest.kt | 25 +++---------------- 1 file changed, 4 insertions(+), 21 deletions(-) diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputEqualsRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputEqualsRuleClassifierProviderTest.kt index 506df4acebe..ae70c8badc0 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputEqualsRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericinput/NumericInputEqualsRuleClassifierProviderTest.kt @@ -12,7 +12,7 @@ import org.junit.runner.RunWith import org.oppia.android.app.model.WrittenTranslationContext import org.oppia.android.domain.classify.InteractionObjectTestBuilder import org.oppia.android.testing.assertThrows -import org.oppia.android.util.math.FLOAT_EQUALITY_EPSILON +import org.oppia.android.util.math.DOUBLE_EQUALITY_EPSILON import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode import javax.inject.Inject @@ -33,13 +33,11 @@ class NumericInputEqualsRuleClassifierProviderTest { private val NEGATIVE_REAL_VALUE_3_5 = InteractionObjectTestBuilder.createReal(value = -3.5) private val FIVE_TIMES_FLOAT_EQUALITY_INTERVAL = - InteractionObjectTestBuilder.createReal(value = 5 * FLOAT_EQUALITY_EPSILON) - private val SIX_TIMES_FLOAT_EQUALITY_INTERVAL = - InteractionObjectTestBuilder.createReal(value = 6 * FLOAT_EQUALITY_EPSILON) + InteractionObjectTestBuilder.createReal(value = 5 * DOUBLE_EQUALITY_EPSILON) private val FIVE_POINT_ONE_TIMES_FLOAT_EQUALITY_INTERVAL = InteractionObjectTestBuilder.createReal( - value = 5 * FLOAT_EQUALITY_EPSILON + - FLOAT_EQUALITY_EPSILON / 10 + value = 5 * DOUBLE_EQUALITY_EPSILON + + DOUBLE_EQUALITY_EPSILON / 10 ) private val STRING_VALUE = InteractionObjectTestBuilder.createString(value = "test") @@ -142,21 +140,6 @@ class NumericInputEqualsRuleClassifierProviderTest { assertThat(matches).isFalse() } - @Test - fun testPositiveRealAnswer_positiveRealInput_valueAtRange_valuesDoNotMatch() { - val inputs = mapOf( - "x" to FIVE_TIMES_FLOAT_EQUALITY_INTERVAL - ) - - val matches = inputEqualsRuleClassifier.matches( - answer = SIX_TIMES_FLOAT_EQUALITY_INTERVAL, - inputs = inputs, - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() - ) - - assertThat(matches).isFalse() - } - @Test fun testRealAnswer_missingInput_throwsException() { val inputs = mapOf("y" to POSITIVE_REAL_VALUE_1_5) From b48d06a78b3ed487201b62070d34fc8ce889cfd2 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 4 Feb 2022 17:00:29 -0800 Subject: [PATCH 102/162] Fix unary expression precedence. Also, use ParameterizedJunitTestRunner for MathExpressionParserTest. --- .../android/util/math/MathExpressionParser.kt | 8 +- .../math/AlgebraicExpressionParserTest.kt | 66 ++++++------- .../org/oppia/android/util/math/BUILD.bazel | 2 +- .../util/math/MathExpressionParserTest.kt | 3 + .../util/math/NumericExpressionParserTest.kt | 97 ++++++++++++++++--- 5 files changed, 123 insertions(+), 53 deletions(-) diff --git a/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt b/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt index 20b44cee192..c04120a7d02 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathExpressionParser.kt @@ -453,9 +453,9 @@ class MathExpressionParser private constructor(private val parseContext: ParseCo } private fun parseGenericNegatedTerm(): MathParsingResult { - // generic_negated_term = minus_operator , generic_mult_div_expression ; + // generic_negated_term = minus_operator , generic_exp_expression ; val minusResult = parseContext.consumeTokenOfType() - val expResult = minusResult.flatMap { parseGenericMultDivExpression() } + val expResult = minusResult.flatMap { parseGenericExpExpression() } return minusResult.combineWith(expResult) { minus, op -> MathExpression.newBuilder().apply { parseStartIndex = minus.startIndex @@ -469,9 +469,9 @@ class MathExpressionParser private constructor(private val parseContext: ParseCo } private fun parseGenericPositiveTerm(): MathParsingResult { - // generic_positive_term = plus_operator , generic_mult_div_expression ; + // generic_positive_term = plus_operator , generic_exp_expression ; val plusResult = parseContext.consumeTokenOfType() - val expResult = plusResult.flatMap { parseGenericMultDivExpression() } + val expResult = plusResult.flatMap { parseGenericExpExpression() } return plusResult.combineWith(expResult) { plus, op -> MathExpression.newBuilder().apply { parseStartIndex = plus.startIndex diff --git a/utility/src/test/java/org/oppia/android/util/math/AlgebraicExpressionParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/AlgebraicExpressionParserTest.kt index be190a7a520..3c327a362cc 100644 --- a/utility/src/test/java/org/oppia/android/util/math/AlgebraicExpressionParserTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/AlgebraicExpressionParserTest.kt @@ -488,19 +488,19 @@ class AlgebraicExpressionParserTest { // Similar to the previous test, but this ensures negation ordering (relative to variables & // implicit multiplication). assertThat(expression).hasStructureThatMatches { - negation { - operand { - multiplication(isImplicit = true) { - leftOperand { + multiplication(isImplicit = true) { + leftOperand { + negation { + operand { constant { withValueThat().isIntegerThat().isEqualTo(2) } } - rightOperand { - variable { - withNameThat().isEqualTo("x") - } - } + } + } + rightOperand { + variable { + withNameThat().isEqualTo("x") } } } @@ -687,41 +687,41 @@ class AlgebraicExpressionParserTest { // This combines all of the distinct pieces tested earlier to demonstrate full polynomial // syntax. assertThat(expression).hasStructureThatMatches { - // -8x^3-7.4x^2+x-12√2 -> (((-(8*(x^3))) - (7.4*(x^2))) + x) - (12/√2) + // -8x^3-7.4x^2+x-12/√2 -> ((((-8) * (x^3)) - (7.4 * (x^2))) + x) - (12/√2) subtraction { leftOperand { - // ((-(8*(x^3))) - (7.4*(x^2))) + x + // (((-8) * (x^3)) - (7.4 * (x^2))) + x addition { leftOperand { - // (-(8*(x^3))) - (7.4*(x^2)) + // ((-8) * (x^3)) - (7.4 * (x^2)) subtraction { leftOperand { - // -(8*(x^3)) - negation { - operand { - // 8*(x^3) - multiplication(isImplicit = true) { - leftOperand { + // (-8) * (x^3) + multiplication(isImplicit = true) { + leftOperand { + // -8 + negation { + operand { // 8 constant { withValueThat().isIntegerThat().isEqualTo(8) } } + } + } + rightOperand { + // x^3 + exponentiation { + leftOperand { + // x + variable { + withNameThat().isEqualTo("x") + } + } rightOperand { - // x^3 - exponentiation { - leftOperand { - // x - variable { - withNameThat().isEqualTo("x") - } - } - rightOperand { - // 3 - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } + // 3 + constant { + withValueThat().isIntegerThat().isEqualTo(3) } } } @@ -729,7 +729,7 @@ class AlgebraicExpressionParserTest { } } rightOperand { - // 7.4*(x^2) + // 7.4 * (x^2) multiplication(isImplicit = true) { leftOperand { // 7.4 diff --git a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel index 063039f99d1..217f16638ab 100644 --- a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel @@ -105,7 +105,7 @@ oppia_android_test( deps = [ "//model/src/main/proto:math_java_proto_lite", "//testing/src/main/java/org/oppia/android/testing/junit:oppia_parameterized_test_runner", - "//testing/src/main/java/org/oppia/android/testing/junit:parameterized_robolectric_test_runner", + "//testing/src/main/java/org/oppia/android/testing/junit:parameterized_junit_test_runner", "//testing/src/main/java/org/oppia/android/testing/math:math_parsing_error_subject", "//third_party:com_google_truth_extensions_truth-liteproto-extension", "//third_party:com_google_truth_truth", diff --git a/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt index 198d24551b4..4a00234fb76 100644 --- a/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt @@ -14,6 +14,8 @@ import org.oppia.android.testing.junit.OppiaParameterizedTestRunner import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Iteration import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Parameter import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.RunParameterized +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.SelectRunnerPlatform +import org.oppia.android.testing.junit.ParameterizedJunitTestRunner import org.oppia.android.testing.math.MathParsingErrorSubject.Companion.assertThat import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode.ALL_ERRORS @@ -40,6 +42,7 @@ import org.robolectric.annotation.LooperMode // SameParameterValue: tests should have specific context included/excluded for readability. @Suppress("FunctionName", "SameParameterValue") @RunWith(OppiaParameterizedTestRunner::class) +@SelectRunnerPlatform(ParameterizedJunitTestRunner::class) @LooperMode(LooperMode.Mode.PAUSED) class MathExpressionParserTest { @Parameter lateinit var lhsOp: String diff --git a/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt index 93628887ae7..c645fcd1636 100644 --- a/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt @@ -413,7 +413,7 @@ class NumericExpressionParserTest { fun testParse_negationAndExponentiation_returnsExpWithNegationResolvedLast() { val expression = parseNumericExpressionWithAllErrors("-3^4") - // Exponentiation is resolved first since negation is lower precedent. + // Exponentiation is resolved first since negation is higher precedence. assertThat(expression).hasStructureThatMatches { negation { operand { @@ -434,6 +434,74 @@ class NumericExpressionParserTest { } } + @Test + fun testParse_exponentiationAndNegatedMultiplication_returnsExpWithMultiplicationResolvedLast() { + val expression = parseNumericExpressionWithAllErrors("10^-5*3") + + // Negation is isolated since multiplication is higher precedence. + assertThat(expression).hasStructureThatMatches { + multiplication { + leftOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(10) + } + } + rightOperand { + negation { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + } + } + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + + @Test + fun testParse_exponentiationAndPositiveMultiplication_returnsExpWithMultiplicationResolvedLast() { + val expression = parseNumericExpressionWithoutOptionalErrors("10^+5*3") + + // Positive is isolated since multiplication is higher precedence. + assertThat(expression).hasStructureThatMatches { + multiplication { + leftOperand { + exponentiation { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(10) + } + } + rightOperand { + positive { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + } + } + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + @Test fun testParse_inlineSquareRootAndExponentiation_returnsExpWithSquareRootResolvedFirst() { val expression = parseNumericExpressionWithAllErrors("√3^4") @@ -1364,25 +1432,24 @@ class NumericExpressionParserTest { fun testParse_multiplicationOfNegations_returnsExpWithCorrectStructure() { val expression = parseNumericExpressionWithAllErrors("-2*-3") - // Note that the following structure is not the same as (-2)*(-2) since unary negation has - // lower precedence than multiplication, so it's computed as first with its operand being the - // multiplication expression. + // Note that the following structure is the same as (-2)*(-2) since unary negation has higher + // precedence than multiplication. assertThat(expression).hasStructureThatMatches { - negation { - operand { - multiplication { - leftOperand { + multiplication { + leftOperand { + negation { + operand { constant { withValueThat().isIntegerThat().isEqualTo(2) } } - rightOperand { - negation { - operand { - constant { - withValueThat().isIntegerThat().isEqualTo(3) - } - } + } + } + rightOperand { + negation { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) } } } From 8ac22cb7eec13d5a14e36e7fd2c62f35561747bf Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 4 Feb 2022 17:05:08 -0800 Subject: [PATCH 103/162] Fixes & add more test cases. --- .../util/math/MathExpressionParserTest.kt | 41 ++++++++++++------- .../util/math/NumericExpressionParserTest.kt | 20 +++++++++ 2 files changed, 46 insertions(+), 15 deletions(-) diff --git a/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt index 4a00234fb76..89e14517e5e 100644 --- a/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt @@ -45,11 +45,16 @@ import org.robolectric.annotation.LooperMode @SelectRunnerPlatform(ParameterizedJunitTestRunner::class) @LooperMode(LooperMode.Mode.PAUSED) class MathExpressionParserTest { - @Parameter lateinit var lhsOp: String - @Parameter lateinit var rhsOp: String - @Parameter lateinit var binOp: String - @Parameter lateinit var subExp: String - @Parameter lateinit var func: String + @Parameter + lateinit var lhsOp: String + @Parameter + lateinit var rhsOp: String + @Parameter + lateinit var binOp: String + @Parameter + lateinit var subExp: String + @Parameter + lateinit var func: String @Test fun testParseNumExp_basicExpression_doesNotFail() { @@ -546,22 +551,25 @@ class MathExpressionParserTest { Iteration("/*", "lhsOp=/", "rhsOp=*"), Iteration("÷*", "lhsOp=÷", "rhsOp=*"), Iteration("^*", "lhsOp=^", "rhsOp=*"), Iteration("+*", "lhsOp=+", "rhsOp=*"), Iteration("-*", "lhsOp=-", "rhsOp=*"), Iteration("−*", "lhsOp=−", "rhsOp=*"), - Iteration("*×", "lhsOp=*", "rhsOp=×"), Iteration("××", "lhsOp=×", "rhsOp=×"), - Iteration("/×", "lhsOp=/", "rhsOp=×"), Iteration("÷×", "lhsOp=÷", "rhsOp=×"), - Iteration("^×", "lhsOp=^", "rhsOp=×"), Iteration("+×", "lhsOp=+", "rhsOp=×"), - Iteration("-×", "lhsOp=-", "rhsOp=×"), Iteration("−×", "lhsOp=−", "rhsOp=×"), + Iteration("–*", "lhsOp=–", "rhsOp=*"), Iteration("*×", "lhsOp=*", "rhsOp=×"), + Iteration("××", "lhsOp=×", "rhsOp=×"), Iteration("/×", "lhsOp=/", "rhsOp=×"), + Iteration("÷×", "lhsOp=÷", "rhsOp=×"), Iteration("^×", "lhsOp=^", "rhsOp=×"), + Iteration("+×", "lhsOp=+", "rhsOp=×"), Iteration("-×", "lhsOp=-", "rhsOp=×"), + Iteration("−×", "lhsOp=−", "rhsOp=×"), Iteration("–×", "lhsOp=–", "rhsOp=×"), Iteration("*/", "lhsOp=*", "rhsOp=/"), Iteration("×/", "lhsOp=×", "rhsOp=/"), Iteration("//", "lhsOp=/", "rhsOp=/"), Iteration("÷/", "lhsOp=÷", "rhsOp=/"), Iteration("^/", "lhsOp=^", "rhsOp=/"), Iteration("+/", "lhsOp=+", "rhsOp=/"), Iteration("-/", "lhsOp=-", "rhsOp=/"), Iteration("−/", "lhsOp=−", "rhsOp=/"), - Iteration("*÷", "lhsOp=*", "rhsOp=÷"), Iteration("×÷", "lhsOp=×", "rhsOp=÷"), - Iteration("/÷", "lhsOp=/", "rhsOp=÷"), Iteration("÷÷", "lhsOp=÷", "rhsOp=÷"), - Iteration("^÷", "lhsOp=^", "rhsOp=÷"), Iteration("+÷", "lhsOp=+", "rhsOp=÷"), - Iteration("-÷", "lhsOp=-", "rhsOp=÷"), Iteration("−÷", "lhsOp=−", "rhsOp=÷"), + Iteration("–/", "lhsOp=–", "rhsOp=/"), Iteration("*÷", "lhsOp=*", "rhsOp=÷"), + Iteration("×÷", "lhsOp=×", "rhsOp=÷"), Iteration("/÷", "lhsOp=/", "rhsOp=÷"), + Iteration("÷÷", "lhsOp=÷", "rhsOp=÷"), Iteration("^÷", "lhsOp=^", "rhsOp=÷"), + Iteration("+÷", "lhsOp=+", "rhsOp=÷"), Iteration("-÷", "lhsOp=-", "rhsOp=÷"), + Iteration("−÷", "lhsOp=−", "rhsOp=÷"), Iteration("–÷", "lhsOp=–", "rhsOp=÷"), Iteration("*^", "lhsOp=*", "rhsOp=^"), Iteration("×^", "lhsOp=×", "rhsOp=^"), Iteration("/^", "lhsOp=/", "rhsOp=^"), Iteration("÷^", "lhsOp=÷", "rhsOp=^"), Iteration("^^", "lhsOp=^", "rhsOp=^"), Iteration("+^", "lhsOp=+", "rhsOp=^"), - Iteration("-^", "lhsOp=-", "rhsOp=^"), Iteration("−^", "lhsOp=−", "rhsOp=^") + Iteration("-^", "lhsOp=-", "rhsOp=^"), Iteration("−^", "lhsOp=−", "rhsOp=^"), + Iteration("–^", "lhsOp=–", "rhsOp=^") ) fun testParseNumExp_adjacentBinaryOps_returnsSubsequentBinaryOperatorsErrorWithDetails() { val expression = "1 $lhsOp$rhsOp 2" @@ -696,6 +704,7 @@ class MathExpressionParserTest { Iteration("something_to_power_of_nothing", "binOp=^"), Iteration("something_adds_nothing", "binOp=+"), Iteration("something_subtracts_nothing_hyphen", "binOp=-"), + Iteration("something_subtracts_nothing_en_dash", "binOp=–"), Iteration("something_subtracts_nothing", "binOp=−") ) fun testParseNumExp_binaryOps_noRightValue_returnsNoVarOrNumAfterBinOperatorErrorWithDetails() { @@ -720,6 +729,7 @@ class MathExpressionParserTest { Iteration("something_to_power_of_nothing", "binOp=^"), Iteration("something_adds_nothing", "binOp=+"), Iteration("something_subtracts_nothing_hyphen", "binOp=-"), + Iteration("something_subtracts_nothing_en_dash", "binOp=–"), Iteration("something_subtracts_nothing", "binOp=−") ) fun testParseAlgExp_binaryOps_noRightValue_returnsNoVarOrNumAfterBinOperatorErrorWithDetails() { @@ -1086,7 +1096,8 @@ class MathExpressionParserTest { "^" to EXPONENTIATE, "+" to ADD, "-" to SUBTRACT, - "−" to SUBTRACT + "−" to SUBTRACT, + "–" to SUBTRACT ) private val LOWERCASE_LATIN_ALPHABET = listOf( "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", diff --git a/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt index c645fcd1636..16933425c25 100644 --- a/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt @@ -129,6 +129,26 @@ class NumericExpressionParserTest { } } + @Test + fun testParse_subtraction_withEnDashSymbol_returnsExpressionWithBinaryOperation() { + val expression = parseNumericExpressionWithAllErrors("1 – 2") + + assertThat(expression).hasStructureThatMatches { + subtraction { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(1) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + } + } + } + @Test fun testParse_multiplication_returnsExpressionWithBinaryOperation() { val expression = parseNumericExpressionWithAllErrors("1 * 2") From 476e6043191985e7342d17241a97a5b421c76d15 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 4 Feb 2022 17:15:23 -0800 Subject: [PATCH 104/162] Post-merge fixes & test changes. Also, update RealExtensionsTest to use the faster JUnit runner. --- .../src/test/java/org/oppia/android/util/math/BUILD.bazel | 2 +- .../android/util/math/NumericExpressionParserTest.kt | 8 ++++++++ .../org/oppia/android/util/math/RealExtensionsTest.kt | 3 +++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel index 625bb3e5e12..56d3e6ab02c 100644 --- a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel @@ -275,7 +275,7 @@ oppia_android_test( "//model/src/main/proto:math_java_proto_lite", "//testing", "//testing/src/main/java/org/oppia/android/testing/junit:oppia_parameterized_test_runner", - "//testing/src/main/java/org/oppia/android/testing/junit:parameterized_robolectric_test_runner", + "//testing/src/main/java/org/oppia/android/testing/junit:parameterized_junit_test_runner", "//testing/src/main/java/org/oppia/android/testing/math:real_subject", "//third_party:com_google_truth_truth", "//third_party:junit_junit", diff --git a/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt index d6241fe75ae..37e89515be3 100644 --- a/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/NumericExpressionParserTest.kt @@ -160,6 +160,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression).evaluatesToIntegerThat().isEqualTo(-1) } @Test @@ -526,6 +527,12 @@ class NumericExpressionParserTest { } } } + assertThat(expression).evaluatesToRationalThat().apply { + hasNegativePropertyThat().isFalse() + hasWholeNumberThat().isEqualTo(0) + hasNumeratorThat().isEqualTo(3) + hasDenominatorThat().isEqualTo(100000) + } } @Test @@ -560,6 +567,7 @@ class NumericExpressionParserTest { } } } + assertThat(expression).evaluatesToIntegerThat().isEqualTo(300000) } @Test diff --git a/utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt b/utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt index a218f852769..744784c355b 100644 --- a/utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt @@ -10,6 +10,8 @@ import org.oppia.android.testing.junit.OppiaParameterizedTestRunner import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Iteration import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Parameter import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.RunParameterized +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.SelectRunnerPlatform +import org.oppia.android.testing.junit.ParameterizedJunitTestRunner import org.oppia.android.testing.math.RealSubject.Companion.assertThat import org.robolectric.annotation.LooperMode @@ -17,6 +19,7 @@ import org.robolectric.annotation.LooperMode // FunctionName: test names are conventionally named with underscores. @Suppress("FunctionName") @RunWith(OppiaParameterizedTestRunner::class) +@SelectRunnerPlatform(ParameterizedJunitTestRunner::class) @LooperMode(LooperMode.Mode.PAUSED) class RealExtensionsTest { private companion object { From 5ed0013ee47b517ad8e091b455ed366f0d4acc44 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 4 Feb 2022 17:33:22 -0800 Subject: [PATCH 105/162] Use utility directly in LaTeX tests. --- .../org/oppia/android/util/math/BUILD.bazel | 3 ++ .../org/oppia/android/util/math/BUILD.bazel | 2 +- .../math/ExpressionToLatexConverterTest.kt | 43 ++++++++++--------- 3 files changed, 26 insertions(+), 22 deletions(-) diff --git a/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel b/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel index 61cb0d11d08..4ae833ce0b7 100644 --- a/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/main/java/org/oppia/android/util/math/BUILD.bazel @@ -156,6 +156,9 @@ kt_android_library( srcs = [ "ExpressionToLatexConverter.kt", ], + visibility = [ + "//:oppia_testing_visibility", + ], deps = [ ":real_extensions", "//model/src/main/proto:math_java_proto_lite", diff --git a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel index 56d3e6ab02c..7021569bf26 100644 --- a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel @@ -56,7 +56,7 @@ oppia_android_test( "//third_party:junit_junit", "//third_party:org_robolectric_robolectric", "//third_party:robolectric_android-all", - "//utility/src/main/java/org/oppia/android/util/math:extensions", + "//utility/src/main/java/org/oppia/android/util/math:expression_to_latex_converter", "//utility/src/main/java/org/oppia/android/util/math:math_expression_parser", ], ) diff --git a/utility/src/test/java/org/oppia/android/util/math/ExpressionToLatexConverterTest.kt b/utility/src/test/java/org/oppia/android/util/math/ExpressionToLatexConverterTest.kt index d9125d519a5..e351f9fc6d9 100644 --- a/utility/src/test/java/org/oppia/android/util/math/ExpressionToLatexConverterTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/ExpressionToLatexConverterTest.kt @@ -6,6 +6,7 @@ import org.junit.Test import org.junit.runner.RunWith import org.oppia.android.app.model.MathEquation import org.oppia.android.app.model.MathExpression +import org.oppia.android.util.math.ExpressionToLatexConverter.Companion.convertToLatex import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult import org.robolectric.annotation.LooperMode @@ -21,7 +22,7 @@ class ExpressionToLatexConverterTest { fun testConvert_numericExp_number_returnsConstantLatex() { val exp = parseNumericExpressionWithAllErrors("1") - val latex = exp.toRawLatex(divAsFraction = false) + val latex = exp.convertToLatex(divAsFraction = false) assertThat(latex).isEqualTo("1") } @@ -30,7 +31,7 @@ class ExpressionToLatexConverterTest { fun testConvert_numericExp_unaryPlus_withoutOptionalErrors_returnLatexWithUnaryPlus() { val exp = parseNumericExpressionWithoutOptionalErrors("+1") - val latex = exp.toRawLatex(divAsFraction = false) + val latex = exp.convertToLatex(divAsFraction = false) assertThat(latex).isEqualTo("+1") } @@ -39,7 +40,7 @@ class ExpressionToLatexConverterTest { fun testConvert_numericExp_unaryMinus_returnLatexWithUnaryMinus() { val exp = parseNumericExpressionWithAllErrors("-1") - val latex = exp.toRawLatex(divAsFraction = false) + val latex = exp.convertToLatex(divAsFraction = false) assertThat(latex).isEqualTo("-1") } @@ -48,7 +49,7 @@ class ExpressionToLatexConverterTest { fun testConvert_numericExp_addition_returnsLatexWithAddition() { val exp = parseNumericExpressionWithAllErrors("1+2") - val latex = exp.toRawLatex(divAsFraction = false) + val latex = exp.convertToLatex(divAsFraction = false) assertThat(latex).isEqualTo("1 + 2") } @@ -57,7 +58,7 @@ class ExpressionToLatexConverterTest { fun testConvert_numericExp_subtraction_returnsLatexWithSubtract() { val exp = parseNumericExpressionWithAllErrors("1-2") - val latex = exp.toRawLatex(divAsFraction = false) + val latex = exp.convertToLatex(divAsFraction = false) assertThat(latex).isEqualTo("1 - 2") } @@ -66,7 +67,7 @@ class ExpressionToLatexConverterTest { fun testConvert_numericExp_multiplication_returnsLatexWithMultiplication() { val exp = parseNumericExpressionWithAllErrors("2*3") - val latex = exp.toRawLatex(divAsFraction = false) + val latex = exp.convertToLatex(divAsFraction = false) assertThat(latex).isEqualTo("2 \\times 3") } @@ -75,7 +76,7 @@ class ExpressionToLatexConverterTest { fun testConvert_numericExp_division_returnsLatexWithDivision() { val exp = parseNumericExpressionWithAllErrors("2/3") - val latex = exp.toRawLatex(divAsFraction = false) + val latex = exp.convertToLatex(divAsFraction = false) assertThat(latex).isEqualTo("2 \\div 3") } @@ -84,7 +85,7 @@ class ExpressionToLatexConverterTest { fun testConvert_numericExp_division_divAsFraction_returnsLatexWithFraction() { val exp = parseNumericExpressionWithAllErrors("2/3") - val latex = exp.toRawLatex(divAsFraction = true) + val latex = exp.convertToLatex(divAsFraction = true) assertThat(latex).isEqualTo("\\frac{2}{3}") } @@ -93,7 +94,7 @@ class ExpressionToLatexConverterTest { fun testConvert_numericExp_multipleDivisions_divAsFraction_returnsLatexWithFractions() { val exp = parseNumericExpressionWithAllErrors("2/3/4") - val latex = exp.toRawLatex(divAsFraction = true) + val latex = exp.convertToLatex(divAsFraction = true) assertThat(latex).isEqualTo("\\frac{\\frac{2}{3}}{4}") } @@ -102,7 +103,7 @@ class ExpressionToLatexConverterTest { fun testConvert_numericExp_exponent_returnsLatexWithExponent() { val exp = parseNumericExpressionWithAllErrors("2^3") - val latex = exp.toRawLatex(divAsFraction = false) + val latex = exp.convertToLatex(divAsFraction = false) assertThat(latex).isEqualTo("2 ^ {3}") } @@ -111,7 +112,7 @@ class ExpressionToLatexConverterTest { fun testConvert_numericExp_inlineSquareRoot_returnsLatexWithSquareRoot() { val exp = parseNumericExpressionWithAllErrors("√2") - val latex = exp.toRawLatex(divAsFraction = false) + val latex = exp.convertToLatex(divAsFraction = false) assertThat(latex).isEqualTo("\\sqrt{2}") } @@ -120,7 +121,7 @@ class ExpressionToLatexConverterTest { fun testConvert_numericExp_inlineSquareRoot_operationArg_returnsLatexWithSquareRoot() { val exp = parseNumericExpressionWithAllErrors("√(1+2)") - val latex = exp.toRawLatex(divAsFraction = false) + val latex = exp.convertToLatex(divAsFraction = false) assertThat(latex).isEqualTo("\\sqrt{(1 + 2)}") } @@ -129,7 +130,7 @@ class ExpressionToLatexConverterTest { fun testConvert_numericExp_squareRoot_returnsLatexWithSquareRoot() { val exp = parseNumericExpressionWithAllErrors("sqrt(2)") - val latex = exp.toRawLatex(divAsFraction = false) + val latex = exp.convertToLatex(divAsFraction = false) assertThat(latex).isEqualTo("\\sqrt{2}") } @@ -138,7 +139,7 @@ class ExpressionToLatexConverterTest { fun testConvert_numericExp_squareRoot_operationArg_returnsLatexWithSquareRoot() { val exp = parseNumericExpressionWithAllErrors("sqrt(1+2)") - val latex = exp.toRawLatex(divAsFraction = false) + val latex = exp.convertToLatex(divAsFraction = false) assertThat(latex).isEqualTo("\\sqrt{1 + 2}") } @@ -147,7 +148,7 @@ class ExpressionToLatexConverterTest { fun testConvert_numericExp_parentheses_returnsLatexWithGroup() { val exp = parseNumericExpressionWithAllErrors("2/(3+4)") - val latex = exp.toRawLatex(divAsFraction = false) + val latex = exp.convertToLatex(divAsFraction = false) assertThat(latex).isEqualTo("2 \\div (3 + 4)") } @@ -156,7 +157,7 @@ class ExpressionToLatexConverterTest { fun testConvert_numericExp_exponentToGroup_returnsCorrectlyWrappedLatex() { val exp = parseNumericExpressionWithAllErrors("2^(7-3)") - val latex = exp.toRawLatex(divAsFraction = false) + val latex = exp.convertToLatex(divAsFraction = false) assertThat(latex).isEqualTo("2 ^ {(7 - 3)}") } @@ -165,7 +166,7 @@ class ExpressionToLatexConverterTest { fun testConvert_algebraicExp_variable_returnsVariableLatex() { val exp = parseAlgebraicExpressionWithAllErrors("x") - val latex = exp.toRawLatex(divAsFraction = false) + val latex = exp.convertToLatex(divAsFraction = false) assertThat(latex).isEqualTo("x") } @@ -174,7 +175,7 @@ class ExpressionToLatexConverterTest { fun testConvert_algebraicExp_twoX_returnsLatexWithImplicitMultiplication() { val exp = parseAlgebraicExpressionWithAllErrors("2x") - val latex = exp.toRawLatex(divAsFraction = false) + val latex = exp.convertToLatex(divAsFraction = false) assertThat(latex).isEqualTo("2x") } @@ -183,7 +184,7 @@ class ExpressionToLatexConverterTest { fun testConvert_algebraicEq_xEqualsOne_returnsLatexWithEquals() { val exp = parseAlgebraicEquationWithAllErrors("x=1") - val latex = exp.toRawLatex(divAsFraction = false) + val latex = exp.convertToLatex(divAsFraction = false) assertThat(latex).isEqualTo("x = 1") } @@ -192,7 +193,7 @@ class ExpressionToLatexConverterTest { fun testConvert_algebraicEq_complexExpression_returnsCorrectLatex() { val exp = parseAlgebraicEquationWithAllErrors("(x+1)(x-2)=(x^3+2x^2-5x-6)/(x+3)") - val latex = exp.toRawLatex(divAsFraction = false) + val latex = exp.convertToLatex(divAsFraction = false) assertThat(latex).isEqualTo("(x + 1)(x - 2) = (x ^ {3} + 2x ^ {2} - 5x - 6) \\div (x + 3)") } @@ -201,7 +202,7 @@ class ExpressionToLatexConverterTest { fun testConvert_algebraicEq_complexExpression_divAsFraction_returnsCorrectLatex() { val exp = parseAlgebraicEquationWithAllErrors("(x+1)(x-2)=(x^3+2x^2-5x-6)/(x+3)") - val latex = exp.toRawLatex(divAsFraction = true) + val latex = exp.convertToLatex(divAsFraction = true) assertThat(latex).isEqualTo("(x + 1)(x - 2) = \\frac{(x ^ {3} + 2x ^ {2} - 5x - 6)}{(x + 3)}") } From c16db644bf3e13a8cbbdacb02b09996651ce3fd2 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 4 Feb 2022 17:40:20 -0800 Subject: [PATCH 106/162] Post-merge fixes. Also, update ExpressionToComparableOperationConverterTest to use the fast JUnit-only runner. --- utility/src/test/java/org/oppia/android/util/math/BUILD.bazel | 2 +- .../util/math/ExpressionToComparableOperationConverterTest.kt | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel index 2f80dd6025b..4ba0812e29f 100644 --- a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel @@ -68,7 +68,7 @@ oppia_android_test( deps = [ "//model/src/main/proto:math_java_proto_lite", "//testing/src/main/java/org/oppia/android/testing/junit:oppia_parameterized_test_runner", - "//testing/src/main/java/org/oppia/android/testing/junit:parameterized_robolectric_test_runner", + "//testing/src/main/java/org/oppia/android/testing/junit:parameterized_junit_test_runner", "//testing/src/main/java/org/oppia/android/testing/math:comparable_operation_subject", "//third_party:com_google_truth_truth", "//third_party:junit_junit", diff --git a/utility/src/test/java/org/oppia/android/util/math/ExpressionToComparableOperationConverterTest.kt b/utility/src/test/java/org/oppia/android/util/math/ExpressionToComparableOperationConverterTest.kt index 4af4b5afaa9..203ad029c5b 100644 --- a/utility/src/test/java/org/oppia/android/util/math/ExpressionToComparableOperationConverterTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/ExpressionToComparableOperationConverterTest.kt @@ -11,6 +11,8 @@ import org.oppia.android.testing.junit.OppiaParameterizedTestRunner import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Iteration import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Parameter import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.RunParameterized +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.SelectRunnerPlatform +import org.oppia.android.testing.junit.ParameterizedJunitTestRunner import org.oppia.android.testing.math.ComparableOperationSubject.Companion.assertThat import org.oppia.android.util.math.ExpressionToComparableOperationConverter.Companion.convertToComparableOperation import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode @@ -28,6 +30,7 @@ import org.robolectric.annotation.LooperMode // FunctionName: test names are conventionally named with underscores. @Suppress("FunctionName") @RunWith(OppiaParameterizedTestRunner::class) +@SelectRunnerPlatform(ParameterizedJunitTestRunner::class) @LooperMode(LooperMode.Mode.PAUSED) class ExpressionToComparableOperationConverterTest { @Parameter lateinit var op1: String From dab330eb1d9ac117859856ecce0519897e2e95cb Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 4 Feb 2022 17:50:04 -0800 Subject: [PATCH 107/162] Post-merge fixes. Also, update PolynomialExtensionsTest to use fast JUnit-only runner. --- utility/src/test/java/org/oppia/android/util/math/BUILD.bazel | 2 +- .../org/oppia/android/util/math/PolynomialExtensionsTest.kt | 3 +++ .../java/org/oppia/android/util/math/RealExtensionsTest.kt | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel index 050fb4b94b4..0f7c78a736c 100644 --- a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel @@ -295,7 +295,7 @@ oppia_android_test( deps = [ "//model/src/main/proto:math_java_proto_lite", "//testing/src/main/java/org/oppia/android/testing/junit:oppia_parameterized_test_runner", - "//testing/src/main/java/org/oppia/android/testing/junit:parameterized_robolectric_test_runner", + "//testing/src/main/java/org/oppia/android/testing/junit:parameterized_junit_test_runner", "//testing/src/main/java/org/oppia/android/testing/math:polynomial_subject", "//third_party:com_google_truth_truth", "//third_party:junit_junit", diff --git a/utility/src/test/java/org/oppia/android/util/math/PolynomialExtensionsTest.kt b/utility/src/test/java/org/oppia/android/util/math/PolynomialExtensionsTest.kt index 36b1aaa5224..2f8813d8096 100644 --- a/utility/src/test/java/org/oppia/android/util/math/PolynomialExtensionsTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/PolynomialExtensionsTest.kt @@ -12,6 +12,8 @@ import org.oppia.android.testing.junit.OppiaParameterizedTestRunner import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Iteration import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Parameter import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.RunParameterized +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.SelectRunnerPlatform +import org.oppia.android.testing.junit.ParameterizedJunitTestRunner import org.oppia.android.testing.math.PolynomialSubject.Companion.assertThat import org.oppia.android.testing.math.RealSubject.Companion.assertThat import org.robolectric.annotation.LooperMode @@ -20,6 +22,7 @@ import org.robolectric.annotation.LooperMode // FunctionName: test names are conventionally named with underscores. @Suppress("FunctionName") @RunWith(OppiaParameterizedTestRunner::class) +@SelectRunnerPlatform(ParameterizedJunitTestRunner::class) @LooperMode(LooperMode.Mode.PAUSED) class PolynomialExtensionsTest { private companion object { diff --git a/utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt b/utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt index 49713266d7e..b6a13238005 100644 --- a/utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt @@ -418,7 +418,7 @@ class RealExtensionsTest { @Test fun testIsApproximatelyZero_irrationalCloseToZero_returnsTrue() { - val real = createIrrationalReal(0.000000001) + val real = createIrrationalReal(0.00000000000000001) val result = real.isApproximatelyZero() From 9c70c31f643a58e7d1bcbb5761b2f69b27a0c876 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 4 Feb 2022 18:13:42 -0800 Subject: [PATCH 108/162] Post-merge fixes. Also, update float interval per new tests. --- ...sEquivalentToRuleClassifierProviderTest.kt | 209 +++++++++--------- ...esExactlyWithRuleClassifierProviderTest.kt | 3 + ...ManipulationsRuleClassifierProviderTest.kt | 3 + .../android/util/math/FloatExtensions.kt | 5 +- .../org/oppia/android/util/math/BUILD.bazel | 2 +- .../math/ComparableOperationExtensionsTest.kt | 2 +- .../android/util/math/FloatExtensionsTest.kt | 23 +- .../util/math/MathExpressionExtensionsTest.kt | 7 +- .../util/math/PolynomialExtensionsTest.kt | 4 +- .../android/util/math/RealExtensionsTest.kt | 24 +- 10 files changed, 149 insertions(+), 133 deletions(-) diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProviderTest.kt index 3f9b00babce..2d2aee241b2 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProviderTest.kt @@ -20,6 +20,8 @@ import org.oppia.android.testing.junit.OppiaParameterizedTestRunner import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Iteration import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Parameter import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.RunParameterized +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.SelectRunnerPlatform +import org.oppia.android.testing.junit.ParameterizedRobolectricTestRunner import org.oppia.android.testing.robolectric.RobolectricModule import org.oppia.android.testing.threading.TestDispatcherModule import org.oppia.android.testing.time.FakeOppiaClockModule @@ -32,6 +34,7 @@ import org.robolectric.annotation.LooperMode // FunctionName: test names are conventionally named with underscores. @Suppress("FunctionName") @RunWith(OppiaParameterizedTestRunner::class) +@SelectRunnerPlatform(ParameterizedRobolectricTestRunner::class) @LooperMode(LooperMode.Mode.PAUSED) @Config(manifest = Config.NONE) class NumericExpressionInputIsEquivalentToRuleClassifierProviderTest { @@ -187,110 +190,110 @@ class NumericExpressionInputIsEquivalentToRuleClassifierProviderTest { @Test @RunParameterized( -// Iteration("2*(2+6+3+4)==2*(2+6+3+4)", "answer=2*(2+6+3+4)", "input=2*(2+6+3+4)"), -// Iteration("2 × (2+6+3+4)==2*(2+6+3+4)", "answer=2 × (2+6+3+4)", "input=2*(2+6+3+4)"), -// Iteration( -// "15 - (6 × 2) + 3==15 - (6 × 2) + 3", "answer=15 - (6 × 2) + 3", "input=15 - (6 × 2) + 3" -// ), -// Iteration( -// "2 × (50 + 150 + 100 + 25) ==(50 + 150 + 100 + 25) × 2", -// "answer=2 × (50 + 150 + 100 + 25) ", -// "input=(50 + 150 + 100 + 25) × 2" -// ), -// Iteration( -// "2 * (50 + 150 + 100 + 25) ==2 × (50 + 150 + 100 + 25)", -// "answer=2 * (50 + 150 + 100 + 25) ", -// "input=2 × (50 + 150 + 100 + 25)" -// ), -// Iteration("2+5==5+2", "answer=2+5", "input=5+2"), -// Iteration("5+2==5+2", "answer=5+2", "input=5+2"), -// Iteration("6 − (− 4)==6 − (− 4)", "answer=6 − (− 4)", "input=6 − (− 4)"), -// Iteration("6-(-4)==6 − (− 4)", "answer=6-(-4)", "input=6 − (− 4)"), -// Iteration("− (− 4) + 6==6 − (− 4)", "answer=− (− 4) + 6", "input=6 − (− 4)"), -// Iteration("10==6 − (− 4)", "answer=10", "input=6 − (− 4)"), -// Iteration("6 + 4==6 − (− 4)", "answer=6 + 4", "input=6 − (− 4)"), -// Iteration("6 + 2^2==6 − (− 4)", "answer=6 + 2^2", "input=6 − (− 4)"), -// Iteration("3 * 2 − (− 4)==6 − (− 4)", "answer=3 * 2 − (− 4)", "input=6 − (− 4)"), -// Iteration("100/10==6 − (− 4)", "answer=100/10", "input=6 − (− 4)"), -// Iteration("3 * 10^-5==3 * 10^-5", "answer=3 * 10^-5", "input=3 * 10^-5"), -// Iteration("3/(10 * 10^4)==3 * 10^-5", "answer=3/(10 * 10^4)", "input=3 * 10^-5"), -// Iteration( -// "1000 + 200 + 30 + 4 + 0.5 + 0.06==1000 + 200 + 30 + 4 + 0.5 + 0.06", -// "answer=1000 + 200 + 30 + 4 + 0.5 + 0.06", -// "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" -// ), -// Iteration( -// "200 + 30 + 4 + 0.5 + 0.06 + 1000==1000 + 200 + 30 + 4 + 0.5 + 0.06", -// "answer=200 + 30 + 4 + 0.5 + 0.06 + 1000", -// "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" -// ), -// Iteration( -// "0.06 + 0.5 + 4 + 30 + 200 + 1000==1000 + 200 + 30 + 4 + 0.5 + 0.06", -// "answer=0.06 + 0.5 + 4 + 30 + 200 + 1000", -// "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" -// ), -// Iteration( -// "1234.56==1000 + 200 + 30 + 4 + 0.5 + 0.06", -// "answer=1234.56", -// "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" -// ), -// Iteration( -// "123456/100==1000 + 200 + 30 + 4 + 0.5 + 0.06", -// "answer=123456/100", -// "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" -// ), -// Iteration( -// "61728/50==1000 + 200 + 30 + 4 + 0.5 + 0.06", -// "answer=61728/50", -// "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" -// ), -// Iteration( -// "1234 + 56/100==1000 + 200 + 30 + 4 + 0.5 + 0.06", -// "answer=1234 + 56/100", -// "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" -// ), -// Iteration( -// "1230 + 4.56==1000 + 200 + 30 + 4 + 0.5 + 0.06", -// "answer=1230 + 4.56", -// "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" -// ), -// Iteration("2 * 2 * 3 * 3==2 * 2 * 3 * 3", "answer=2 * 2 * 3 * 3", "input=2 * 2 * 3 * 3"), -// Iteration( -// "2 * 2 * 3 * 3 * 1==2 * 2 * 3 * 3", "answer=2 * 2 * 3 * 3 * 1", "input=2 * 2 * 3 * 3" -// ), -// Iteration("2 * 2 * 9==2 * 2 * 3 * 3", "answer=2 * 2 * 9", "input=2 * 2 * 3 * 3"), -// Iteration("4 * 3^2==2 * 2 * 3 * 3", "answer=4 * 3^2", "input=2 * 2 * 3 * 3"), -// Iteration("8/2 * 3 * 3==2 * 2 * 3 * 3", "answer=8/2 * 3 * 3", "input=2 * 2 * 3 * 3"), -// Iteration("36==2 * 2 * 3 * 3", "answer=36", "input=2 * 2 * 3 * 3"), -// Iteration("sqrt(4-2)==sqrt(2)", "answer=sqrt(4-2)", "input=sqrt(2)"), -// Iteration("2*(6+3+4) + 4==2*(2+6+3+4)", "answer=2*(6+3+4) + 4", "input=2*(2+6+3+4)"), -// Iteration("2*(2+6+3) + 8==2*(2+6+3+4)", "answer=2*(2+6+3) + 8", "input=2*(2+6+3+4)"), -// Iteration("(2+6+3+4)*2==2*(2+6+3+4)", "answer=(2+6+3+4)*2", "input=2*(2+6+3+4)"), -// Iteration("(2+6+3+4) × 2==2*(2+6+3+4)", "answer=(2+6+3+4) × 2", "input=2*(2+6+3+4)"), -// Iteration("15 - 12 + 3==15 - (6 × 2) + 3", "answer=15 - 12 + 3", "input=15 - (6 × 2) + 3"), -// Iteration( -// "3 - (6 * 2) + 15==15 - (6 × 2) + 3", "answer=3 - (6 * 2) + 15", "input=15 - (6 × 2) + 3" -// ), -// Iteration( -// "15 - (2 × 6) + 3==15 - (6 × 2) + 3", "answer=15 - (2 × 6) + 3", "input=15 - (6 × 2) + 3" -// ), -// Iteration( -// "2 *(50 + 150) + 2*(100 + 25)==(50 + 150 + 100 + 25) × 2", -// "answer=2 *(50 + 150) + 2*(100 + 25)", -// "input=(50 + 150 + 100 + 25) × 2" -// ), -// Iteration( -// "2* ( 25+50+100+150)==(50 + 150 + 100 + 25) × 2", -// "answer=2* ( 25+50+100+150)", -// "input=(50 + 150 + 100 + 25) × 2" -// ), -// Iteration("10^−5 * 3==3 * 10^-5", "answer=10^−5 * 3", "input=3 * 10^-5"), -// Iteration("30 * 10^−6==3 * 10^-5", "answer=30 * 10^−6", "input=3 * 10^-5"), -// Iteration("0.00003==3 * 10^-5", "answer=0.00003", "input=3 * 10^-5"), -// Iteration("3/10^5==3 * 10^-5", "answer=3/10^5", "input=3 * 10^-5"), + Iteration("2*(2+6+3+4)==2*(2+6+3+4)", "answer=2*(2+6+3+4)", "input=2*(2+6+3+4)"), + Iteration("2 × (2+6+3+4)==2*(2+6+3+4)", "answer=2 × (2+6+3+4)", "input=2*(2+6+3+4)"), + Iteration( + "15 - (6 × 2) + 3==15 - (6 × 2) + 3", "answer=15 - (6 × 2) + 3", "input=15 - (6 × 2) + 3" + ), + Iteration( + "2 × (50 + 150 + 100 + 25) ==(50 + 150 + 100 + 25) × 2", + "answer=2 × (50 + 150 + 100 + 25) ", + "input=(50 + 150 + 100 + 25) × 2" + ), + Iteration( + "2 * (50 + 150 + 100 + 25) ==2 × (50 + 150 + 100 + 25)", + "answer=2 * (50 + 150 + 100 + 25) ", + "input=2 × (50 + 150 + 100 + 25)" + ), + Iteration("2+5==5+2", "answer=2+5", "input=5+2"), + Iteration("5+2==5+2", "answer=5+2", "input=5+2"), + Iteration("6 − (− 4)==6 − (− 4)", "answer=6 − (− 4)", "input=6 − (− 4)"), + Iteration("6-(-4)==6 − (− 4)", "answer=6-(-4)", "input=6 − (− 4)"), + Iteration("− (− 4) + 6==6 − (− 4)", "answer=− (− 4) + 6", "input=6 − (− 4)"), + Iteration("10==6 − (− 4)", "answer=10", "input=6 − (− 4)"), + Iteration("6 + 4==6 − (− 4)", "answer=6 + 4", "input=6 − (− 4)"), + Iteration("6 + 2^2==6 − (− 4)", "answer=6 + 2^2", "input=6 − (− 4)"), + Iteration("3 * 2 − (− 4)==6 − (− 4)", "answer=3 * 2 − (− 4)", "input=6 − (− 4)"), + Iteration("100/10==6 − (− 4)", "answer=100/10", "input=6 − (− 4)"), + Iteration("3 * 10^-5==3 * 10^-5", "answer=3 * 10^-5", "input=3 * 10^-5"), + Iteration("3/(10 * 10^4)==3 * 10^-5", "answer=3/(10 * 10^4)", "input=3 * 10^-5"), + Iteration( + "1000 + 200 + 30 + 4 + 0.5 + 0.06==1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=1000 + 200 + 30 + 4 + 0.5 + 0.06", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration( + "200 + 30 + 4 + 0.5 + 0.06 + 1000==1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=200 + 30 + 4 + 0.5 + 0.06 + 1000", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration( + "0.06 + 0.5 + 4 + 30 + 200 + 1000==1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=0.06 + 0.5 + 4 + 30 + 200 + 1000", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration( + "1234.56==1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=1234.56", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration( + "123456/100==1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=123456/100", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration( + "61728/50==1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=61728/50", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration( + "1234 + 56/100==1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=1234 + 56/100", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration( + "1230 + 4.56==1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=1230 + 4.56", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration("2 * 2 * 3 * 3==2 * 2 * 3 * 3", "answer=2 * 2 * 3 * 3", "input=2 * 2 * 3 * 3"), + Iteration( + "2 * 2 * 3 * 3 * 1==2 * 2 * 3 * 3", "answer=2 * 2 * 3 * 3 * 1", "input=2 * 2 * 3 * 3" + ), + Iteration("2 * 2 * 9==2 * 2 * 3 * 3", "answer=2 * 2 * 9", "input=2 * 2 * 3 * 3"), + Iteration("4 * 3^2==2 * 2 * 3 * 3", "answer=4 * 3^2", "input=2 * 2 * 3 * 3"), + Iteration("8/2 * 3 * 3==2 * 2 * 3 * 3", "answer=8/2 * 3 * 3", "input=2 * 2 * 3 * 3"), + Iteration("36==2 * 2 * 3 * 3", "answer=36", "input=2 * 2 * 3 * 3"), + Iteration("sqrt(4-2)==sqrt(2)", "answer=sqrt(4-2)", "input=sqrt(2)"), + Iteration("2*(6+3+4) + 4==2*(2+6+3+4)", "answer=2*(6+3+4) + 4", "input=2*(2+6+3+4)"), + Iteration("2*(2+6+3) + 8==2*(2+6+3+4)", "answer=2*(2+6+3) + 8", "input=2*(2+6+3+4)"), + Iteration("(2+6+3+4)*2==2*(2+6+3+4)", "answer=(2+6+3+4)*2", "input=2*(2+6+3+4)"), + Iteration("(2+6+3+4) × 2==2*(2+6+3+4)", "answer=(2+6+3+4) × 2", "input=2*(2+6+3+4)"), + Iteration("15 - 12 + 3==15 - (6 × 2) + 3", "answer=15 - 12 + 3", "input=15 - (6 × 2) + 3"), + Iteration( + "3 - (6 * 2) + 15==15 - (6 × 2) + 3", "answer=3 - (6 * 2) + 15", "input=15 - (6 × 2) + 3" + ), + Iteration( + "15 - (2 × 6) + 3==15 - (6 × 2) + 3", "answer=15 - (2 × 6) + 3", "input=15 - (6 × 2) + 3" + ), + Iteration( + "2 *(50 + 150) + 2*(100 + 25)==(50 + 150 + 100 + 25) × 2", + "answer=2 *(50 + 150) + 2*(100 + 25)", + "input=(50 + 150 + 100 + 25) × 2" + ), + Iteration( + "2* ( 25+50+100+150)==(50 + 150 + 100 + 25) × 2", + "answer=2* ( 25+50+100+150)", + "input=(50 + 150 + 100 + 25) × 2" + ), + Iteration("10^−5 * 3==3 * 10^-5", "answer=10^−5 * 3", "input=3 * 10^-5"), + Iteration("30 * 10^−6==3 * 10^-5", "answer=30 * 10^−6", "input=3 * 10^-5"), + Iteration("0.00003==3 * 10^-5", "answer=0.00003", "input=3 * 10^-5"), + Iteration("3/10^5==3 * 10^-5", "answer=3/10^5", "input=3 * 10^-5"), Iteration("3 *2 – (− 4)==6 − (− 4)", "answer=3 *2 – (− 4)", "input=6 − (− 4)"), -// Iteration("7==5+2", "answer=7", "input=5+2"), -// Iteration("3+4==5+2", "answer=3+4", "input=5+2") + Iteration("7==5+2", "answer=7", "input=5+2"), + Iteration("3+4==5+2", "answer=3+4", "input=5+2") ) fun testMatches_assortedExpressions_withMatchingCharacteristics_returnsTrue() { val answerExpression = createMathExpression(answer) diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest.kt index 42b4c7e2b99..a64d6296159 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest.kt @@ -20,6 +20,8 @@ import org.oppia.android.testing.junit.OppiaParameterizedTestRunner import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Iteration import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Parameter import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.RunParameterized +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.SelectRunnerPlatform +import org.oppia.android.testing.junit.ParameterizedRobolectricTestRunner import org.oppia.android.testing.robolectric.RobolectricModule import org.oppia.android.testing.threading.TestDispatcherModule import org.oppia.android.testing.time.FakeOppiaClockModule @@ -32,6 +34,7 @@ import org.robolectric.annotation.LooperMode // FunctionName: test names are conventionally named with underscores. @Suppress("FunctionName") @RunWith(OppiaParameterizedTestRunner::class) +@SelectRunnerPlatform(ParameterizedRobolectricTestRunner::class) @LooperMode(LooperMode.Mode.PAUSED) @Config(manifest = Config.NONE) class NumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest { diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt index bbd4085d420..e0aaeb73822 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt @@ -20,6 +20,8 @@ import org.oppia.android.testing.junit.OppiaParameterizedTestRunner import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Iteration import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Parameter import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.RunParameterized +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.SelectRunnerPlatform +import org.oppia.android.testing.junit.ParameterizedRobolectricTestRunner import org.oppia.android.testing.robolectric.RobolectricModule import org.oppia.android.testing.threading.TestDispatcherModule import org.oppia.android.testing.time.FakeOppiaClockModule @@ -32,6 +34,7 @@ import org.robolectric.annotation.LooperMode // FunctionName: test names are conventionally named with underscores. @Suppress("FunctionName") @RunWith(OppiaParameterizedTestRunner::class) +@SelectRunnerPlatform(ParameterizedRobolectricTestRunner::class) @LooperMode(LooperMode.Mode.PAUSED) @Config(manifest = Config.NONE) class NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest { diff --git a/utility/src/main/java/org/oppia/android/util/math/FloatExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/FloatExtensions.kt index 3d8c27dfd37..b5362e166a7 100644 --- a/utility/src/main/java/org/oppia/android/util/math/FloatExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/FloatExtensions.kt @@ -9,8 +9,7 @@ import kotlin.math.abs * * Note that the machine epsilon value from https://en.wikipedia.org/wiki/Machine_epsilon is defined * defined as the smallest value that, when added to, or subtract from, 1, will result in a value - * that is exactly equal to 1. A slightly larger value is picked here for some allowance in - * variance. + * that is exactly equal to 1. A larger value is picked here for more allowance in variance. */ const val FLOAT_EQUALITY_EPSILON: Float = 1e-6f @@ -19,7 +18,7 @@ const val FLOAT_EQUALITY_EPSILON: Float = 1e-6f * * See [FLOAT_EQUALITY_EPSILON] for an explanation of this value. */ -const val DOUBLE_EQUALITY_EPSILON: Double = 1e-15 +const val DOUBLE_EQUALITY_EPSILON: Double = 1e-13 /** * Returns whether this float approximately equals another based on a consistent epsilon value diff --git a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel index 8e3b36f2dda..7a59fa5612c 100644 --- a/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel +++ b/utility/src/test/java/org/oppia/android/util/math/BUILD.bazel @@ -198,7 +198,7 @@ oppia_android_test( "//model/src/main/proto:math_java_proto_lite", "//testing", "//testing/src/main/java/org/oppia/android/testing/junit:oppia_parameterized_test_runner", - "//testing/src/main/java/org/oppia/android/testing/junit:parameterized_robolectric_test_runner", + "//testing/src/main/java/org/oppia/android/testing/junit:parameterized_junit_test_runner", "//testing/src/main/java/org/oppia/android/testing/math:polynomial_subject", "//testing/src/main/java/org/oppia/android/testing/math:real_subject", "//third_party:com_google_truth_truth", diff --git a/utility/src/test/java/org/oppia/android/util/math/ComparableOperationExtensionsTest.kt b/utility/src/test/java/org/oppia/android/util/math/ComparableOperationExtensionsTest.kt index 0e277196fa3..719e30c8a8c 100644 --- a/utility/src/test/java/org/oppia/android/util/math/ComparableOperationExtensionsTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/ComparableOperationExtensionsTest.kt @@ -129,7 +129,7 @@ class ComparableOperationExtensionsTest { @Test fun testIsApproximatelyEqualTo_firstIsConstInt2_secondIsConstDouble2PlusMargin_returnsTrue() { val first = createConstantOp(constant = 2) - val second = createConstantOp(constant = 2.0000001) + val second = createConstantOp(constant = 2.000000000000001) val result1 = first.isApproximatelyEqualTo(second) val result2 = second.isApproximatelyEqualTo(first) diff --git a/utility/src/test/java/org/oppia/android/util/math/FloatExtensionsTest.kt b/utility/src/test/java/org/oppia/android/util/math/FloatExtensionsTest.kt index 9b40f04c808..e814d753cae 100644 --- a/utility/src/test/java/org/oppia/android/util/math/FloatExtensionsTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/FloatExtensionsTest.kt @@ -12,9 +12,8 @@ import org.robolectric.annotation.LooperMode @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) class FloatExtensionsTest { - @Test - fun testFloat_approximatelyEquals_bothZero_returnsTrue() { + fun testFloat_isApproximatelyEqualTo_bothZero_returnsTrue() { val leftFloat = 0f val rightFloat = 0f @@ -24,7 +23,7 @@ class FloatExtensionsTest { } @Test - fun testFloat_approximatelyEquals_sameNonZeroValue_returnsTrue() { + fun testFloat_isApproximatelyEqualTo_sameNonZeroValue_returnsTrue() { val leftFloat = 1.2f val rightFloat = 1.2f @@ -34,7 +33,7 @@ class FloatExtensionsTest { } @Test - fun testFloat_approximatelyEquals_nonZeroValues_withinInterval_returnsTrue() { + fun testFloat_isApproximatelyEqualTo_nonZeroValues_withinInterval_returnsTrue() { val leftFloat = 1.2f val rightFloat = leftFloat + FLOAT_EQUALITY_EPSILON / 10f @@ -46,7 +45,7 @@ class FloatExtensionsTest { } @Test - fun testFloat_approximatelyEquals_zeroAndNonZeroValue_veryDifferent_returnsFalse() { + fun testFloat_isApproximatelyEqualTo_zeroAndNonZeroValue_veryDifferent_returnsFalse() { val leftFloat = 0f val rightFloat = 7.3f @@ -56,7 +55,7 @@ class FloatExtensionsTest { } @Test - fun testFloat_approximatelyEquals_nonZeroValues_outsideInterval_returnsFalse() { + fun testFloat_isApproximatelyEqualTo_nonZeroValues_outsideInterval_returnsFalse() { val leftFloat = 1.2f val rightFloat = leftFloat + FLOAT_EQUALITY_EPSILON * 2f @@ -66,7 +65,7 @@ class FloatExtensionsTest { } @Test - fun testFloat_approximatelyEquals_nonZeroValues_veryDifferent_returnsFalse() { + fun testFloat_isApproximatelyEqualTo_nonZeroValues_veryDifferent_returnsFalse() { val leftFloat = 1.2f val rightFloat = 7.3f @@ -76,7 +75,7 @@ class FloatExtensionsTest { } @Test - fun testDouble_approximatelyEquals_bothZero_returnsTrue() { + fun testDouble_isApproximatelyEqualTo_bothZero_returnsTrue() { val leftDouble = 0.0 val rightDouble = 0.0 @@ -86,7 +85,7 @@ class FloatExtensionsTest { } @Test - fun testDouble_approximatelyEquals_sameNonZeroValue_returnsTrue() { + fun testDouble_isApproximatelyEqualTo_sameNonZeroValue_returnsTrue() { val leftDouble = 1.2 val rightDouble = 1.2 @@ -96,7 +95,7 @@ class FloatExtensionsTest { } @Test - fun testDouble_approximatelyEquals_nonZeroValues_withinInterval_returnsTrue() { + fun testDouble_isApproximatelyEqualTo_nonZeroValues_withinInterval_returnsTrue() { val leftDouble = 0.2 val rightDouble = leftDouble + DOUBLE_EQUALITY_EPSILON / 10.0 @@ -108,7 +107,7 @@ class FloatExtensionsTest { } @Test - fun testDouble_approximatelyEquals_nonZeroValues_outsideInterval_returnsFalse() { + fun testDouble_isApproximatelyEqualTo_nonZeroValues_outsideInterval_returnsFalse() { val leftDouble = 1.2 val rightDouble = leftDouble + DOUBLE_EQUALITY_EPSILON * 2 @@ -118,7 +117,7 @@ class FloatExtensionsTest { } @Test - fun testDouble_approximatelyEquals_nonZeroValues_veryDifferent_returnsFalse() { + fun testDouble_isApproximatelyEqualTo_nonZeroValues_veryDifferent_returnsFalse() { val leftDouble = 1.2 val rightDouble = 7.3 diff --git a/utility/src/test/java/org/oppia/android/util/math/MathExpressionExtensionsTest.kt b/utility/src/test/java/org/oppia/android/util/math/MathExpressionExtensionsTest.kt index abc2888f94e..a973125994e 100644 --- a/utility/src/test/java/org/oppia/android/util/math/MathExpressionExtensionsTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/MathExpressionExtensionsTest.kt @@ -1,16 +1,16 @@ package org.oppia.android.util.math import com.google.common.truth.Truth.assertThat -import java.lang.IllegalStateException import org.junit.Test import org.junit.runner.RunWith import org.oppia.android.app.model.MathEquation import org.oppia.android.app.model.MathExpression -import org.oppia.android.testing.assertThrows import org.oppia.android.testing.junit.OppiaParameterizedTestRunner import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Iteration import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Parameter import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.RunParameterized +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.SelectRunnerPlatform +import org.oppia.android.testing.junit.ParameterizedJunitTestRunner import org.oppia.android.testing.math.PolynomialSubject.Companion.assertThat import org.oppia.android.testing.math.RealSubject.Companion.assertThat import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode @@ -35,6 +35,7 @@ import org.robolectric.annotation.LooperMode // SameParameterValue: tests should have specific context included/excluded for readability. @Suppress("FunctionName", "SameParameterValue") @RunWith(OppiaParameterizedTestRunner::class) +@SelectRunnerPlatform(ParameterizedJunitTestRunner::class) @LooperMode(LooperMode.Mode.PAUSED) class MathExpressionExtensionsTest { @Parameter lateinit var exp1: String @@ -173,7 +174,7 @@ class MathExpressionExtensionsTest { @Test @RunParameterized( Iteration("2==2", "exp1=2", "exp2=2"), - Iteration("2==2.000000001", "exp1=2", "exp2=2.000000001"), + Iteration("2==2.000000000000001", "exp1=2", "exp2=2.000000000000001"), Iteration("x+1==x+1", "exp1=x+1", "exp2=x+1"), Iteration("x-1==x-1", "exp1=x-1", "exp2=x-1"), Iteration("x*2==x*2", "exp1=x*2", "exp2=x*2"), diff --git a/utility/src/test/java/org/oppia/android/util/math/PolynomialExtensionsTest.kt b/utility/src/test/java/org/oppia/android/util/math/PolynomialExtensionsTest.kt index 28b91fcbce3..d8a25a368db 100644 --- a/utility/src/test/java/org/oppia/android/util/math/PolynomialExtensionsTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/PolynomialExtensionsTest.kt @@ -1379,7 +1379,7 @@ class PolynomialExtensionsTest { val first = createPolynomial(createTerm(coefficient = TWO_REAL)) val second = createPolynomial( createTerm(coefficient = Real.newBuilder().apply { - irrational = 2.00000001 + irrational = 2.00000000000000001 }.build()) ) @@ -1420,7 +1420,7 @@ class PolynomialExtensionsTest { fun testIsApproximatelyEqualTo_firstIsDoublePointThrees_secondIsFracOneThird_returnsTrue() { val first = createPolynomial( createTerm(coefficient = Real.newBuilder().apply { - irrational = 0.33333333333 + irrational = 0.33333333333333333 }.build()) ) val second = createPolynomial(createTerm(coefficient = ONE_THIRD_REAL)) diff --git a/utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt b/utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt index a2133f5c59f..fee8d4e00e7 100644 --- a/utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/RealExtensionsTest.kt @@ -410,11 +410,11 @@ class RealExtensionsTest { Iteration("0==0.0", "lhsInt=0", "rhsDouble=0.0"), Iteration("1==1.0", "lhsInt=1", "rhsDouble=1.0"), Iteration("2==2.0", "lhsInt=2", "rhsDouble=2.0"), - Iteration("2==2.0000001", "lhsInt=2", "rhsDouble=2.0000001"), - Iteration("2==1.9999999", "lhsInt=2", "rhsDouble=1.9999999"), + Iteration("2==2.000000000000001", "lhsInt=2", "rhsDouble=2.000000000000001"), + Iteration("2==1.999999999999999", "lhsInt=2", "rhsDouble=1.999999999999999"), Iteration("-2==-2.0", "lhsInt=-2", "rhsDouble=-2.0"), - Iteration("-2==-2.0000001", "lhsInt=-2", "rhsDouble=-2.0000001"), - Iteration("-2==-1.9999999", "lhsInt=-2", "rhsDouble=-1.9999999") + Iteration("-2==-2.00000000000001", "lhsInt=-2", "rhsDouble=-2.00000000000001"), + Iteration("-2==-1.999999999999999", "lhsInt=-2", "rhsDouble=-1.999999999999999") ) fun testIsApproximatelyEqualTo_oneIsInt_otherIsSimilarDouble_returnsTrue() { val first = createIntegerReal(lhsInt) @@ -502,8 +502,8 @@ class RealExtensionsTest { Iteration("2==2.0", "lhsFrac=2", "rhsDouble=2.0"), Iteration("2/1==2.0", "lhsFrac=2/1", "rhsDouble=2.0"), Iteration("3/2==1.5", "lhsFrac=3/2", "rhsDouble=1.5"), - Iteration("1/3==0.33333333333", "lhsFrac=1/3", "rhsDouble=0.33333333333"), - Iteration("1 2/3==1.66666666666", "lhsFrac=1 2/3", "rhsDouble=1.66666666666"), + Iteration("1/3==0.33333333333333333", "lhsFrac=1/3", "rhsDouble=0.33333333333333333"), + Iteration("1 2/3==1.66666666666666666", "lhsFrac=1 2/3", "rhsDouble=1.66666666666666666"), Iteration("-2==-2.0", "lhsFrac=-2", "rhsDouble=-2.0"), Iteration("-3/2==-1.5", "lhsFrac=-3/2", "rhsDouble=-1.5") ) @@ -548,10 +548,18 @@ class RealExtensionsTest { @RunParameterized( Iteration("0.0==0.0", "lhsDouble=0.0", "rhsDouble=0.0"), Iteration("2.0==2.0", "lhsDouble=2.0", "rhsDouble=2.0"), - Iteration("2.0000000001==1.9999999999", "lhsDouble=2.0000000001", "rhsDouble=1.9999999999"), + Iteration( + "2.000000000000001==1.999999999999999", + "lhsDouble=2.000000000000001", + "rhsDouble=1.999999999999999" + ), Iteration("3.14==3.14", "lhsDouble=3.14", "rhsDouble=3.14"), Iteration("-2.0==-2.0", "lhsDouble=-2.0", "rhsDouble=-2.0"), - Iteration("-2.0000000001==-1.9999999999", "lhsDouble=-2.0000000001", "rhsDouble=-1.9999999999"), + Iteration( + "-2.000000000000001==-1.999999999999999", + "lhsDouble=-2.000000000000001", + "rhsDouble=-1.999999999999999" + ), Iteration("-3.14==-3.14", "lhsDouble=-3.14", "rhsDouble=-3.14") ) fun testIsApproximatelyEqualTo_oneIsDouble_otherIsSimilarDouble_returnsTrue() { From 3248e84dd6d16ff2ad043ae9a5779cb407035b12 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 4 Feb 2022 18:24:52 -0800 Subject: [PATCH 109/162] Lint & other check fixes. --- ...putIsEquivalentToRuleClassifierProvider.kt | 8 +++- ...atchesExactlyWithRuleClassifierProvider.kt | 9 +++- ...vialManipulationsRuleClassifierProvider.kt | 11 ++++- ...sEquivalentToRuleClassifierProviderTest.kt | 14 +++--- ...esExactlyWithRuleClassifierProviderTest.kt | 43 ++++++++++++------- ...ManipulationsRuleClassifierProviderTest.kt | 23 ++++++---- .../NumericExpressionInputModuleTest.kt | 4 +- .../file_content_validation_checks.textproto | 4 ++ .../math/ComparableOperationExtensions.kt | 4 +- .../util/math/MathExpressionExtensions.kt | 14 +++--- .../math/ComparableOperationExtensionsTest.kt | 5 ++- .../util/math/MathExpressionExtensionsTest.kt | 3 +- .../util/math/PolynomialExtensionsTest.kt | 16 ++++--- 13 files changed, 106 insertions(+), 52 deletions(-) diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProvider.kt index 5d041e3e2ce..92b22428778 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProvider.kt @@ -1,6 +1,5 @@ package org.oppia.android.domain.classify.rules.numericexpressioninput -import javax.inject.Inject import org.oppia.android.app.model.InteractionObject import org.oppia.android.app.model.Real import org.oppia.android.app.model.WrittenTranslationContext @@ -12,7 +11,14 @@ import org.oppia.android.util.math.MathExpressionParser import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult import org.oppia.android.util.math.evaluateAsNumericExpression import org.oppia.android.util.math.isApproximatelyEqualTo +import javax.inject.Inject +/** + * Provider for a classifier that determines whether a numeric expression is numerically equivalent + * to the creator-specific expression defined as the input to this interaction. + * + * See this class's tests for a list of supported cases (both for matching and not matching). + */ class NumericExpressionInputIsEquivalentToRuleClassifierProvider @Inject constructor( private val classifierFactory: GenericRuleClassifier.Factory, private val consoleLogger: ConsoleLogger diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProvider.kt index a62e75d9acb..653197f0727 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProvider.kt @@ -9,9 +9,16 @@ import org.oppia.android.domain.classify.rules.RuleClassifierProvider import org.oppia.android.util.logging.ConsoleLogger import org.oppia.android.util.math.MathExpressionParser import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult -import javax.inject.Inject import org.oppia.android.util.math.isApproximatelyEqualTo +import javax.inject.Inject +/** + * Provider for a classifier that determines whether a numeric expression is exactly equal to the + * creator-specific expression defined as the input to this interaction, including any parenthetical + * groups in the expressions and operand order. + * + * See this class's tests for a list of supported cases (both for matching and not matching). + */ class NumericExpressionInputMatchesExactlyWithRuleClassifierProvider @Inject constructor( private val classifierFactory: GenericRuleClassifier.Factory, private val consoleLogger: ConsoleLogger diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt index b4f4b074099..fe45d1cf6ba 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt @@ -9,10 +9,19 @@ import org.oppia.android.domain.classify.rules.RuleClassifierProvider import org.oppia.android.util.logging.ConsoleLogger import org.oppia.android.util.math.MathExpressionParser import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult +import org.oppia.android.util.math.isApproximatelyEqualTo import org.oppia.android.util.math.toComparableOperation import javax.inject.Inject -import org.oppia.android.util.math.isApproximatelyEqualTo +/** + * Provider for a classifier that determines whether a numeric expression is equal to the + * creator-specific expression defined as the input to this interaction, with some manipulations. + * + * 'Trivial manipulations' indicates rearranging any operands for commutative operations, or changes + * in resolution order (i.e. associative) without changing the meaning of the expression. + * + * See this class's tests for a list of supported cases (both for matching and not matching). + */ class NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider @Inject constructor( private val classifierFactory: GenericRuleClassifier.Factory, diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProviderTest.kt index 2d2aee241b2..b2f289f384c 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProviderTest.kt @@ -8,8 +8,6 @@ import dagger.BindsInstance import dagger.Component import dagger.Module import dagger.Provides -import javax.inject.Inject -import javax.inject.Singleton import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -29,8 +27,16 @@ import org.oppia.android.util.locale.LocaleProdModule import org.oppia.android.util.logging.LoggerModule import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode +import javax.inject.Inject +import javax.inject.Singleton -/** Tests for [NumericExpressionInputIsEquivalentToRuleClassifierProvider]. */ +/** + * Tests for [NumericExpressionInputIsEquivalentToRuleClassifierProvider]. + * + * Note that the tests implemented in this suite are specifically set up to verify the cases + * outlined in this sheet: + * https://docs.google.com/spreadsheets/d/1u1fQdah2WsmdYKWKGmuXy5TPT7Ot-b8A7O9iZF-j5XE/edit#gid=0. + */ // FunctionName: test names are conventionally named with underscores. @Suppress("FunctionName") @RunWith(OppiaParameterizedTestRunner::class) @@ -38,8 +44,6 @@ import org.robolectric.annotation.LooperMode @LooperMode(LooperMode.Mode.PAUSED) @Config(manifest = Config.NONE) class NumericExpressionInputIsEquivalentToRuleClassifierProviderTest { - // TODO: add details about the sheet to this test's KDoc. - @Inject internal lateinit var provider: NumericExpressionInputIsEquivalentToRuleClassifierProvider diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest.kt index a64d6296159..ec362363cb8 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest.kt @@ -8,8 +8,6 @@ import dagger.BindsInstance import dagger.Component import dagger.Module import dagger.Provides -import javax.inject.Inject -import javax.inject.Singleton import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -29,8 +27,17 @@ import org.oppia.android.util.locale.LocaleProdModule import org.oppia.android.util.logging.LoggerModule import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode - -/** Tests for [NumericExpressionInputMatchesExactlyWithRuleClassifierProvider]. */ +import javax.inject.Inject +import javax.inject.Singleton +import org.oppia.android.domain.classify.rules.numericexpressioninput.DaggerNumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest_TestApplicationComponent as DaggerTestApplicationComponent + +/** + * Tests for [NumericExpressionInputMatchesExactlyWithRuleClassifierProvider]. + * + * Note that the tests implemented in this suite are specifically set up to verify the cases + * outlined in this sheet: + * https://docs.google.com/spreadsheets/d/1u1fQdah2WsmdYKWKGmuXy5TPT7Ot-b8A7O9iZF-j5XE/edit#gid=0. + */ // FunctionName: test names are conventionally named with underscores. @Suppress("FunctionName") @RunWith(OppiaParameterizedTestRunner::class) @@ -38,8 +45,6 @@ import org.robolectric.annotation.LooperMode @LooperMode(LooperMode.Mode.PAUSED) @Config(manifest = Config.NONE) class NumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest { - // TODO: add details about the sheet to this test's KDoc. - @Inject internal lateinit var provider: NumericExpressionInputMatchesExactlyWithRuleClassifierProvider @@ -245,27 +250,33 @@ class NumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest { Iteration( "0.06 + 0.5 + 4 + 30 + 200 + 1000!=1000 + 200 + 30 + 4 + 0.5 + 0.06", "answer=0.06 + 0.5 + 4 + 30 + 200 + 1000", - "input=1000 + 200 + 30 + 4 + 0.5 + 0.06"), + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), Iteration( "1234.56!=1000 + 200 + 30 + 4 + 0.5 + 0.06", "answer=1234.56", - "input=1000 + 200 + 30 + 4 + 0.5 + 0.06"), + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), Iteration( "123456/100!=1000 + 200 + 30 + 4 + 0.5 + 0.06", "answer=123456/100", - "input=1000 + 200 + 30 + 4 + 0.5 + 0.06"), + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), Iteration( "61728/50!=1000 + 200 + 30 + 4 + 0.5 + 0.06", "answer=61728/50", - "input=1000 + 200 + 30 + 4 + 0.5 + 0.06"), + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), Iteration( "1234 + 56/10!=1000 + 200 + 30 + 4 + 0.5 + 0.06", "answer=1234 + 56/10", - "input=1000 + 200 + 30 + 4 + 0.5 + 0.06"), + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), Iteration( "1230 + 4.56!=1000 + 200 + 30 + 4 + 0.5 + 0.06", "answer=1230 + 4.56", - "input=1000 + 200 + 30 + 4 + 0.5 + 0.06"), + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), Iteration( "2 * 2 * 3 * 3 * 1!=2 * 2 * 3 * 3", "answer=2 * 2 * 3 * 3 * 1", "input=2 * 2 * 3 * 3" ), @@ -305,11 +316,13 @@ class NumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest { Iteration( "123456!=1000 + 200 + 30 + 4 + 0.5 + 0.06", "answer=123456", - "input=1000 + 200 + 30 + 4 + 0.5 + 0.06"), + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), Iteration( "1000 + 200 + 30!=1000 + 200 + 30 + 4 + 0.5 + 0.06", "answer=1000 + 200 + 30", - "input=1000 + 200 + 30 + 4 + 0.5 + 0.06"), + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), Iteration("3 *2 – (− 4)!=6 − (− 4)", "answer=3 *2 – (− 4)", "input=6 − (− 4)"), Iteration("6 − 4!=6 − (− 4)", "answer=6 − 4", "input=6 − (− 4)"), Iteration("6 + (− 4)!=6 − (− 4)", "answer=6 + (− 4)", "input=6 − (− 4)"), @@ -346,7 +359,7 @@ class NumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest { }.build() private fun setUpTestApplicationComponent() { - DaggerNumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest_TestApplicationComponent + DaggerTestApplicationComponent .builder() .setApplication(ApplicationProvider.getApplicationContext()).build().inject(this) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt index e0aaeb73822..f75c05b451a 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt @@ -8,8 +8,6 @@ import dagger.BindsInstance import dagger.Component import dagger.Module import dagger.Provides -import javax.inject.Inject -import javax.inject.Singleton import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -29,8 +27,18 @@ import org.oppia.android.util.locale.LocaleProdModule import org.oppia.android.util.logging.LoggerModule import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode - -/** Tests for [NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider]. */ +import javax.inject.Inject +import javax.inject.Singleton +import org.oppia.android.domain.classify.rules.numericexpressioninput.DaggerNumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest_TestApplicationComponent as DaggerTestApplicationComponent +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider as RuleClassifierProvider + +/** + * Tests for [RuleClassifierProvider]. + * + * Note that the tests implemented in this suite are specifically set up to verify the cases + * outlined in this sheet: + * https://docs.google.com/spreadsheets/dNumericExpressionInputIsEquivalentToRuleClassifierProvider/1u1fQdah2WsmdYKWKGmuXy5TPT7Ot-b8A7O9iZF-j5XE/edit#gid=0. + */ // FunctionName: test names are conventionally named with underscores. @Suppress("FunctionName") @RunWith(OppiaParameterizedTestRunner::class) @@ -38,10 +46,7 @@ import org.robolectric.annotation.LooperMode @LooperMode(LooperMode.Mode.PAUSED) @Config(manifest = Config.NONE) class NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest { - // TODO: add details about the sheet to this test's KDoc. - - @Inject - internal lateinit var provider: NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider + @Inject internal lateinit var provider: RuleClassifierProvider @Parameter lateinit var answer: String @Parameter lateinit var input: String @@ -368,7 +373,7 @@ class NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvide }.build() private fun setUpTestApplicationComponent() { - DaggerNumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest_TestApplicationComponent + DaggerTestApplicationComponent .builder() .setApplication(ApplicationProvider.getApplicationContext()).build().inject(this) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputModuleTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputModuleTest.kt index 3ad0eed220e..b12cc16af95 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputModuleTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputModuleTest.kt @@ -9,8 +9,6 @@ import dagger.BindsInstance import dagger.Component import dagger.Module import dagger.Provides -import javax.inject.Inject -import javax.inject.Singleton import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -23,6 +21,8 @@ import org.oppia.android.util.locale.LocaleProdModule import org.oppia.android.util.logging.LoggerModule import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode +import javax.inject.Inject +import javax.inject.Singleton /** Tests for [NumericExpressionInputModule]. */ // FunctionName: test names are conventionally named with underscores. diff --git a/scripts/assets/file_content_validation_checks.textproto b/scripts/assets/file_content_validation_checks.textproto index 0c9432585fb..1408521c618 100644 --- a/scripts/assets/file_content_validation_checks.textproto +++ b/scripts/assets/file_content_validation_checks.textproto @@ -286,8 +286,12 @@ file_content_checks { file_path_regex: ".+?\\.kt" prohibited_content_regex: "OppiaParameterizedTestRunner" failure_message: "To use OppiaParameterizedTestRunner, please add an exemption to file_content_validation_checks.textproto and add an explanation for your use case in your PR description. Note that parameterized tests should only be used in special circumstances where a single behavior can be tested across multiple inputs, or for especially large test suites that can be trivially reduced." + exempted_file_name: "domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProviderTest.kt" + exempted_file_name: "domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest.kt" + exempted_file_name: "domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt" exempted_file_name: "scripts/src/javatests/org/oppia/android/scripts/regex/RegexPatternValidationCheckTest.kt" exempted_file_name: "utility/src/test/java/org/oppia/android/util/math/ExpressionToComparableOperationConverterTest.kt" + exempted_file_name: "utility/src/test/java/org/oppia/android/util/math/MathExpressionExtensionsTest.kt" exempted_file_name: "utility/src/test/java/org/oppia/android/util/math/MathExpressionParserTest.kt" exempted_file_name: "utility/src/test/java/org/oppia/android/util/math/MathTokenizerTest.kt" exempted_file_name: "utility/src/test/java/org/oppia/android/util/math/PolynomialExtensionsTest.kt" diff --git a/utility/src/main/java/org/oppia/android/util/math/ComparableOperationExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/ComparableOperationExtensions.kt index 249bbea8394..65dadeebecc 100644 --- a/utility/src/main/java/org/oppia/android/util/math/ComparableOperationExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/ComparableOperationExtensions.kt @@ -53,8 +53,8 @@ private fun NonCommutativeOperation.isApproximatelyEqualTo( if (operationTypeCase != other.operationTypeCase) return false return when (operationTypeCase) { EXPONENTIATION -> { - exponentiation.leftOperand.isApproximatelyEqualTo(other.exponentiation.leftOperand) - && exponentiation.rightOperand.isApproximatelyEqualTo(other.exponentiation.rightOperand) + exponentiation.leftOperand.isApproximatelyEqualTo(other.exponentiation.leftOperand) && + exponentiation.rightOperand.isApproximatelyEqualTo(other.exponentiation.rightOperand) } SQUARE_ROOT -> squareRoot.isApproximatelyEqualTo(other.squareRoot) OPERATIONTYPE_NOT_SET, null -> true diff --git a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt index ab2cdd1d5c2..165c6a1cf26 100644 --- a/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt +++ b/utility/src/main/java/org/oppia/android/util/math/MathExpressionExtensions.kt @@ -66,17 +66,17 @@ fun MathExpression.isApproximatelyEqualTo(other: MathExpression): Boolean { CONSTANT -> constant.isApproximatelyEqualTo(other.constant) VARIABLE -> variable == other.variable BINARY_OPERATION -> { - binaryOperation.operator == other.binaryOperation.operator - && binaryOperation.leftOperand.isApproximatelyEqualTo(other.binaryOperation.leftOperand) - && binaryOperation.rightOperand.isApproximatelyEqualTo(other.binaryOperation.rightOperand) + binaryOperation.operator == other.binaryOperation.operator && + binaryOperation.leftOperand.isApproximatelyEqualTo(other.binaryOperation.leftOperand) && + binaryOperation.rightOperand.isApproximatelyEqualTo(other.binaryOperation.rightOperand) } UNARY_OPERATION -> { - unaryOperation.operator == other.unaryOperation.operator - && unaryOperation.operand.isApproximatelyEqualTo(other.unaryOperation.operand) + unaryOperation.operator == other.unaryOperation.operator && + unaryOperation.operand.isApproximatelyEqualTo(other.unaryOperation.operand) } FUNCTION_CALL -> { - functionCall.functionType == other.functionCall.functionType - && functionCall.argument.isApproximatelyEqualTo(other.functionCall.argument) + functionCall.functionType == other.functionCall.functionType && + functionCall.argument.isApproximatelyEqualTo(other.functionCall.argument) } GROUP -> group.isApproximatelyEqualTo(other.group) EXPRESSIONTYPE_NOT_SET, null -> true diff --git a/utility/src/test/java/org/oppia/android/util/math/ComparableOperationExtensionsTest.kt b/utility/src/test/java/org/oppia/android/util/math/ComparableOperationExtensionsTest.kt index 719e30c8a8c..f7198f2ba4e 100644 --- a/utility/src/test/java/org/oppia/android/util/math/ComparableOperationExtensionsTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/ComparableOperationExtensionsTest.kt @@ -738,7 +738,8 @@ class ComparableOperationExtensionsTest { }.build() private fun createExpOp( - lhs: ComparableOperation, rhs: ComparableOperation + lhs: ComparableOperation, + rhs: ComparableOperation ) = ComparableOperation.newBuilder().apply { nonCommutativeOperation = NonCommutativeOperation.newBuilder().apply { exponentiation = NonCommutativeOperation.BinaryOperation.newBuilder().apply { @@ -759,7 +760,7 @@ class ComparableOperationExtensionsTest { private fun createIntegerReal(value: Int) = Real.newBuilder().apply { integer = value }.build() - + private fun createRationalReal(rawFractionExpression: String) = Real.newBuilder().apply { rational = fractionParser.parseFractionFromString(rawFractionExpression) }.build() diff --git a/utility/src/test/java/org/oppia/android/util/math/MathExpressionExtensionsTest.kt b/utility/src/test/java/org/oppia/android/util/math/MathExpressionExtensionsTest.kt index a973125994e..6406f3e6e70 100644 --- a/utility/src/test/java/org/oppia/android/util/math/MathExpressionExtensionsTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/MathExpressionExtensionsTest.kt @@ -282,7 +282,8 @@ class MathExpressionExtensionsTest { } private fun parseAlgebraicExpression( - expression: String, errorCheckingMode: ErrorCheckingMode = ALL_ERRORS + expression: String, + errorCheckingMode: ErrorCheckingMode = ALL_ERRORS ): MathExpression { return parseAlgebraicExpression( expression, allowedVariables = listOf("x", "y", "z"), errorCheckingMode diff --git a/utility/src/test/java/org/oppia/android/util/math/PolynomialExtensionsTest.kt b/utility/src/test/java/org/oppia/android/util/math/PolynomialExtensionsTest.kt index d8a25a368db..b3cc704c4c9 100644 --- a/utility/src/test/java/org/oppia/android/util/math/PolynomialExtensionsTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/PolynomialExtensionsTest.kt @@ -1378,9 +1378,11 @@ class PolynomialExtensionsTest { fun testIsApproximatelyEqualTo_firstIsPolyOfInt2_secondIsPolyOfDouble2PlusMargin_returnsTrue() { val first = createPolynomial(createTerm(coefficient = TWO_REAL)) val second = createPolynomial( - createTerm(coefficient = Real.newBuilder().apply { - irrational = 2.00000000000000001 - }.build()) + createTerm( + coefficient = Real.newBuilder().apply { + irrational = 2.00000000000000001 + }.build() + ) ) val result1 = first.isApproximatelyEqualTo(second) @@ -1419,9 +1421,11 @@ class PolynomialExtensionsTest { @Test fun testIsApproximatelyEqualTo_firstIsDoublePointThrees_secondIsFracOneThird_returnsTrue() { val first = createPolynomial( - createTerm(coefficient = Real.newBuilder().apply { - irrational = 0.33333333333333333 - }.build()) + createTerm( + coefficient = Real.newBuilder().apply { + irrational = 0.33333333333333333 + }.build() + ) ) val second = createPolynomial(createTerm(coefficient = ONE_THIRD_REAL)) From 3960220a08fd3b7eb6af2dcfebbbf00203ee467d Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 4 Feb 2022 18:38:50 -0800 Subject: [PATCH 110/162] Replace deprecated term. --- ...MatchesUpToTrivialManipulationsRuleClassifierProvider.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt index fe45d1cf6ba..b616874d193 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt @@ -40,12 +40,12 @@ class NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvide input: String, writtenTranslationContext: WrittenTranslationContext ): Boolean { - val answerExpression = parseComparableOperationList(answer) ?: return false - val inputExpression = parseComparableOperationList(input) ?: return false + val answerExpression = parseComparableOperation(answer) ?: return false + val inputExpression = parseComparableOperation(input) ?: return false return answerExpression.isApproximatelyEqualTo(inputExpression) } - private fun parseComparableOperationList(rawExpression: String): ComparableOperation? { + private fun parseComparableOperation(rawExpression: String): ComparableOperation? { return when (val expResult = MathExpressionParser.parseNumericExpression(rawExpression)) { is MathParsingResult.Success -> expResult.result.toComparableOperation() is MathParsingResult.Failure -> { From f7ec7511db78466c3677495b081aa645a82d5201 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 4 Feb 2022 20:15:48 -0800 Subject: [PATCH 111/162] Post-merge fixes. --- ...putIsEquivalentToRuleClassifierProvider.kt | 4 ++-- ...atchesExactlyWithRuleClassifierProvider.kt | 4 ++-- ...vialManipulationsRuleClassifierProvider.kt | 20 +++++++++---------- ...sEquivalentToRuleClassifierProviderTest.kt | 3 ++- ...esExactlyWithRuleClassifierProviderTest.kt | 3 ++- ...ManipulationsRuleClassifierProviderTest.kt | 3 ++- 6 files changed, 20 insertions(+), 17 deletions(-) diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputIsEquivalentToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputIsEquivalentToRuleClassifierProvider.kt index a6ad8e8e010..56e771b93c0 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputIsEquivalentToRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputIsEquivalentToRuleClassifierProvider.kt @@ -11,7 +11,7 @@ import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingRes import org.oppia.android.util.math.MathExpressionParser.Companion.parseAlgebraicExpression import org.oppia.android.util.math.toPolynomial import javax.inject.Inject -import org.oppia.android.util.math.approximatelyEquals +import org.oppia.android.util.math.isApproximatelyEqualTo class AlgebraicExpressionInputIsEquivalentToRuleClassifierProvider @Inject constructor( private val classifierFactory: GenericRuleClassifier.Factory, @@ -33,7 +33,7 @@ class AlgebraicExpressionInputIsEquivalentToRuleClassifierProvider @Inject const val allowedVariables = classificationContext.extractAllowedVariables() val answerExpression = parsePolynomial(answer, allowedVariables) ?: return false val inputExpression = parsePolynomial(input, allowedVariables) ?: return false - return answerExpression.approximatelyEquals(inputExpression) + return answerExpression.isApproximatelyEqualTo(inputExpression) } private fun parsePolynomial(rawExpression: String, allowedVariables: List): Polynomial? { diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider.kt index 6feeba0696a..4c20ce52903 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider.kt @@ -10,7 +10,7 @@ import org.oppia.android.util.logging.ConsoleLogger import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult import org.oppia.android.util.math.MathExpressionParser.Companion.parseAlgebraicExpression import javax.inject.Inject -import org.oppia.android.util.math.approximatelyEquals +import org.oppia.android.util.math.isApproximatelyEqualTo class AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider @Inject constructor( private val classifierFactory: GenericRuleClassifier.Factory, @@ -32,7 +32,7 @@ class AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider @Inject c val allowedVariables = classificationContext.extractAllowedVariables() val answerExpression = parseExpression(answer, allowedVariables) ?: return false val inputExpression = parseExpression(input, allowedVariables) ?: return false - return answerExpression.approximatelyEquals(inputExpression) + return answerExpression.isApproximatelyEqualTo(inputExpression) } private fun parseExpression( diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt index 03c39a3c64f..6f618dfc857 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt @@ -1,6 +1,7 @@ package org.oppia.android.domain.classify.rules.algebraicexpressioninput -import org.oppia.android.app.model.ComparableOperationList +import javax.inject.Inject +import org.oppia.android.app.model.ComparableOperation import org.oppia.android.app.model.InteractionObject import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier @@ -9,9 +10,8 @@ import org.oppia.android.domain.classify.rules.RuleClassifierProvider import org.oppia.android.util.logging.ConsoleLogger import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult import org.oppia.android.util.math.MathExpressionParser.Companion.parseAlgebraicExpression -import org.oppia.android.util.math.toComparableOperationList -import javax.inject.Inject -import org.oppia.android.util.math.approximatelyEquals +import org.oppia.android.util.math.isApproximatelyEqualTo +import org.oppia.android.util.math.toComparableOperation class AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider @Inject constructor( @@ -32,17 +32,17 @@ class AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvi classificationContext: ClassificationContext ): Boolean { val allowedVariables = classificationContext.extractAllowedVariables() - val answerExpression = parseComparableOperationList(answer, allowedVariables) ?: return false - val inputExpression = parseComparableOperationList(input, allowedVariables) ?: return false - return answerExpression.approximatelyEquals(inputExpression) + val answerExpression = parseComparableOperation(answer, allowedVariables) ?: return false + val inputExpression = parseComparableOperation(input, allowedVariables) ?: return false + return answerExpression.isApproximatelyEqualTo(inputExpression) } - private fun parseComparableOperationList( + private fun parseComparableOperation( rawExpression: String, allowedVariables: List - ): ComparableOperationList? { + ): ComparableOperation? { return when (val expResult = parseAlgebraicExpression(rawExpression, allowedVariables)) { - is MathParsingResult.Success -> expResult.result.toComparableOperationList() + is MathParsingResult.Success -> expResult.result.toComparableOperation() is MathParsingResult.Failure -> { consoleLogger.e( "AlgebraExpTrivialManips", diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProviderTest.kt index b2f289f384c..4ef5e2928c4 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProviderTest.kt @@ -29,6 +29,7 @@ import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode import javax.inject.Inject import javax.inject.Singleton +import org.oppia.android.domain.classify.ClassificationContext /** * Tests for [NumericExpressionInputIsEquivalentToRuleClassifierProvider]. @@ -349,7 +350,7 @@ class NumericExpressionInputIsEquivalentToRuleClassifierProviderTest { return classifier.matches( answerExpression, inputs = mapOf("x" to inputExpression), - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest.kt index ec362363cb8..228e1e3582e 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest.kt @@ -29,6 +29,7 @@ import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode import javax.inject.Inject import javax.inject.Singleton +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.rules.numericexpressioninput.DaggerNumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest_TestApplicationComponent as DaggerTestApplicationComponent /** @@ -350,7 +351,7 @@ class NumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest { return classifier.matches( answerExpression, inputs = mapOf("x" to inputExpression), - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt index f75c05b451a..beef3fdf622 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt @@ -29,6 +29,7 @@ import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode import javax.inject.Inject import javax.inject.Singleton +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.rules.numericexpressioninput.DaggerNumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest_TestApplicationComponent as DaggerTestApplicationComponent import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider as RuleClassifierProvider @@ -364,7 +365,7 @@ class NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvide return classifier.matches( answerExpression, inputs = mapOf("x" to inputExpression), - writtenTranslationContext = WrittenTranslationContext.getDefaultInstance() + classificationContext = ClassificationContext() ) } From 782e05829cbe76bfe84d28cee28320ee75f41b51 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 4 Feb 2022 22:44:12 -0800 Subject: [PATCH 112/162] Add full test suites for alg exp classifiers. --- ...sEquivalentToRuleClassifierProviderTest.kt | 665 +++++++++++++++++ ...esExactlyWithRuleClassifierProviderTest.kt | 649 +++++++++++++++++ ...ManipulationsRuleClassifierProviderTest.kt | 666 ++++++++++++++++++ .../AlgebraicExpressionInputModuleTest.kt | 107 +++ .../algebraicexpressioninput/BUILD.bazel | 105 +++ ...ManipulationsRuleClassifierProviderTest.kt | 4 +- .../util/math/PolynomialExtensionsTest.kt | 2 +- 7 files changed, 2195 insertions(+), 3 deletions(-) create mode 100644 domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputIsEquivalentToRuleClassifierProviderTest.kt create mode 100644 domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProviderTest.kt create mode 100644 domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt create mode 100644 domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModuleTest.kt create mode 100644 domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/BUILD.bazel diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputIsEquivalentToRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputIsEquivalentToRuleClassifierProviderTest.kt new file mode 100644 index 00000000000..60d09fbf36b --- /dev/null +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputIsEquivalentToRuleClassifierProviderTest.kt @@ -0,0 +1,665 @@ +package org.oppia.android.domain.classify.rules.algebraicexpressioninput + +import android.app.Application +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import com.google.common.truth.Truth.assertThat +import dagger.BindsInstance +import dagger.Component +import dagger.Module +import dagger.Provides +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.oppia.android.app.model.InteractionObject +import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.RuleClassifier +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Iteration +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Parameter +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.RunParameterized +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.SelectRunnerPlatform +import org.oppia.android.testing.junit.ParameterizedRobolectricTestRunner +import org.oppia.android.testing.robolectric.RobolectricModule +import org.oppia.android.testing.threading.TestDispatcherModule +import org.oppia.android.testing.time.FakeOppiaClockModule +import org.oppia.android.util.locale.LocaleProdModule +import org.oppia.android.util.logging.LoggerModule +import org.robolectric.annotation.Config +import org.robolectric.annotation.LooperMode +import javax.inject.Inject +import javax.inject.Singleton +import org.oppia.android.app.model.SchemaObject +import org.oppia.android.app.model.SchemaObjectList +import org.oppia.android.domain.classify.ClassificationContext + +/** + * Tests for [AlgebraicExpressionInputIsEquivalentToRuleClassifierProvider]. + * + * Note that the tests implemented in this suite are specifically set up to verify the cases + * outlined in this sheet: + * https://docs.google.com/spreadsheets/d/1u1fQdah2WsmdYKWKGmuXy5TPT7Ot-b8A7O9iZF-j5XE/edit#gid=0. + */ +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") +@RunWith(OppiaParameterizedTestRunner::class) +@SelectRunnerPlatform(ParameterizedRobolectricTestRunner::class) +@LooperMode(LooperMode.Mode.PAUSED) +@Config(manifest = Config.NONE) +class AlgebraicExpressionInputIsEquivalentToRuleClassifierProviderTest { + @Inject + internal lateinit var provider: AlgebraicExpressionInputIsEquivalentToRuleClassifierProvider + + @Parameter lateinit var answer: String + @Parameter lateinit var input: String + + private lateinit var classifier: RuleClassifier + private val allPossibleVariables = (('a'..'z') + ('A'..'Z')).toList().map { it.toString() } + + @Before + fun setUp() { + setUpTestApplicationComponent() + classifier = provider.createRuleClassifier() + } + + @Test + fun testMatches_answerHasDisallowedVariable_returnsFalse() { + val answerExpression = createMathExpression("y") + val inputExpression = createMathExpression("y") + + val matches = matchesClassifier(answerExpression, inputExpression, allowedVariables = listOf()) + + // Despite the answer otherwise being equal, the variable isn't allowed. This shouldn't actually + // be the case in practice since neither the creator nor the learner would be allowed to input a + // disallowed variable (so this check is mainly a "just-in-case"). + assertThat(matches).isFalse() + } + + @Test + @RunParameterized( + Iteration("0==0", "answer=0", "input=0"), + Iteration("1==1", "answer=1", "input=1"), + Iteration("2==2", "answer=2", "input=2"), + Iteration("x==x", "answer=x", "input=x"), + Iteration("y==y", "answer=y", "input=y") + ) + fun testMatches_sameSingleTerms_returnsTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // If the two expressions are exactly the same, the classifier should match. + assertThat(matches).isTrue() + } + + @Test + @RunParameterized( + Iteration("-2==-2", "answer=-2", "input=-2"), + Iteration("1+3.14==1+3.14", "answer=1+3.14", "input=1+3.14"), + Iteration(" 1 + 3.14 ==1+3.14", "answer= 1 + 3.14 ", "input=1+3.14"), + Iteration("1+2+3==1+2+3", "answer=1+2+3", "input=1+2+3"), + Iteration("1-3.14==1-3.14", "answer=1-3.14", "input=1-3.14"), + Iteration("2*3.14==2*3.14", "answer=2*3.14", "input=2*3.14"), + Iteration("2/3==2/3", "answer=2/3", "input=2/3"), + Iteration("2/3.14==2/3.14", "answer=2/3.14", "input=2/3.14"), + Iteration("2^3==2^3", "answer=2^3", "input=2^3"), + Iteration("2^3.14==2^3.14", "answer=2^3.14", "input=2^3.14"), + Iteration("sqrt(2)==sqrt(2)", "answer=sqrt(2)", "input=sqrt(2)"), + Iteration("-x==-x", "answer=-x", "input=-x"), + Iteration("x+3.14==x+3.14", "answer=x+3.14", "input=x+3.14"), + Iteration("x-3.14==x-3.14", "answer=x-3.14", "input=x-3.14"), + Iteration("x*3.14==x*3.14", "answer=x*3.14", "input=x*3.14"), + Iteration("x/3==x/3", "answer=x/3", "input=x/3") + ) + fun testMatches_sameSingleOperations_returnsTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // If the two expressions are exactly the same, the classifier should match. + assertThat(matches).isTrue() + } + + @Test + @RunParameterized( + Iteration("sqrt(x)!=sqrt(x)", "answer=sqrt(x)", "input=sqrt(x)") + ) + fun testMatches_sameSingleOperations_thatCannotBecomePolynomials_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // Despite the terms being the same, if they can't be converted to a polynomial then the + // classifier won't match. + assertThat(matches).isFalse() + } + + @Test + @RunParameterized( + Iteration("1!=0", "answer=1", "input=0"), + Iteration("0!=1", "answer=0", "input=1"), + Iteration("3.14!=1", "answer=3.14", "input=1"), + Iteration("1!=3.14", "answer=1", "input=3.14"), + Iteration("x!=3.14", "answer=x", "input=3.14"), + Iteration("y!=x", "answer=y", "input=x"), + Iteration("3.14!=x", "answer=3.14", "input=x") + ) + fun testMatches_differentSingleTerms_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // If the two expressions don't evaluate to the same value then the classifier won't match them. + assertThat(matches).isFalse() + } + + @Test + @RunParameterized( + Iteration("3.14+1==1+3.14", "answer=3.14+1", "input=1+3.14"), + Iteration("3+2+1==1+2+3", "answer=3+2+1", "input=1+2+3"), + Iteration("-3.14+1==1-3.14", "answer=-3.14+1", "input=1-3.14"), + Iteration("3.14*2==2*3.14", "answer=3.14*2", "input=2*3.14"), + Iteration("2+x==x+2", "answer=2+x", "input=x+2"), + Iteration("y+x==x+y", "answer=y+x", "input=x+y"), + Iteration("x*2==2x", "answer=x*2", "input=2x"), + Iteration("yx==xy", "answer=yx", "input=xy") + ) + fun testMatches_operationsDiffer_byCommutativity_returnsTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // Reordering terms by commutativity is allowed by this classifier. + assertThat(matches).isTrue() + } + + @Test + @RunParameterized( + Iteration("1+(2+3)==(1+2)+3", "answer=1+(2+3)", "input=(1+2)+3"), + Iteration("2*(3*4)==(2*3)*4", "answer=2*(3*4)", "input=(2*3)*4"), + Iteration("x+(2+3)==(x+2)+3", "answer=x+(2+3)", "input=(x+2)+3"), + Iteration("x+(y+z)==(x+y)+z", "answer=x+(y+z)", "input=(x+y)+z"), + Iteration("2*(3x)==(2x)*3", "answer=2*(3x)", "input=(2x)*3"), + Iteration("x(yz)==(xy)z", "answer=x(yz)", "input=(xy)z") + ) + fun testMatches_operationsDiffer_byAssociativity_returnsTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // Changing operation associativity is allowed by this classifier. + assertThat(matches).isTrue() + } + + @Test + @RunParameterized( + Iteration("3.14-1!=1-3.14", "answer=3.14-1", "input=1-3.14"), + Iteration("1-(2-3)!=(1-2)-3", "answer=1-(2-3)", "input=(1-2)-3"), + Iteration("3.14/2!=2/3.14", "answer=3.14/2", "input=2/3.14"), + Iteration("2/(3/4)!=(2/3)/4", "answer=2/(3/4)", "input=(2/3)/4"), + Iteration("3.14^2!=2^3.14", "answer=3.14^2", "input=2^3.14"), + Iteration("3.14-x!=x-3.14", "answer=3.14-x", "input=x-3.14"), + Iteration("x-(y-z)!=(x-y)-z", "answer=x-(y-z)", "input=(x-y)-z"), + Iteration("3.14/x!=x/3.14", "answer=3.14/x", "input=x/3.14"), + Iteration("x/(y/z)!=(x/y)/z", "answer=x/(y/z)", "input=(x/y)/z") + ) + fun testMatches_operationsDiffer_byNonCommutativeOrAssociativeReordering_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // Non-commutative and non-associative reordering generally results in a different value, so the + // classifier will fail to match. + assertThat(matches).isFalse() + } + + @Test + @RunParameterized( + Iteration("1-2==-(2-1)", "answer=1-2", "input=-(2-1)"), + Iteration("1+2==1+1+1", "answer=1+2", "input=1+1+1"), + Iteration("1+2==1-(-2)", "answer=1+2", "input=1-(-2)"), + Iteration("4-6==1-2-1", "answer=4-6", "input=1-2-1"), + Iteration("2*3*2*2==2*3*4", "answer=2*3*2*2", "input=2*3*4"), + Iteration("-6-2==2*-(3+1)", "answer=-6-2", "input=2*-(3+1)"), + Iteration("2/3/2/2==2/3/4", "answer=2/3/2/2", "input=2/3/4"), + Iteration("2^(2+1)==2^3", "answer=2^(2+1)", "input=2^3"), + Iteration("2^(-1)==1/2", "answer=2^(-1)", "input=1/2"), + Iteration("x-y==-(y-x)", "answer=x-y", "input=-(y-x)"), + Iteration("2+x==1+x+1", "answer=2+x", "input=1+x+1"), + Iteration("1+x==1-(-x)", "answer=1+x", "input=1-(-x)"), + Iteration("-x==1-x-1", "answer=-x", "input=1-x-1"), + Iteration("4x==2*2*x", "answer=4x", "input=2*2*x"), + Iteration("2-6x==2*(-3x+1)", "answer=2-6x", "input=2*(-3x+1)"), + Iteration("x/4==x/2/2", "answer=x/4", "input=x/2/2"), + Iteration("x^(2+1)==x^3", "answer=x^(2+1)", "input=x^3"), + Iteration("x*(2^(-1))==x/2", "answer=x*(2^(-1))", "input=x/2") + ) + fun testMatches_operationsDiffer_byDistributionAndCombining_returnTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // This classifier supports any distribution or combining of terms. + assertThat(matches).isTrue() + } + + @Test + @RunParameterized( + Iteration("3x(3x - 2) + 1==(3x-1)^2", "answer=3x(3x - 2) + 1", "input=(3x-1)^2"), + Iteration("3(3x^2) - 6x +1==(3x-1)^2", "answer=3(3x^2) - 6x +1", "input=(3x-1)^2"), + Iteration("(1 - 3x)^2!=(3x-1)^2", "answer=(1 - 3x)^2", "input=(3x-1)^2"), + Iteration("2x==sqrt(4x^2)", "answer=2x", "input=sqrt(4x^2)"), + Iteration("x^2+2x+1==(x+1)^2", "answer=x^2+2x+1", "input=(x+1)^2"), + Iteration("x^2-1==(x+1)(x-1)", "answer=x^2-1", "input=(x+1)(x-1)"), + Iteration("x+1==(x^2+2x+1)/(x+1)", "answer=x+1", "input=(x^2+2x+1)/(x+1)"), + Iteration("x-1==(x^2-1)/(x+1)", "answer=x-1", "input=(x^2-1)/(x+1)"), + Iteration("x+1==(x^2-1)/(x-1)", "answer=x+1", "input=(x^2-1)/(x-1)"), + Iteration("-3x==(-27x^3)^(1/3)", "answer=-3x", "input=(-27x^3)^(1/3)"), + Iteration("1==(x^2-1)/(x^2-1)", "answer=1", "input=(x^2-1)/(x^2-1)"), + Iteration("2*(2+6+3+4)==2*(2+6+3+4)", "answer=2*(2+6+3+4)", "input=2*(2+6+3+4)"), + Iteration("2 × (2+6+3+4)==2*(2+6+3+4)", "answer=2 × (2+6+3+4)", "input=2*(2+6+3+4)"), + Iteration( + "15 - (6 × 2) + 3==15 - (6 × 2) + 3", "answer=15 - (6 × 2) + 3", "input=15 - (6 × 2) + 3" + ), + Iteration( + "2 × (50 + 150 + 100 + 25) ==(50 + 150 + 100 + 25) × 2", + "answer=2 × (50 + 150 + 100 + 25) ", + "input=(50 + 150 + 100 + 25) × 2" + ), + Iteration( + "2 * (50 + 150 + 100 + 25) ==2 × (50 + 150 + 100 + 25)", + "answer=2 * (50 + 150 + 100 + 25) ", + "input=2 × (50 + 150 + 100 + 25)" + ), + Iteration("2+5==5+2", "answer=2+5", "input=5+2"), + Iteration("5+2==5+2", "answer=5+2", "input=5+2"), + Iteration("6 − (− 4)==6 − (− 4)", "answer=6 − (− 4)", "input=6 − (− 4)"), + Iteration("6-(-4)==6 − (− 4)", "answer=6-(-4)", "input=6 − (− 4)"), + Iteration("− (− 4) + 6==6 − (− 4)", "answer=− (− 4) + 6", "input=6 − (− 4)"), + Iteration("10==6 − (− 4)", "answer=10", "input=6 − (− 4)"), + Iteration("6 + 4==6 − (− 4)", "answer=6 + 4", "input=6 − (− 4)"), + Iteration("6 + 2^2==6 − (− 4)", "answer=6 + 2^2", "input=6 − (− 4)"), + Iteration("3 * 2 − (− 4)==6 − (− 4)", "answer=3 * 2 − (− 4)", "input=6 − (− 4)"), + Iteration("100/10==6 − (− 4)", "answer=100/10", "input=6 − (− 4)"), + Iteration("3 * 10^-5==3 * 10^-5", "answer=3 * 10^-5", "input=3 * 10^-5"), + Iteration("3/(10 * 10^4)==3 * 10^-5", "answer=3/(10 * 10^4)", "input=3 * 10^-5"), + Iteration( + "1000 + 200 + 30 + 4 + 0.5 + 0.06==1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=1000 + 200 + 30 + 4 + 0.5 + 0.06", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration( + "200 + 30 + 4 + 0.5 + 0.06 + 1000==1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=200 + 30 + 4 + 0.5 + 0.06 + 1000", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration( + "0.06 + 0.5 + 4 + 30 + 200 + 1000==1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=0.06 + 0.5 + 4 + 30 + 200 + 1000", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration( + "1234.56==1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=1234.56", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration( + "123456/100==1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=123456/100", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration( + "61728/50==1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=61728/50", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration( + "1234 + 56/100==1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=1234 + 56/100", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration( + "1230 + 4.56==1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=1230 + 4.56", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration("2 * 2 * 3 * 3==2 * 2 * 3 * 3", "answer=2 * 2 * 3 * 3", "input=2 * 2 * 3 * 3"), + Iteration( + "2 * 2 * 3 * 3 * 1==2 * 2 * 3 * 3", "answer=2 * 2 * 3 * 3 * 1", "input=2 * 2 * 3 * 3" + ), + Iteration("2 * 2 * 9==2 * 2 * 3 * 3", "answer=2 * 2 * 9", "input=2 * 2 * 3 * 3"), + Iteration("4 * 3^2==2 * 2 * 3 * 3", "answer=4 * 3^2", "input=2 * 2 * 3 * 3"), + Iteration("8/2 * 3 * 3==2 * 2 * 3 * 3", "answer=8/2 * 3 * 3", "input=2 * 2 * 3 * 3"), + Iteration("36==2 * 2 * 3 * 3", "answer=36", "input=2 * 2 * 3 * 3"), + Iteration("sqrt(4-2)==sqrt(2)", "answer=sqrt(4-2)", "input=sqrt(2)"), + Iteration("4x^2+20x==4*x^2+20x", "answer=4x^2+20x", "input=4*x^2+20x"), + Iteration("3+x-5==3+x-5", "answer=3+x-5", "input=3+x-5"), + Iteration("Z+A-Z==Z+A-Z", "answer=Z+A-Z", "input=Z+A-Z"), + Iteration("6C - 5A -1==6C - 5A -1", "answer=6C - 5A -1", "input=6C - 5A -1"), + Iteration("5Z-w==5*Z-w", "answer=5Z-w", "input=5*Z-w"), + Iteration("5*Z-w==5*Z-w", "answer=5*Z-w", "input=5*Z-w"), + Iteration("LS-3S+L==L*S-3S+L", "answer=LS-3S+L", "input=L*S-3S+L"), + Iteration("L*S-3S+L==L*S-3S+L", "answer=L*S-3S+L", "input=L*S-3S+L"), + Iteration("L*S-3*S+L==L*S-3S+L", "answer=L*S-3*S+L", "input=L*S-3S+L"), + Iteration("LS-3*S+L==L*S-3S+L", "answer=LS-3*S+L", "input=L*S-3S+L"), + Iteration("9x^2 − 6x + 1==9x^2 − 6x + 1", "answer=9x^2 − 6x + 1", "input=9x^2 − 6x + 1"), + Iteration("c*b-c==c*b-c", "answer=c*b-c", "input=c*b-c"), + Iteration("bc-c==c*b-c", "answer=bc-c", "input=c*b-c"), + Iteration("cb-c==c*b-c", "answer=cb-c", "input=c*b-c"), + Iteration("-c+bc==c*b-c", "answer=-c+bc", "input=c*b-c"), + Iteration("-c+cb==c*b-c", "answer=-c+cb", "input=c*b-c"), + Iteration("x^2+y+4x==x^2+y+4x", "answer=x^2+y+4x", "input=x^2+y+4x"), + Iteration("y+4x+x^2==x^2+y+4x", "answer=y+4x+x^2", "input=x^2+y+4x"), + Iteration("x^2+4x+y==x^2+y+4x", "answer=x^2+4x+y", "input=x^2+y+4x"), + Iteration("Y+5==Y+5", "answer=Y+5", "input=Y+5"), + Iteration("5+Y==Y+5", "answer=5+Y", "input=Y+5"), + Iteration( + "a^2 + b^2 + c^2+ 2a*b + 2a*c + 2bc==a^2+b^2+c^2+2a*b+2a*c+2bc", + "answer=a^2 + b^2 + c^2+ 2a*b + 2a*c + 2bc", + "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + ), + Iteration( + "a^2 + b^2 + c^2+ 2a*b + 2bc + 2a*c==a^2+b^2+c^2+2a*b+2a*c+2bc", + "answer=a^2 + b^2 + c^2+ 2a*b + 2bc + 2a*c", + "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + ), + Iteration( + "2a*b + 2bc + 2a*c + a^2 + b^2 + c^2==a^2+b^2+c^2+2a*b+2a*c+2bc", + "answer=2a*b + 2bc + 2a*c + a^2 + b^2 + c^2", + "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + ), + Iteration( + "2a*b + b^2 + c^2+ a^2 + 2bc + 2a*c==a^2+b^2+c^2+2a*b+2a*c+2bc", + "answer=2a*b + b^2 + c^2+ a^2 + 2bc + 2a*c", + "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + ), + Iteration( + "a*a + b*b + c*c + 2*a*b + 2*a*c + 2*b*c==a^2+b^2+c^2+2a*b+2a*c+2bc", + "answer=a*a + b*b + c*c + 2*a*b + 2*a*c + 2*b*c", + "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + ), + Iteration( + "(a+ b)^2 + c^2 + 2bc + 2a*c==a^2+b^2+c^2+2a*b+2a*c+2bc", + "answer=(a+ b)^2 + c^2 + 2bc + 2a*c", + "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + ), + Iteration( + "(a+b+c)^2==a^2+b^2+c^2+2a*b+2a*c+2bc", "answer=(a+b+c)^2", "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + ), + Iteration( + "(-a -b -c)^2==a^2+b^2+c^2+2a*b+2a*c+2bc", + "answer=(-a -b -c)^2", + "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + ), + Iteration("1 - 6x + 9x^2==9x^2 − 6x + 1", "answer=1 - 6x + 9x^2", "input=9x^2 − 6x + 1"), + Iteration("9x^2 + 1 - 6x==9x^2 − 6x + 1", "answer=9x^2 + 1 - 6x", "input=9x^2 − 6x + 1"), + Iteration("2+1+x==x+1+2", "answer=2+1+x", "input=x+1+2"), + Iteration("1+2+x==x+1+2", "answer=1+2+x", "input=x+1+2"), + Iteration("1+x+2==x+1+2", "answer=1+x+2", "input=x+1+2"), + Iteration("2+x+1==x+1+2", "answer=2+x+1", "input=x+1+2"), + Iteration("(x+1)+2==x+1+2", "answer=(x+1)+2", "input=x+1+2"), + Iteration("x + (1+2)==x+1+2", "answer=x + (1+2)", "input=x+1+2"), + Iteration( + "y+1+ 9x(x − 6)==9x(x − 6) + 1+ y", "answer=y+1+ 9x(x − 6)", "input=9x(x − 6) + 1+ y" + ), + Iteration("1+y+9x(x − 6)==9x(x − 6) + 1+ y", "answer=1+y+9x(x − 6)", "input=9x(x − 6) + 1+ y"), + Iteration( + "1 + 9x(x − 6) + y==9x(x − 6) + 1+ y", "answer=1 + 9x(x − 6) + y", "input=9x(x − 6) + 1+ y" + ), + Iteration( + "(y+1)+9x(x − 6)==9x(x − 6) + 1+ y", "answer=(y+1)+9x(x − 6)", "input=9x(x − 6) + 1+ y" + ), + Iteration( + "(x^2 − x)/3 − 4y==(x^2 − x)/3 − 4y", "answer=(x^2 − x)/3 − 4y", "input=(x^2 − x)/3 − 4y" + ), + Iteration( + "-4y + (x^2 − x)/3==(x^2 − x)/3 − 4y", "answer=-4y + (x^2 − x)/3", "input=(x^2 − x)/3 − 4y" + ), + Iteration( + "x(x − 1)/3 −4y==(x^2 − x)/3 − 4y", "answer=x(x − 1)/3 −4y", "input=(x^2 − x)/3 − 4y" + ), + Iteration( + "x^2/3 − x/3 − 4y==(x^2 − x)/3 − 4y", "answer=x^2/3 − x/3 − 4y", "input=(x^2 − x)/3 − 4y" + ), + Iteration( + "x^2/3 − (x/3 + 4y)==(x^2 − x)/3 − 4y", "answer=x^2/3 − (x/3 + 4y)", "input=(x^2 − x)/3 − 4y" + ), + Iteration("(3x -1)^2==(3x-1)^2", "answer=(3x -1)^2", "input=(3x-1)^2"), + Iteration("2*(6+3+4) + 4==2*(2+6+3+4)", "answer=2*(6+3+4) + 4", "input=2*(2+6+3+4)"), + Iteration("2*(2+6+3) + 8==2*(2+6+3+4)", "answer=2*(2+6+3) + 8", "input=2*(2+6+3+4)"), + Iteration("(2+6+3+4)*2==2*(2+6+3+4)", "answer=(2+6+3+4)*2", "input=2*(2+6+3+4)"), + Iteration("(2+6+3+4) × 2==2*(2+6+3+4)", "answer=(2+6+3+4) × 2", "input=2*(2+6+3+4)"), + Iteration("15 - 12 + 3==15 - (6 × 2) + 3", "answer=15 - 12 + 3", "input=15 - (6 × 2) + 3"), + Iteration( + "3 - (6 * 2) + 15==15 - (6 × 2) + 3", "answer=3 - (6 * 2) + 15", "input=15 - (6 × 2) + 3" + ), + Iteration( + "15 - (2 × 6) + 3==15 - (6 × 2) + 3", "answer=15 - (2 × 6) + 3", "input=15 - (6 × 2) + 3" + ), + Iteration( + "2 *(50 + 150) + 2*(100 + 25)==(50 + 150 + 100 + 25) × 2", + "answer=2 *(50 + 150) + 2*(100 + 25)", + "input=(50 + 150 + 100 + 25) × 2" + ), + Iteration( + "2* ( 25+50+100+150)==(50 + 150 + 100 + 25) × 2", + "answer=2* ( 25+50+100+150)", + "input=(50 + 150 + 100 + 25) × 2" + ), + Iteration("10^−5 * 3==3 * 10^-5", "answer=10^−5 * 3", "input=3 * 10^-5"), + Iteration("30 * 10^−6==3 * 10^-5", "answer=30 * 10^−6", "input=3 * 10^-5"), + Iteration("0.00003==3 * 10^-5", "answer=0.00003", "input=3 * 10^-5"), + Iteration("3/10^5==3 * 10^-5", "answer=3/10^5", "input=3 * 10^-5"), + Iteration("3 *2 – (− 4)==6 − (− 4)", "answer=3 *2 – (− 4)", "input=6 − (− 4)"), + Iteration("7==5+2", "answer=7", "input=5+2"), + Iteration("3+4==5+2", "answer=3+4", "input=5+2"), + Iteration("20x+4x^2==4*x^2+20x", "answer=20x+4x^2", "input=4*x^2+20x"), + Iteration("x-5+3==3+x-5", "answer=x-5+3", "input=3+x-5"), + Iteration("-5+3+x==3+x-5", "answer=-5+3+x", "input=3+x-5"), + Iteration("-5+x+3==3+x-5", "answer=-5+x+3", "input=3+x-5"), + Iteration("3+(x-5)==3+x-5", "answer=3+(x-5)", "input=3+x-5"), + Iteration("A==Z+A-Z", "answer=A", "input=Z+A-Z"), + Iteration("A+Z-Z==Z+A-Z", "answer=A+Z-Z", "input=Z+A-Z"), + Iteration("Z+(A-Z)==Z+A-Z", "answer=Z+(A-Z)", "input=Z+A-Z"), + Iteration("6C - (5A+1)==6C - 5A -1", "answer=6C - (5A+1)", "input=6C - 5A -1"), + Iteration("-5A-1+6C==6C - 5A -1", "answer=-5A-1+6C", "input=6C - 5A -1"), + Iteration("-W+5Z==5*Z-W", "answer=-W+5Z", "input=5*Z-W"), + Iteration("L(1+S)-3S==L*S-3S+L", "answer=L(1+S)-3S", "input=L*S-3S+L"), + Iteration("S(L-3)+L==L*S-3S+L", "answer=S(L-3)+L", "input=L*S-3S+L"), + Iteration("L+LS-3S==L*S-3S+L", "answer=L+LS-3S", "input=L*S-3S+L"), + Iteration( + "x(x − 1)/3 − 4y==(x^2 − x)/3 − 4y", "answer=x(x − 1)/3 − 4y", "input=(x^2 − x)/3 − 4y" + ), + Iteration( + "- 4y + (x^2 − x)/3==(x^2 − x)/3 − 4y", "answer=- 4y + (x^2 − x)/3", "input=(x^2 − x)/3 − 4y" + ), + Iteration( + "(x^2 − x) * 3^-1 − 4y==(x^2 − x)/3 − 4y", + "answer=(x^2 − x) * 3^-1 − 4y", + "input=(x^2 − x)/3 − 4y" + ), + Iteration( + "a^2 + b^2 + c^2 + 2(a*b + a*c + bc)==a^2+b^2+c^2+2a*b+2a*c+2bc", + "answer=a^2 + b^2 + c^2 + 2(a*b + a*c + bc)", + "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + ), + Iteration( + "(a + b)^2 + c^2 + 2a*c + 2bc==a^2+b^2+c^2+2a*b+2a*c+2bc", + "answer=(a + b)^2 + c^2 + 2a*c + 2bc", + "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + ), + Iteration( + "a * a + b * b + c^3/c + 2a*b + 2a*c + 2bc==a^2+b^2+c^2+2a*b+2a*c+2bc", + "answer=a * a + b * b + c^3/c + 2a*b + 2a*c + 2bc", + "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + ), + Iteration( + "a^2+ b^2 + c^2 + 2bc + 2a*c + 2a*b==a^2+b^2+c^2+2a*b+2a*c+2bc", + "answer=a^2+ b^2 + c^2 + 2bc + 2a*c + 2a*b", + "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + ), + Iteration("(3x − 1)^2==9x^2 − 6x + 1", "answer=(3x − 1)^2", "input=9x^2 − 6x + 1"), + Iteration("3x(3x − 2) + 1==9x^2 − 6x + 1", "answer=3x(3x − 2) + 1", "input=9x^2 − 6x + 1"), + Iteration("3(3x^2 − 2x) + 1==9x^2 − 6x + 1", "answer=3(3x^2 − 2x) + 1", "input=9x^2 − 6x + 1"), + Iteration("(3x)^2 − 6x + 1==9x^2 − 6x + 1", "answer=(3x)^2 − 6x + 1", "input=9x^2 − 6x + 1"), + Iteration("c(b-1)==c*b-c", "answer=c(b-1)", "input=c*b-c"), + Iteration("x(x+4)+y==x^2+y+4x", "answer=x(x+4)+y", "input=x^2+y+4x"), + Iteration("x+3==x+1+2", "answer=x+3", "input=x+1+2") + ) + fun testMatches_assortedExpressions_withMatchingCharacteristics_returnsTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // This verifies a variety of expressions that per the PRD and technical specification should + // pass for this classifier. + assertThat(matches).isTrue() + } + + @Test + @RunParameterized( + Iteration("√(3x −1)4!=(3x-1)^2", "answer=√(3x −1)4", "input=(3x-1)^2"), + Iteration("3 * 10^5!=3 * 10^-5", "answer=3 * 10^5", "input=3 * 10^-5"), + Iteration("2 * 10^−5!=3 * 10^-5", "answer=2 * 10^−5", "input=3 * 10^-5"), + Iteration("5 * 10^−3!=3 * 10^-5", "answer=5 * 10^−3", "input=3 * 10^-5"), + Iteration( + "123456!=1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=123456", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration( + "1000 + 200 + 30!=1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=1000 + 200 + 30", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration("6 − 4!=6 − (− 4)", "answer=6 − 4", "input=6 − (− 4)"), + Iteration("6 + (− 4)!=6 − (− 4)", "answer=6 + (− 4)", "input=6 − (− 4)"), + Iteration("100!=6 − (− 4)", "answer=100", "input=6 − (− 4)"), + Iteration("2 * 2 * 3!=2 * 2 * 3 * 3", "answer=2 * 2 * 3", "input=2 * 2 * 3 * 3"), + Iteration("2 * 3 * 3 * 3!=2 * 2 * 3 * 3", "answer=2 * 3 * 3 * 3", "input=2 * 2 * 3 * 3"), + Iteration( + "x(x^2 − x)/3 − 4y!=(x^2 − x)/3 − 4y", "answer=x(x^2 − x)/3 − 4y", "input=(x^2 − x)/3 − 4y" + ), + Iteration( + "(x^2 − x)/3 + 4y!=(x^2 − x)/3 − 4y", "answer=(x^2 − x)/3 + 4y", "input=(x^2 − x)/3 − 4y" + ), + Iteration( + "(x^2 + x)/3 - 4y!=(x^2 − x)/3 − 4y", "answer=(x^2 + x)/3 - 4y", "input=(x^2 − x)/3 − 4y" + ), + Iteration( + "(x^2 − x)*0.33 - 4y!=(x^2 − x)/3 − 4y", + "answer=(x^2 − x)*0.33 - 4y", + "input=(x^2 − x)/3 − 4y" + ), + Iteration( + "(a + b + c)^3!=a^2+b^2+c^2+2a*b+2a*c+2bc", + "answer=(a + b + c)^3", + "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + ), + Iteration( + "a^2 + b^2 + c^2!=a^2+b^2+c^2+2a*b+2a*c+2bc", + "answer=a^2 + b^2 + c^2", + "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + ), + Iteration( + "a^2 + b^2 + c^2- 2a*b - 2a*c - 2bc!=a^2+b^2+c^2+2a*b+2a*c+2bc", + "answer=a^2 + b^2 + c^2- 2a*b - 2a*c - 2bc", + "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + ), + Iteration("9x^2 - 6x - 1!=(3x-1)^2", "answer=9x^2 - 6x - 1", "input=(3x-1)^2"), + Iteration("(3x −1)!=(3x-1)^2", "answer=(3x −1)", "input=(3x-1)^2"), + Iteration("2x!=sqrt(2x)^2", "answer=2x", "input=sqrt(2x)^2"), + Iteration("2x!=sqrt(-4x^2)", "answer=2x", "input=sqrt(-4x^2)"), + Iteration("x^2+2x+1!=(x+2)^2", "answer=x^2+2x+1", "input=(x+2)^2"), + Iteration("x^2-1!=(x+1)(1-x)", "answer=x^2-1", "input=(x+1)(1-x)"), + Iteration("x+1!=(x^2+2x+1)/(x-1)", "answer=x+1", "input=(x^2+2x+1)/(x-1)"), + Iteration("x-1!=(x^2-1)/x", "answer=x-1", "input=(x^2-1)/x"), + Iteration("x+1!=(x^2-1)/(x-2)", "answer=x+1", "input=(x^2-1)/(x-2)"), + Iteration("-3x!=(9x^3)^(1/3)", "answer=-3x", "input=(9x^3)^(1/3)"), + Iteration("Y==Y+5", "answer=Y", "input=Y+5"), + Iteration("5==Y+5", "answer=5", "input=Y+5") + ) + fun testMatches_assortedExpressions_withoutMatchingCharacteristics_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // This verifies a variety of expressions that per the PRD and technical specification should + // not pass for this classifier. + assertThat(matches).isFalse() + } + + private fun matchesClassifier( + answerExpression: InteractionObject, + inputExpression: InteractionObject, + allowedVariables: List = allPossibleVariables + ): Boolean { + return classifier.matches( + answerExpression, + inputs = mapOf("x" to inputExpression), + classificationContext = ClassificationContext( + customizationArgs = mapOf( + "customOskLetters" to SchemaObject.newBuilder().apply { + schemaObjectList = SchemaObjectList.newBuilder().apply { + addAllSchemaObject( + allowedVariables.map { + SchemaObject.newBuilder().setNormalizedString(it).build() + } + ) + }.build() + }.build() + ) + ) + ) + } + + private fun createMathExpression(rawExpression: String) = InteractionObject.newBuilder().apply { + mathExpression = rawExpression + }.build() + + private fun setUpTestApplicationComponent() { + DaggerAlgebraicExpressionInputIsEquivalentToRuleClassifierProviderTest_TestApplicationComponent + .builder() + .setApplication(ApplicationProvider.getApplicationContext()).build().inject(this) + } + + // TODO(#89): Move this to a common test application component. + @Module + class TestModule { + @Provides + @Singleton + fun provideContext(application: Application): Context { + return application + } + } + + // TODO(#89): Move this to a common test application component. + @Singleton + @Component( + modules = [ + TestModule::class, LocaleProdModule::class, FakeOppiaClockModule::class, + TestDispatcherModule::class, LoggerModule::class, RobolectricModule::class + ] + ) + interface TestApplicationComponent { + @Component.Builder + interface Builder { + @BindsInstance + fun setApplication(application: Application): Builder + + fun build(): TestApplicationComponent + } + + fun inject(test: AlgebraicExpressionInputIsEquivalentToRuleClassifierProviderTest) + } +} diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProviderTest.kt new file mode 100644 index 00000000000..0c6e401a976 --- /dev/null +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProviderTest.kt @@ -0,0 +1,649 @@ +package org.oppia.android.domain.classify.rules.algebraicexpressioninput + +import android.app.Application +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import com.google.common.truth.Truth.assertThat +import dagger.BindsInstance +import dagger.Component +import dagger.Module +import dagger.Provides +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.oppia.android.app.model.InteractionObject +import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.RuleClassifier +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Iteration +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Parameter +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.RunParameterized +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.SelectRunnerPlatform +import org.oppia.android.testing.junit.ParameterizedRobolectricTestRunner +import org.oppia.android.testing.robolectric.RobolectricModule +import org.oppia.android.testing.threading.TestDispatcherModule +import org.oppia.android.testing.time.FakeOppiaClockModule +import org.oppia.android.util.locale.LocaleProdModule +import org.oppia.android.util.logging.LoggerModule +import org.robolectric.annotation.Config +import org.robolectric.annotation.LooperMode +import javax.inject.Inject +import javax.inject.Singleton +import org.oppia.android.app.model.SchemaObject +import org.oppia.android.app.model.SchemaObjectList +import org.oppia.android.domain.classify.ClassificationContext + +/** + * Tests for [AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider]. + * + * Note that the tests implemented in this suite are specifically set up to verify the cases + * outlined in this sheet: + * https://docs.google.com/spreadsheets/d/1u1fQdah2WsmdYKWKGmuXy5TPT7Ot-b8A7O9iZF-j5XE/edit#gid=0. + */ +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") +@RunWith(OppiaParameterizedTestRunner::class) +@SelectRunnerPlatform(ParameterizedRobolectricTestRunner::class) +@LooperMode(LooperMode.Mode.PAUSED) +@Config(manifest = Config.NONE) +class AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProviderTest { + @Inject + internal lateinit var provider: AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider + + @Parameter lateinit var answer: String + @Parameter lateinit var input: String + + private lateinit var classifier: RuleClassifier + private val allPossibleVariables = (('a'..'z') + ('A'..'Z')).toList().map { it.toString() } + + @Before + fun setUp() { + setUpTestApplicationComponent() + classifier = provider.createRuleClassifier() + } + + @Test + fun testMatches_answerHasDisallowedVariable_returnsFalse() { + val answerExpression = createMathExpression("y") + val inputExpression = createMathExpression("y") + + val matches = matchesClassifier(answerExpression, inputExpression, allowedVariables = listOf()) + + // Despite the answer otherwise being equal, the variable isn't allowed. This shouldn't actually + // be the case in practice since neither the creator nor the learner would be allowed to input a + // disallowed variable (so this check is mainly a "just-in-case"). + assertThat(matches).isFalse() + } + + @Test + @RunParameterized( + Iteration("0==0", "answer=0", "input=0"), + Iteration("1==1", "answer=1", "input=1"), + Iteration("2==2", "answer=2", "input=2"), + Iteration("x==x", "answer=x", "input=x"), + Iteration("y==y", "answer=y", "input=y") + ) + fun testMatches_sameSingleTerms_returnsTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // If the two expressions are exactly the same, the classifier should match. + assertThat(matches).isTrue() + } + + @Test + @RunParameterized( + Iteration("-2==-2", "answer=-2", "input=-2"), + Iteration("1+3.14==1+3.14", "answer=1+3.14", "input=1+3.14"), + Iteration(" 1 + 3.14 ==1+3.14", "answer= 1 + 3.14 ", "input=1+3.14"), + Iteration("1+2+3==1+2+3", "answer=1+2+3", "input=1+2+3"), + Iteration("1-3.14==1-3.14", "answer=1-3.14", "input=1-3.14"), + Iteration("2*3.14==2*3.14", "answer=2*3.14", "input=2*3.14"), + Iteration("2/3==2/3", "answer=2/3", "input=2/3"), + Iteration("2/3.14==2/3.14", "answer=2/3.14", "input=2/3.14"), + Iteration("2^3==2^3", "answer=2^3", "input=2^3"), + Iteration("2^3.14==2^3.14", "answer=2^3.14", "input=2^3.14"), + Iteration("sqrt(2)==sqrt(2)", "answer=sqrt(2)", "input=sqrt(2)"), + Iteration("-x==-x", "answer=-x", "input=-x"), + Iteration("x+3.14==x+3.14", "answer=x+3.14", "input=x+3.14"), + Iteration("x-3.14==x-3.14", "answer=x-3.14", "input=x-3.14"), + Iteration("x*3.14==x*3.14", "answer=x*3.14", "input=x*3.14"), + Iteration("x/3==x/3", "answer=x/3", "input=x/3"), + Iteration("sqrt(x)==sqrt(x)", "answer=sqrt(x)", "input=sqrt(x)") + ) + fun testMatches_sameSingleOperations_returnsTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // If the two expressions are exactly the same, the classifier should match. + assertThat(matches).isTrue() + } + + @Test + @RunParameterized( + Iteration("1!=0", "answer=1", "input=0"), + Iteration("0!=1", "answer=0", "input=1"), + Iteration("3.14!=1", "answer=3.14", "input=1"), + Iteration("1!=3.14", "answer=1", "input=3.14"), + Iteration("x!=3.14", "answer=x", "input=3.14"), + Iteration("y!=x", "answer=y", "input=x"), + Iteration("3.14!=x", "answer=3.14", "input=x") + ) + fun testMatches_differentSingleTerms_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // If the two expressions aren't exactly the same (minus whitespace), they won't match. + assertThat(matches).isFalse() + } + + @Test + @RunParameterized( + Iteration("3.14+1!=1+3.14", "answer=3.14+1", "input=1+3.14"), + Iteration("3+2+1!=1+2+3", "answer=3+2+1", "input=1+2+3"), + Iteration("-3.14+1!=1-3.14", "answer=-3.14+1", "input=1-3.14"), + Iteration("3.14*2!=2*3.14", "answer=3.14*2", "input=2*3.14"), + Iteration("2+x!=x+2", "answer=2+x", "input=x+2"), + Iteration("y+x!=x+y", "answer=y+x", "input=x+y"), + Iteration("x*2!=2x", "answer=x*2", "input=2x"), + Iteration("yx!=xy", "answer=yx", "input=xy") + ) + fun testMatches_operationsDiffer_byCommutativity_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // This classifier expects commutativity to be retained. + assertThat(matches).isFalse() + } + + @Test + @RunParameterized( + Iteration("1+(2+3)!=(1+2)+3", "answer=1+(2+3)", "input=(1+2)+3"), + Iteration("2*(3*4)!=(2*3)*4", "answer=2*(3*4)", "input=(2*3)*4"), + Iteration("x+(2+3)!=(x+2)+3", "answer=x+(2+3)", "input=(x+2)+3"), + Iteration("x+(y+z)!=(x+y)+z", "answer=x+(y+z)", "input=(x+y)+z"), + Iteration("2*(3x)!=(2x)*3", "answer=2*(3x)", "input=(2x)*3"), + Iteration("x(yz)!=(xy)z", "answer=x(yz)", "input=(xy)z") + ) + fun testMatches_operationsDiffer_byAssociativity_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // This classifier expects associativity to be retained. + assertThat(matches).isFalse() + } + + @Test + @RunParameterized( + Iteration("3.14-1!=1-3.14", "answer=3.14-1", "input=1-3.14"), + Iteration("1-(2-3)!=(1-2)-3", "answer=1-(2-3)", "input=(1-2)-3"), + Iteration("3.14/2!=2/3.14", "answer=3.14/2", "input=2/3.14"), + Iteration("2/(3/4)!=(2/3)/4", "answer=2/(3/4)", "input=(2/3)/4"), + Iteration("3.14^2!=2^3.14", "answer=3.14^2", "input=2^3.14"), + Iteration("3.14-x!=x-3.14", "answer=3.14-x", "input=x-3.14"), + Iteration("x-(y-z)!=(x-y)-z", "answer=x-(y-z)", "input=(x-y)-z"), + Iteration("3.14/x!=x/3.14", "answer=3.14/x", "input=x/3.14"), + Iteration("x/(y/z)!=(x/y)/z", "answer=x/(y/z)", "input=(x/y)/z") + ) + fun testMatches_operationsDiffer_byNonCommutativeOrAssociativeReordering_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // Non-commutative and non-associative reordering generally results in a different value, so the + // classifier will fail to match. + assertThat(matches).isFalse() + } + + @Test + @RunParameterized( + Iteration("1-2!=-(2-1)", "answer=1-2", "input=-(2-1)"), + Iteration("1+2!=1+1+1", "answer=1+2", "input=1+1+1"), + Iteration("1+2!=1-(-2)", "answer=1+2", "input=1-(-2)"), + Iteration("4-6!=1-2-1", "answer=4-6", "input=1-2-1"), + Iteration("2*3*2*2!=2*3*4", "answer=2*3*2*2", "input=2*3*4"), + Iteration("-6-2!=2*-(3+1)", "answer=-6-2", "input=2*-(3+1)"), + Iteration("2/3/2/2!=2/3/4", "answer=2/3/2/2", "input=2/3/4"), + Iteration("2^(2+1)!=2^3", "answer=2^(2+1)", "input=2^3"), + Iteration("2^(-1)!=1/2", "answer=2^(-1)", "input=1/2"), + Iteration("x-y!=-(y-x)", "answer=x-y", "input=-(y-x)"), + Iteration("2+x!=1+x+1", "answer=2+x", "input=1+x+1"), + Iteration("1+x!=1-(-x)", "answer=1+x", "input=1-(-x)"), + Iteration("x!=1-x-1", "answer=x", "input=1-x-1"), + Iteration("4x!=2*2*x", "answer=4x", "input=2*2*x"), + Iteration("2-6x!=2*(-3x+1)", "answer=2-6x", "input=2*(-3x+1)"), + Iteration("x/4!=x/2/2", "answer=x/4", "input=x/2/2"), + Iteration("x^(2+1)!=x^3", "answer=x^(2+1)", "input=x^3"), + Iteration("x*(2^(-1))!=x/2", "answer=x*(2^(-1))", "input=x/2") + ) + fun testMatches_operationsDiffer_byDistributionAndCombining_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // This classifier doesn't support distributing or combining terms. + assertThat(matches).isFalse() + } + + @Test + @RunParameterized( + Iteration("2*(2+6+3+4)==2*(2+6+3+4)", "answer=2*(2+6+3+4)", "input=2*(2+6+3+4)"), + Iteration("2 × (2+6+3+4)==2*(2+6+3+4)", "answer=2 × (2+6+3+4)", "input=2*(2+6+3+4)"), + Iteration( + "15 - (6 × 2) + 3==15 - (6 × 2) + 3", "answer=15 - (6 × 2) + 3", "input=15 - (6 × 2) + 3" + ), + Iteration( + "2 * (50 + 150 + 100 + 25) ==2 × (50 + 150 + 100 + 25)", + "answer=2 * (50 + 150 + 100 + 25) ", + "input=2 × (50 + 150 + 100 + 25)" + ), + Iteration("5+2==5+2", "answer=5+2", "input=5+2"), + Iteration("6 − (− 4)==6 − (− 4)", "answer=6 − (− 4)", "input=6 − (− 4)"), + Iteration("6-(-4)==6 − (− 4)", "answer=6-(-4)", "input=6 − (− 4)"), + Iteration("3 * 10^-5==3 * 10^-5", "answer=3 * 10^-5", "input=3 * 10^-5"), + Iteration( + "1000 + 200 + 30 + 4 + 0.5 + 0.06==1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=1000 + 200 + 30 + 4 + 0.5 + 0.06", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration("2 * 2 * 3 * 3==2 * 2 * 3 * 3", "answer=2 * 2 * 3 * 3", "input=2 * 2 * 3 * 3"), + Iteration("4x^2+20x==4*x^2+20x", "answer=4x^2+20x", "input=4*x^2+20x"), + Iteration("3+x-5==3+x-5", "answer=3+x-5", "input=3+x-5"), + Iteration("Z+A-Z==Z+A-Z", "answer=Z+A-Z", "input=Z+A-Z"), + Iteration("6C - 5A -1==6C - 5A -1", "answer=6C - 5A -1", "input=6C - 5A -1"), + Iteration("5Z-w==5*Z-w", "answer=5Z-w", "input=5*Z-w"), + Iteration("5*Z-w==5*Z-w", "answer=5*Z-w", "input=5*Z-w"), + Iteration("LS-3S+L==L*S-3S+L", "answer=LS-3S+L", "input=L*S-3S+L"), + Iteration("L*S-3S+L==L*S-3S+L", "answer=L*S-3S+L", "input=L*S-3S+L"), + Iteration("L*S-3*S+L==L*S-3S+L", "answer=L*S-3*S+L", "input=L*S-3S+L"), + Iteration("LS-3*S+L==L*S-3S+L", "answer=LS-3*S+L", "input=L*S-3S+L"), + Iteration("9x^2 − 6x + 1==9x^2 − 6x + 1", "answer=9x^2 − 6x + 1", "input=9x^2 − 6x + 1"), + Iteration("c*b-c==c*b-c", "answer=c*b-c", "input=c*b-c"), + Iteration("cb-c==c*b-c", "answer=cb-c", "input=c*b-c"), + Iteration("x^2+y+4x==x^2+y+4x", "answer=x^2+y+4x", "input=x^2+y+4x"), + Iteration("Y+5==Y+5", "answer=Y+5", "input=Y+5"), + Iteration( + "a^2 + b^2 + c^2+ 2a*b + 2a*c + 2bc==a^2+b^2+c^2+2a*b+2a*c+2bc", + "answer=a^2 + b^2 + c^2+ 2a*b + 2a*c + 2bc", + "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + ), + Iteration( + "(x^2 − x)/3 − 4y==(x^2 − x)/3 − 4y", "answer=(x^2 − x)/3 − 4y", "input=(x^2 − x)/3 − 4y" + ), + Iteration("(3x -1)^2==(3x-1)^2", "answer=(3x -1)^2", "input=(3x-1)^2") + ) + fun testMatches_assortedExpressions_withMatchingCharacteristics_returnsTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // This verifies a variety of expressions that per the PRD and technical specification should + // pass for this classifier. + assertThat(matches).isTrue() + } + + @Test + @RunParameterized( + Iteration( + "2 × (50 + 150 + 100 + 25) ==(50 + 150 + 100 + 25) × 2", + "answer=2 × (50 + 150 + 100 + 25) ", + "input=(50 + 150 + 100 + 25) × 2" + ), + Iteration("2+5!=5+2", "answer=2+5", "input=5+2"), + Iteration("− (− 4) + 6!=6 − (− 4)", "answer=− (− 4) + 6", "input=6 − (− 4)"), + Iteration("10!=6 − (− 4)", "answer=10", "input=6 − (− 4)"), + Iteration("6 + 4!=6 − (− 4)", "answer=6 + 4", "input=6 − (− 4)"), + Iteration("6 + 2^2!=6 − (− 4)", "answer=6 + 2^2", "input=6 − (− 4)"), + Iteration("3 * 2 − (− 4)!=6 − (− 4)", "answer=3 * 2 − (− 4)", "input=6 − (− 4)"), + Iteration("100/10!=6 − (− 4)", "answer=100/10", "input=6 − (− 4)"), + Iteration("3/(10 * 10^4)!=3 * 10^-5", "answer=3/(10 * 10^4)", "input=3 * 10^-5"), + Iteration( + "200 + 30 + 4 + 0.5 + 0.06 + 1000!=1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=200 + 30 + 4 + 0.5 + 0.06 + 1000", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration( + "0.06 + 0.5 + 4 + 30 + 200 + 1000!=1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=0.06 + 0.5 + 4 + 30 + 200 + 1000", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration( + "1234.56!=1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=1234.56", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration( + "123456/100!=1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=123456/100", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration( + "61728/50!=1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=61728/50", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration( + "1234 + 56/100!=1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=1234 + 56/100", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration( + "1230 + 4.56!=1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=1230 + 4.56", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration( + "2 * 2 * 3 * 3 * 1!=2 * 2 * 3 * 3", "answer=2 * 2 * 3 * 3 * 1", "input=2 * 2 * 3 * 3" + ), + Iteration("2 * 2 * 9!=2 * 2 * 3 * 3", "answer=2 * 2 * 9", "input=2 * 2 * 3 * 3"), + Iteration("4 * 3^2!=2 * 2 * 3 * 3", "answer=4 * 3^2", "input=2 * 2 * 3 * 3"), + Iteration("8/2 * 3 * 3!=2 * 2 * 3 * 3", "answer=8/2 * 3 * 3", "input=2 * 2 * 3 * 3"), + Iteration("36!=2 * 2 * 3 * 3", "answer=36", "input=2 * 2 * 3 * 3"), + Iteration("sqrt(4-2)!=sqrt(2)", "answer=sqrt(4-2)", "input=sqrt(2)"), + Iteration("bc-c!=c*b-c", "answer=bc-c", "input=c*b-c"), + Iteration("-c+bc!=c*b-c", "answer=-c+bc", "input=c*b-c"), + Iteration("-c+cb!=c*b-c", "answer=-c+cb", "input=c*b-c"), + Iteration("y+4x+x^2!=x^2+y+4x", "answer=y+4x+x^2", "input=x^2+y+4x"), + Iteration("x^2+4x+y!=x^2+y+4x", "answer=x^2+4x+y", "input=x^2+y+4x"), + Iteration("5+Y!=Y+5", "answer=5+Y", "input=Y+5"), + Iteration( + "a^2 + b^2 + c^2+ 2a*b + 2bc + 2a*c!=a^2+b^2+c^2+2a*b+2a*c+2bc", + "answer=a^2 + b^2 + c^2+ 2a*b + 2bc + 2a*c", + "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + ), + Iteration( + "2a*b + 2bc + 2a*c + a^2 + b^2 + c^2!=a^2+b^2+c^2+2a*b+2a*c+2bc", + "answer=2a*b + 2bc + 2a*c + a^2 + b^2 + c^2", + "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + ), + Iteration( + "2a*b + b^2 + c^2+ a^2 + 2bc + 2a*c!=a^2+b^2+c^2+2a*b+2a*c+2bc", + "answer=2a*b + b^2 + c^2+ a^2 + 2bc + 2a*c", + "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + ), + Iteration( + "a*a + b*b + c*c + 2*a*b + 2*a*c + 2*b*c!=a^2+b^2+c^2+2a*b+2a*c+2bc", + "answer=a*a + b*b + c*c + 2*a*b + 2*a*c + 2*b*c", + "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + ), + Iteration( + "(a+ b)^2 + c^2 + 2bc + 2a*c!=a^2+b^2+c^2+2a*b+2a*c+2bc", + "answer=(a+ b)^2 + c^2 + 2bc + 2a*c", + "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + ), + Iteration( + "(a+b+c)^2!=a^2+b^2+c^2+2a*b+2a*c+2bc", "answer=(a+b+c)^2", "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + ), + Iteration( + "(-a -b -c)^2!=a^2+b^2+c^2+2a*b+2a*c+2bc", "answer=(-a -b -c)^2", "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + ), + Iteration("1 - 6x + 9x^2!=9x^2 − 6x + 1", "answer=1 - 6x + 9x^2", "input=9x^2 − 6x + 1"), + Iteration("9x^2 + 1 - 6x!=9x^2 − 6x + 1", "answer=9x^2 + 1 - 6x", "input=9x^2 − 6x + 1"), + Iteration("2+1+x!=x+1+2", "answer=2+1+x", "input=x+1+2"), + Iteration("1+2+x!=x+1+2", "answer=1+2+x", "input=x+1+2"), + Iteration("1+x+2!=x+1+2", "answer=1+x+2", "input=x+1+2"), + Iteration("2+x+1!=x+1+2", "answer=2+x+1", "input=x+1+2"), + Iteration("(x+1)+2!=x+1+2", "answer=(x+1)+2", "input=x+1+2"), + Iteration("x + (1+2)!=x+1+2", "answer=x + (1+2)", "input=x+1+2"), + Iteration( + "y+1+ 9x(x − 6)!=9x(x − 6) + 1+ y", "answer=y+1+ 9x(x − 6)", "input=9x(x − 6) + 1+ y" + ), + Iteration("1+y+9x(x − 6)!=9x(x − 6) + 1+ y", "answer=1+y+9x(x − 6)", "input=9x(x − 6) + 1+ y"), + Iteration( + "1 + 9x(x − 6) + y!=9x(x − 6) + 1+ y", "answer=1 + 9x(x − 6) + y", "input=9x(x − 6) + 1+ y" + ), + Iteration( + "(y+1)+9x(x − 6)!=9x(x − 6) + 1+ y", "answer=(y+1)+9x(x − 6)", "input=9x(x − 6) + 1+ y" + ), + Iteration( + "-4y + (x^2 − x)/3!=(x^2 − x)/3 − 4y", "answer=-4y + (x^2 − x)/3", "input=(x^2 − x)/3 − 4y" + ), + Iteration( + "x(x − 1)/3 −4y!=(x^2 − x)/3 − 4y", "answer=x(x − 1)/3 −4y", "input=(x^2 − x)/3 − 4y" + ), + Iteration( + "x^2/3 − x/3 − 4y!=(x^2 − x)/3 − 4y", "answer=x^2/3 − x/3 − 4y", "input=(x^2 − x)/3 − 4y" + ), + Iteration( + "x^2/3 − (x/3 + 4y)!=(x^2 − x)/3 − 4y", "answer=x^2/3 − (x/3 + 4y)", "input=(x^2 − x)/3 − 4y" + ), + Iteration("√(3x −1)4!=(3x-1)^2", "answer=√(3x −1)4", "input=(3x-1)^2"), + Iteration("3x(3x - 2) + 1!=(3x-1)^2", "answer=3x(3x - 2) + 1", "input=(3x-1)^2"), + Iteration("3(3x^2) - 6x +1!=(3x-1)^2", "answer=3(3x^2) - 6x +1", "input=(3x-1)^2"), + Iteration("2x!=sqrt(4x^2)", "answer=2x", "input=sqrt(4x^2)"), + Iteration("x^2+2x+1!=(x+1)^2", "answer=x^2+2x+1", "input=(x+1)^2"), + Iteration("x^2-1!=(x+1)(x-1)", "answer=x^2-1", "input=(x+1)(x-1)"), + Iteration("x+1!=(x^2+2x+1)/(x+1)", "answer=x+1", "input=(x^2+2x+1)/(x+1)"), + Iteration("x-1!=(x^2-1)/(x+1)", "answer=x-1", "input=(x^2-1)/(x+1)"), + Iteration("x+1!=(x^2-1)/(x-1)", "answer=x+1", "input=(x^2-1)/(x-1)"), + Iteration("-3x!=(-27x^3)^(1/3)", "answer=-3x", "input=(-27x^3)^(1/3)"), + Iteration("1!=(x^2-1)/(x^2-1)", "answer=1", "input=(x^2-1)/(x^2-1)"), + Iteration("2*(6+3+4) + 4!=2*(2+6+3+4)", "answer=2*(6+3+4) + 4", "input=2*(2+6+3+4)"), + Iteration("2*(2+6+3) + 8!=2*(2+6+3+4)", "answer=2*(2+6+3) + 8", "input=2*(2+6+3+4)"), + Iteration("(2+6+3+4)*2!=2*(2+6+3+4)", "answer=(2+6+3+4)*2", "input=2*(2+6+3+4)"), + Iteration("(2+6+3+4) × 2!=2*(2+6+3+4)", "answer=(2+6+3+4) × 2", "input=2*(2+6+3+4)"), + Iteration("15 - 12 + 3!=15 - (6 × 2) + 3", "answer=15 - 12 + 3", "input=15 - (6 × 2) + 3"), + Iteration( + "3 - (6 * 2) + 15!=15 - (6 × 2) + 3", "answer=3 - (6 * 2) + 15", "input=15 - (6 × 2) + 3" + ), + Iteration( + "15 - (2 × 6) + 3!=15 - (6 × 2) + 3", "answer=15 - (2 × 6) + 3", "input=15 - (6 × 2) + 3" + ), + Iteration( + "2 *(50 + 150) + 2*(100 + 25)!=(50 + 150 + 100 + 25) × 2", + "answer=2 *(50 + 150) + 2*(100 + 25)", + "input=(50 + 150 + 100 + 25) × 2" + ), + Iteration( + "2* ( 25+50+100+150)!=(50 + 150 + 100 + 25) × 2", + "answer=2* ( 25+50+100+150)", + "input=(50 + 150 + 100 + 25) × 2" + ), + Iteration("10^−5 * 3!=3 * 10^-5", "answer=10^−5 * 3", "input=3 * 10^-5"), + Iteration("3 * 10^5!=3 * 10^-5", "answer=3 * 10^5", "input=3 * 10^-5"), + Iteration("2 * 10^−5!=3 * 10^-5", "answer=2 * 10^−5", "input=3 * 10^-5"), + Iteration("5 * 10^−3!=3 * 10^-5", "answer=5 * 10^−3", "input=3 * 10^-5"), + Iteration("30 * 10^−6!=3 * 10^-5", "answer=30 * 10^−6", "input=3 * 10^-5"), + Iteration("0.00003!=3 * 10^-5", "answer=0.00003", "input=3 * 10^-5"), + Iteration("3/10^5!=3 * 10^-5", "answer=3/10^5", "input=3 * 10^-5"), + Iteration( + "123456!=1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=123456", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration( + "1000 + 200 + 30!=1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=1000 + 200 + 30", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration("3 *2 – (− 4)!=6 − (− 4)", "answer=3 *2 – (− 4)", "input=6 − (− 4)"), + Iteration("6 − 4!=6 − (− 4)", "answer=6 − 4", "input=6 − (− 4)"), + Iteration("6 + (− 4)!=6 − (− 4)", "answer=6 + (− 4)", "input=6 − (− 4)"), + Iteration("100!=6 − (− 4)", "answer=100", "input=6 − (− 4)"), + Iteration("7!=5+2", "answer=7", "input=5+2"), + Iteration("3+4!=5+2", "answer=3+4", "input=5+2"), + Iteration("2 * 2 * 3!=2 * 2 * 3 * 3", "answer=2 * 2 * 3", "input=2 * 2 * 3 * 3"), + Iteration("2 * 3 * 3 * 3!=2 * 2 * 3 * 3", "answer=2 * 3 * 3 * 3", "input=2 * 2 * 3 * 3"), + Iteration("20x+4x^2!=4*x^2+20x", "answer=20x+4x^2", "input=4*x^2+20x"), + Iteration("x-5+3!=3+x-5", "answer=x-5+3", "input=3+x-5"), + Iteration("-5+3+x!=3+x-5", "answer=-5+3+x", "input=3+x-5"), + Iteration("-5+x+3!=3+x-5", "answer=-5+x+3", "input=3+x-5"), + Iteration("3+(x-5)!=3+x-5", "answer=3+(x-5)", "input=3+x-5"), + Iteration("A!=Z+A-Z", "answer=A", "input=Z+A-Z"), + Iteration("A+Z-Z!=Z+A-Z", "answer=A+Z-Z", "input=Z+A-Z"), + Iteration("Z+(A-Z)!=Z+A-Z", "answer=Z+(A-Z)", "input=Z+A-Z"), + Iteration("6C - (5A+1)!=6C - 5A -1", "answer=6C - (5A+1)", "input=6C - 5A -1"), + Iteration("-5A-1+6C!=6C - 5A -1", "answer=-5A-1+6C", "input=6C - 5A -1"), + Iteration("-W+5Z!=5*Z-W", "answer=-W+5Z", "input=5*Z-W"), + Iteration("L(1+S)-3S!=L*S-3S+L", "answer=L(1+S)-3S", "input=L*S-3S+L"), + Iteration("S(L-3)+L!=L*S-3S+L", "answer=S(L-3)+L", "input=L*S-3S+L"), + Iteration("L+LS-3S!=L*S-3S+L", "answer=L+LS-3S", "input=L*S-3S+L"), + Iteration( + "x(x − 1)/3 − 4y!=(x^2 − x)/3 − 4y", "answer=x(x − 1)/3 − 4y", "input=(x^2 − x)/3 − 4y" + ), + Iteration( + "- 4y + (x^2 − x)/3!=(x^2 − x)/3 − 4y", "answer=- 4y + (x^2 − x)/3", "input=(x^2 − x)/3 − 4y" + ), + Iteration( + "(x^2 − x) * 3^-1 − 4y!=(x^2 − x)/3 − 4y", + "answer=(x^2 − x) * 3^-1 − 4y", + "input=(x^2 − x)/3 − 4y" + ), + Iteration( + "x(x^2 − x)/3 − 4y!=(x^2 − x)/3 − 4y", "answer=x(x^2 − x)/3 − 4y", "input=(x^2 − x)/3 − 4y" + ), + Iteration( + "(x^2 − x)/3 + 4y!=(x^2 − x)/3 − 4y", "answer=(x^2 − x)/3 + 4y", "input=(x^2 − x)/3 − 4y" + ), + Iteration( + "(x^2 + x)/3 - 4y!=(x^2 − x)/3 − 4y", "answer=(x^2 + x)/3 - 4y", "input=(x^2 − x)/3 − 4y" + ), + Iteration( + "(x^2 − x)*0.33 - 4y!=(x^2 − x)/3 − 4y", + "answer=(x^2 − x)*0.33 - 4y", + "input=(x^2 − x)/3 − 4y" + ), + Iteration( + "a^2 + b^2 + c^2 + 2(a*b + a*c + bc)!=a^2+b^2+c^2+2a*b+2a*c+2bc", + "answer=a^2 + b^2 + c^2 + 2(a*b + a*c + bc)", + "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + ), + Iteration( + "(a + b)^2 + c^2 + 2a*c + 2bc!=a^2+b^2+c^2+2a*b+2a*c+2bc", + "answer=(a + b)^2 + c^2 + 2a*c + 2bc", + "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + ), + Iteration( + "a * a + b * b + c^3/c + 2a*b + 2a*c + 2bc!=a^2+b^2+c^2+2a*b+2a*c+2bc", + "answer=a * a + b * b + c^3/c + 2a*b + 2a*c + 2bc", + "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + ), + Iteration( + "a^2+ b^2 + c^2 + 2bc + 2a*c + 2a*b!=a^2+b^2+c^2+2a*b+2a*c+2bc", + "answer=a^2+ b^2 + c^2 + 2bc + 2a*c + 2a*b", + "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + ), + Iteration( + "(a + b + c)^3!=a^2+b^2+c^2+2a*b+2a*c+2bc", + "answer=(a + b + c)^3", + "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + ), + Iteration( + "a^2 + b^2 + c^2!=a^2+b^2+c^2+2a*b+2a*c+2bc", + "answer=a^2 + b^2 + c^2", + "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + ), + Iteration( + "a^2 + b^2 + c^2- 2a*b - 2a*c - 2bc!=a^2+b^2+c^2+2a*b+2a*c+2bc", + "answer=a^2 + b^2 + c^2- 2a*b - 2a*c - 2bc", + "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + ), + Iteration("(3x − 1)^2!=9x^2 − 6x + 1", "answer=(3x − 1)^2", "input=9x^2 − 6x + 1"), + Iteration("3x(3x − 2) + 1!=9x^2 − 6x + 1", "answer=3x(3x − 2) + 1", "input=9x^2 − 6x + 1"), + Iteration("3(3x^2 − 2x) + 1!=9x^2 − 6x + 1", "answer=3(3x^2 − 2x) + 1", "input=9x^2 − 6x + 1"), + Iteration("(3x)^2 − 6x + 1!=9x^2 − 6x + 1", "answer=(3x)^2 − 6x + 1", "input=9x^2 − 6x + 1"), + Iteration("c(b-1)!=c*b-c", "answer=c(b-1)", "input=c*b-c"), + Iteration("x(x+4)+y!=x^2+y+4x", "answer=x(x+4)+y", "input=x^2+y+4x"), + Iteration("Y!=Y+5", "answer=Y", "input=Y+5"), + Iteration("5!=Y+5", "answer=5", "input=Y+5"), + Iteration("x+3!=x+1+2", "answer=x+3", "input=x+1+2"), + Iteration("(1 - 3x)^2!=(3x-1)^2", "answer=(1 - 3x)^2", "input=(3x-1)^2"), + Iteration("9x^2 - 6x - 1!=(3x-1)^2", "answer=9x^2 - 6x - 1", "input=(3x-1)^2"), + Iteration("(3x −1)!=(3x-1)^2", "answer=(3x −1)", "input=(3x-1)^2"), + Iteration("2x!=sqrt(2x)^2", "answer=2x", "input=sqrt(2x)^2"), + Iteration("2x!=sqrt(-4x^2)", "answer=2x", "input=sqrt(-4x^2)"), + Iteration("x^2+2x+1!=(x+2)^2", "answer=x^2+2x+1", "input=(x+2)^2"), + Iteration("x^2-1!=(x+1)(1-x)", "answer=x^2-1", "input=(x+1)(1-x)"), + Iteration("x+1!=(x^2+2x+1)/(x-1)", "answer=x+1", "input=(x^2+2x+1)/(x-1)"), + Iteration("x-1!=(x^2-1)/x", "answer=x-1", "input=(x^2-1)/x"), + Iteration("x+1!=(x^2-1)/(x-2)", "answer=x+1", "input=(x^2-1)/(x-2)"), + Iteration("-3x!=(9x^3)^(1/3)", "answer=-3x", "input=(9x^3)^(1/3)") + ) + fun testMatches_assortedExpressions_withoutMatchingCharacteristics_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // This verifies a variety of expressions that per the PRD and technical specification should + // not pass for this classifier. + assertThat(matches).isFalse() + } + + private fun matchesClassifier( + answerExpression: InteractionObject, + inputExpression: InteractionObject, + allowedVariables: List = allPossibleVariables + ): Boolean { + return classifier.matches( + answerExpression, + inputs = mapOf("x" to inputExpression), + classificationContext = ClassificationContext( + customizationArgs = mapOf( + "customOskLetters" to SchemaObject.newBuilder().apply { + schemaObjectList = SchemaObjectList.newBuilder().apply { + addAllSchemaObject( + allowedVariables.map { + SchemaObject.newBuilder().setNormalizedString(it).build() + } + ) + }.build() + }.build() + ) + ) + ) + } + + private fun createMathExpression(rawExpression: String) = InteractionObject.newBuilder().apply { + mathExpression = rawExpression + }.build() + + private fun setUpTestApplicationComponent() { + DaggerAlgebraicExpressionInputMatchesExactlyWithRuleClassifierProviderTest_TestApplicationComponent + .builder() + .setApplication(ApplicationProvider.getApplicationContext()).build().inject(this) + } + + // TODO(#89): Move this to a common test application component. + @Module + class TestModule { + @Provides + @Singleton + fun provideContext(application: Application): Context { + return application + } + } + + // TODO(#89): Move this to a common test application component. + @Singleton + @Component( + modules = [ + TestModule::class, LocaleProdModule::class, FakeOppiaClockModule::class, + TestDispatcherModule::class, LoggerModule::class, RobolectricModule::class + ] + ) + interface TestApplicationComponent { + @Component.Builder + interface Builder { + @BindsInstance + fun setApplication(application: Application): Builder + + fun build(): TestApplicationComponent + } + + fun inject(test: AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProviderTest) + } +} diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt new file mode 100644 index 00000000000..19dd3fbc92e --- /dev/null +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt @@ -0,0 +1,666 @@ +package org.oppia.android.domain.classify.rules.algebraicexpressioninput + +import android.app.Application +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import com.google.common.truth.Truth.assertThat +import dagger.BindsInstance +import dagger.Component +import dagger.Module +import dagger.Provides +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.oppia.android.app.model.InteractionObject +import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.RuleClassifier +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Iteration +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Parameter +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.RunParameterized +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.SelectRunnerPlatform +import org.oppia.android.testing.junit.ParameterizedRobolectricTestRunner +import org.oppia.android.testing.robolectric.RobolectricModule +import org.oppia.android.testing.threading.TestDispatcherModule +import org.oppia.android.testing.time.FakeOppiaClockModule +import org.oppia.android.util.locale.LocaleProdModule +import org.oppia.android.util.logging.LoggerModule +import org.robolectric.annotation.Config +import org.robolectric.annotation.LooperMode +import javax.inject.Inject +import javax.inject.Singleton +import org.oppia.android.app.model.SchemaObject +import org.oppia.android.app.model.SchemaObjectList +import org.oppia.android.domain.classify.ClassificationContext + +/** + * Tests for [AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider]. + * + * Note that the tests implemented in this suite are specifically set up to verify the cases + * outlined in this sheet: + * https://docs.google.com/spreadsheets/d/1u1fQdah2WsmdYKWKGmuXy5TPT7Ot-b8A7O9iZF-j5XE/edit#gid=0. + */ +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") +@RunWith(OppiaParameterizedTestRunner::class) +@SelectRunnerPlatform(ParameterizedRobolectricTestRunner::class) +@LooperMode(LooperMode.Mode.PAUSED) +@Config(manifest = Config.NONE) +class AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest { + @Inject internal lateinit var provider: AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider + + @Parameter lateinit var answer: String + @Parameter lateinit var input: String + + private lateinit var classifier: RuleClassifier + private val allPossibleVariables = (('a'..'z') + ('A'..'Z')).toList().map { it.toString() } + + @Before + fun setUp() { + setUpTestApplicationComponent() + classifier = provider.createRuleClassifier() + } + + @Test + fun testMatches_answerHasDisallowedVariable_returnsFalse() { + val answerExpression = createMathExpression("y") + val inputExpression = createMathExpression("y") + + val matches = matchesClassifier(answerExpression, inputExpression, allowedVariables = listOf()) + + // Despite the answer otherwise being equal, the variable isn't allowed. This shouldn't actually + // be the case in practice since neither the creator nor the learner would be allowed to input a + // disallowed variable (so this check is mainly a "just-in-case"). + assertThat(matches).isFalse() + } + + @Test + @RunParameterized( + Iteration("0==0", "answer=0", "input=0"), + Iteration("1==1", "answer=1", "input=1"), + Iteration("2==2", "answer=2", "input=2"), + Iteration("x==x", "answer=x", "input=x"), + Iteration("y==y", "answer=y", "input=y") + ) + fun testMatches_sameSingleTerms_returnsTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // If the two expressions are exactly the same, the classifier should match. + assertThat(matches).isTrue() + } + + @Test + @RunParameterized( + Iteration("-2==-2", "answer=-2", "input=-2"), + Iteration("1+3.14==1+3.14", "answer=1+3.14", "input=1+3.14"), + Iteration(" 1 + 3.14 ==1+3.14", "answer= 1 + 3.14 ", "input=1+3.14"), + Iteration("1+2+3==1+2+3", "answer=1+2+3", "input=1+2+3"), + Iteration("1-3.14==1-3.14", "answer=1-3.14", "input=1-3.14"), + Iteration("2*3.14==2*3.14", "answer=2*3.14", "input=2*3.14"), + Iteration("2/3==2/3", "answer=2/3", "input=2/3"), + Iteration("2/3.14==2/3.14", "answer=2/3.14", "input=2/3.14"), + Iteration("2^3==2^3", "answer=2^3", "input=2^3"), + Iteration("2^3.14==2^3.14", "answer=2^3.14", "input=2^3.14"), + Iteration("sqrt(2)==sqrt(2)", "answer=sqrt(2)", "input=sqrt(2)"), + Iteration("-x==-x", "answer=-x", "input=-x"), + Iteration("x+3.14==x+3.14", "answer=x+3.14", "input=x+3.14"), + Iteration("x-3.14==x-3.14", "answer=x-3.14", "input=x-3.14"), + Iteration("x*3.14==x*3.14", "answer=x*3.14", "input=x*3.14"), + Iteration("x/3==x/3", "answer=x/3", "input=x/3"), + Iteration("sqrt(x)==sqrt(x)", "answer=sqrt(x)", "input=sqrt(x)") + ) + fun testMatches_sameSingleOperations_returnsTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // If the two expressions are exactly the same, the classifier should match. + assertThat(matches).isTrue() + } + + @Test + @RunParameterized( + Iteration("1!=0", "answer=1", "input=0"), + Iteration("0!=1", "answer=0", "input=1"), + Iteration("3.14!=1", "answer=3.14", "input=1"), + Iteration("1!=3.14", "answer=1", "input=3.14"), + Iteration("x!=3.14", "answer=x", "input=3.14"), + Iteration("y!=x", "answer=y", "input=x"), + Iteration("3.14!=x", "answer=3.14", "input=x") + ) + fun testMatches_differentSingleTerms_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // If the two expressions aren't exactly the same (minus whitespace and some minor term + // reordering), they won't match. + assertThat(matches).isFalse() + } + + @Test + @RunParameterized( + Iteration("3.14+1==1+3.14", "answer=3.14+1", "input=1+3.14"), + Iteration("3+2+1==1+2+3", "answer=3+2+1", "input=1+2+3"), + Iteration("-3.14+1==1-3.14", "answer=-3.14+1", "input=1-3.14"), + Iteration("3.14*2==2*3.14", "answer=3.14*2", "input=2*3.14"), + Iteration("2+x==x+2", "answer=2+x", "input=x+2"), + Iteration("y+x==x+y", "answer=y+x", "input=x+y"), + Iteration("x*2==2x", "answer=x*2", "input=2x"), + Iteration("yx==xy", "answer=yx", "input=xy") + ) + fun testMatches_operationsDiffer_byCommutativity_returnsTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // Reordering terms by commutativity is allowed by this classifier. + assertThat(matches).isTrue() + } + + @Test + @RunParameterized( + Iteration("1+(2+3)==(1+2)+3", "answer=1+(2+3)", "input=(1+2)+3"), + Iteration("2*(3*4)==(2*3)*4", "answer=2*(3*4)", "input=(2*3)*4"), + Iteration("x+(2+3)==(x+2)+3", "answer=x+(2+3)", "input=(x+2)+3"), + Iteration("x+(y+z)==(x+y)+z", "answer=x+(y+z)", "input=(x+y)+z"), + Iteration("2*(3x)==(2x)*3", "answer=2*(3x)", "input=(2x)*3"), + Iteration("x(yz)==(xy)z", "answer=x(yz)", "input=(xy)z") + ) + fun testMatches_operationsDiffer_byAssociativity_returnsTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // Changing operation associativity is allowed by this classifier. + assertThat(matches).isTrue() + } + + @Test + @RunParameterized( + Iteration("3.14-1!=1-3.14", "answer=3.14-1", "input=1-3.14"), + Iteration("1-(2-3)!=(1-2)-3", "answer=1-(2-3)", "input=(1-2)-3"), + Iteration("3.14/2!=2/3.14", "answer=3.14/2", "input=2/3.14"), + Iteration("2/(3/4)!=(2/3)/4", "answer=2/(3/4)", "input=(2/3)/4"), + Iteration("3.14^2!=2^3.14", "answer=3.14^2", "input=2^3.14"), + Iteration("3.14-x!=x-3.14", "answer=3.14-x", "input=x-3.14"), + Iteration("x-(y-z)!=(x-y)-z", "answer=x-(y-z)", "input=(x-y)-z"), + Iteration("3.14/x!=x/3.14", "answer=3.14/x", "input=x/3.14"), + Iteration("x/(y/z)!=(x/y)/z", "answer=x/(y/z)", "input=(x/y)/z") + ) + fun testMatches_operationsDiffer_byNonCommutativeOrAssociativeReordering_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // Non-commutative and non-associative reordering generally results in a different value, so the + // classifier will fail to match. + assertThat(matches).isFalse() + } + + @Test + @RunParameterized( + Iteration("1+2==1-(-2)", "answer=1+2", "input=1-(-2)"), + Iteration("1+x==1-(-x)", "answer=1+x", "input=1-(-x)") + ) + fun testMatches_operationsDiffer_byDistributingNegation_returnsTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // The classifier does support distributing negations (e.g. a*cross groups). + assertThat(matches).isTrue() + } + + @Test + @RunParameterized( + Iteration("x-y==-(y-x)", "answer=x-y", "input=-(y-x)"), + Iteration("1-2!=-(2-1)", "answer=1-2", "input=-(2-1)"), + Iteration("1+2!=1+1+1", "answer=1+2", "input=1+1+1"), + Iteration("4-6!=1-2-1", "answer=4-6", "input=1-2-1"), + Iteration("2*3*2*2!=2*3*4", "answer=2*3*2*2", "input=2*3*4"), + Iteration("-6-2!=2*-(3+1)", "answer=-6-2", "input=2*-(3+1)"), + Iteration("2/3/2/2!=2/3/4", "answer=2/3/2/2", "input=2/3/4"), + Iteration("2^(2+1)!=2^3", "answer=2^(2+1)", "input=2^3"), + Iteration("2^(-1)!=1/2", "answer=2^(-1)", "input=1/2"), + Iteration("2+x!=1+x+1", "answer=2+x", "input=1+x+1"), + Iteration("x!=1-x-1", "answer=x", "input=1-x-1"), + Iteration("4x!=2*2*x", "answer=4x", "input=2*2*x"), + Iteration("2-6x!=2*(-3x+1)", "answer=2-6x", "input=2*(-3x+1)"), + Iteration("x/4!=x/2/2", "answer=x/4", "input=x/2/2"), + Iteration("x^(2+1)!=x^3", "answer=x^(2+1)", "input=x^3"), + Iteration("x*(2^(-1))!=x/2", "answer=x*(2^(-1))", "input=x/2") + ) + fun testMatches_operationsDiffer_byDistributionAndCombining_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // This classifier doesn't support broadly distributing or combining terms. + assertThat(matches).isFalse() + } + + @Test + @RunParameterized( + Iteration("2*(2+6+3+4)==2*(2+6+3+4)", "answer=2*(2+6+3+4)", "input=2*(2+6+3+4)"), + Iteration("2 × (2+6+3+4)==2*(2+6+3+4)", "answer=2 × (2+6+3+4)", "input=2*(2+6+3+4)"), + Iteration( + "15 - (6 × 2) + 3==15 - (6 × 2) + 3", "answer=15 - (6 × 2) + 3", "input=15 - (6 × 2) + 3" + ), + Iteration( + "2 × (50 + 150 + 100 + 25) ==(50 + 150 + 100 + 25) × 2", + "answer=2 × (50 + 150 + 100 + 25) ", + "input=(50 + 150 + 100 + 25) × 2" + ), + Iteration( + "2 * (50 + 150 + 100 + 25) ==2 × (50 + 150 + 100 + 25)", + "answer=2 * (50 + 150 + 100 + 25) ", + "input=2 × (50 + 150 + 100 + 25)" + ), + Iteration("2+5==5+2", "answer=2+5", "input=5+2"), + Iteration("5+2==5+2", "answer=5+2", "input=5+2"), + Iteration("6 + 4!=6 − (− 4)", "answer=6 + 4", "input=6 − (− 4)"), + Iteration("6 − (− 4)==6 − (− 4)", "answer=6 − (− 4)", "input=6 − (− 4)"), + Iteration("6-(-4)==6 − (− 4)", "answer=6-(-4)", "input=6 − (− 4)"), + Iteration("− (− 4) + 6==6 − (− 4)", "answer=− (− 4) + 6", "input=6 − (− 4)"), + Iteration("10^−5 * 3!=3 * 10^-5", "answer=10^−5 * 3", "input=3 * 10^-5"), + Iteration("3 * 10^-5==3 * 10^-5", "answer=3 * 10^-5", "input=3 * 10^-5"), + Iteration( + "1000 + 200 + 30 + 4 + 0.5 + 0.06==1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=1000 + 200 + 30 + 4 + 0.5 + 0.06", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration( + "200 + 30 + 4 + 0.5 + 0.06 + 1000==1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=200 + 30 + 4 + 0.5 + 0.06 + 1000", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration( + "0.06 + 0.5 + 4 + 30 + 200 + 1000==1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=0.06 + 0.5 + 4 + 30 + 200 + 1000", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration("2 * 2 * 3 * 3==2 * 2 * 3 * 3", "answer=2 * 2 * 3 * 3", "input=2 * 2 * 3 * 3"), + Iteration("4x^2+20x==4*x^2+20x", "answer=4x^2+20x", "input=4*x^2+20x"), + Iteration("3+x-5==3+x-5", "answer=3+x-5", "input=3+x-5"), + Iteration("Z+A-Z==Z+A-Z", "answer=Z+A-Z", "input=Z+A-Z"), + Iteration("6C - 5A -1==6C - 5A -1", "answer=6C - 5A -1", "input=6C - 5A -1"), + Iteration("5Z-w==5*Z-w", "answer=5Z-w", "input=5*Z-w"), + Iteration("5*Z-w==5*Z-w", "answer=5*Z-w", "input=5*Z-w"), + Iteration("LS-3S+L==L*S-3S+L", "answer=LS-3S+L", "input=L*S-3S+L"), + Iteration("L*S-3S+L==L*S-3S+L", "answer=L*S-3S+L", "input=L*S-3S+L"), + Iteration("L*S-3*S+L==L*S-3S+L", "answer=L*S-3*S+L", "input=L*S-3S+L"), + Iteration("LS-3*S+L==L*S-3S+L", "answer=LS-3*S+L", "input=L*S-3S+L"), + Iteration("9x^2 − 6x + 1==9x^2 − 6x + 1", "answer=9x^2 − 6x + 1", "input=9x^2 − 6x + 1"), + Iteration("c*b-c==c*b-c", "answer=c*b-c", "input=c*b-c"), + Iteration("bc-c==c*b-c", "answer=bc-c", "input=c*b-c"), + Iteration("cb-c==c*b-c", "answer=cb-c", "input=c*b-c"), + Iteration("-c+bc==c*b-c", "answer=-c+bc", "input=c*b-c"), + Iteration("-c+cb==c*b-c", "answer=-c+cb", "input=c*b-c"), + Iteration("x^2+y+4x==x^2+y+4x", "answer=x^2+y+4x", "input=x^2+y+4x"), + Iteration("y+4x+x^2==x^2+y+4x", "answer=y+4x+x^2", "input=x^2+y+4x"), + Iteration("x^2+4x+y==x^2+y+4x", "answer=x^2+4x+y", "input=x^2+y+4x"), + Iteration("Y+5==Y+5", "answer=Y+5", "input=Y+5"), + Iteration("5+Y==Y+5", "answer=5+Y", "input=Y+5"), + Iteration( + "a^2 + b^2 + c^2+ 2a*b + 2a*c + 2bc==a^2+b^2+c^2+2a*b+2a*c+2bc", + "answer=a^2 + b^2 + c^2+ 2a*b + 2a*c + 2bc", + "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + ), + Iteration( + "a^2 + b^2 + c^2+ 2a*b + 2bc + 2a*c==a^2+b^2+c^2+2a*b+2a*c+2bc", + "answer=a^2 + b^2 + c^2+ 2a*b + 2bc + 2a*c", + "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + ), + Iteration( + "2a*b + 2bc + 2a*c + a^2 + b^2 + c^2==a^2+b^2+c^2+2a*b+2a*c+2bc", + "answer=2a*b + 2bc + 2a*c + a^2 + b^2 + c^2", + "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + ), + Iteration( + "2a*b + b^2 + c^2+ a^2 + 2bc + 2a*c==a^2+b^2+c^2+2a*b+2a*c+2bc", + "answer=2a*b + b^2 + c^2+ a^2 + 2bc + 2a*c", + "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + ), + Iteration("1 - 6x + 9x^2==9x^2 − 6x + 1", "answer=1 - 6x + 9x^2", "input=9x^2 − 6x + 1"), + Iteration("9x^2 + 1 - 6x==9x^2 − 6x + 1", "answer=9x^2 + 1 - 6x", "input=9x^2 − 6x + 1"), + Iteration("2+1+x==x+1+2", "answer=2+1+x", "input=x+1+2"), + Iteration("1+2+x==x+1+2", "answer=1+2+x", "input=x+1+2"), + Iteration("1+x+2==x+1+2", "answer=1+x+2", "input=x+1+2"), + Iteration("2+x+1==x+1+2", "answer=2+x+1", "input=x+1+2"), + Iteration("(x+1)+2==x+1+2", "answer=(x+1)+2", "input=x+1+2"), + Iteration("x + (1+2)==x+1+2", "answer=x + (1+2)", "input=x+1+2"), + Iteration( + "y+1+ 9x(x − 6)==9x(x − 6) + 1+ y", "answer=y+1+ 9x(x − 6)", "input=9x(x − 6) + 1+ y" + ), + Iteration("1+y+9x(x − 6)==9x(x − 6) + 1+ y", "answer=1+y+9x(x − 6)", "input=9x(x − 6) + 1+ y"), + Iteration( + "1 + 9x(x − 6) + y==9x(x − 6) + 1+ y", "answer=1 + 9x(x − 6) + y", "input=9x(x − 6) + 1+ y" + ), + Iteration( + "(y+1)+9x(x − 6)==9x(x − 6) + 1+ y", "answer=(y+1)+9x(x − 6)", "input=9x(x − 6) + 1+ y" + ), + Iteration( + "(x^2 − x)/3 − 4y==(x^2 − x)/3 − 4y", "answer=(x^2 − x)/3 − 4y", "input=(x^2 − x)/3 − 4y" + ), + Iteration( + "-4y + (x^2 − x)/3==(x^2 − x)/3 − 4y", "answer=-4y + (x^2 − x)/3", "input=(x^2 − x)/3 − 4y" + ), + Iteration("(3x -1)^2==(3x-1)^2", "answer=(3x -1)^2", "input=(3x-1)^2"), + Iteration("(2+6+3+4)*2==2*(2+6+3+4)", "answer=(2+6+3+4)*2", "input=2*(2+6+3+4)"), + Iteration("(2+6+3+4) × 2==2*(2+6+3+4)", "answer=(2+6+3+4) × 2", "input=2*(2+6+3+4)"), + Iteration( + "3 - (6 * 2) + 15==15 - (6 × 2) + 3", "answer=3 - (6 * 2) + 15", "input=15 - (6 × 2) + 3" + ), + Iteration( + "15 - (2 × 6) + 3==15 - (6 × 2) + 3", "answer=15 - (2 × 6) + 3", "input=15 - (6 × 2) + 3" + ), + Iteration( + "2* ( 25+50+100+150)==(50 + 150 + 100 + 25) × 2", + "answer=2* ( 25+50+100+150)", + "input=(50 + 150 + 100 + 25) × 2" + ), + Iteration("20x+4x^2==4*x^2+20x", "answer=20x+4x^2", "input=4*x^2+20x"), + Iteration("x-5+3==3+x-5", "answer=x-5+3", "input=3+x-5"), + Iteration("-5+3+x==3+x-5", "answer=-5+3+x", "input=3+x-5"), + Iteration("-5+x+3==3+x-5", "answer=-5+x+3", "input=3+x-5"), + Iteration("3+(x-5)==3+x-5", "answer=3+(x-5)", "input=3+x-5"), + Iteration("A+Z-Z==Z+A-Z", "answer=A+Z-Z", "input=Z+A-Z"), + Iteration("Z+(A-Z)==Z+A-Z", "answer=Z+(A-Z)", "input=Z+A-Z"), + Iteration("6C - (5A+1)==6C - 5A -1", "answer=6C - (5A+1)", "input=6C - 5A -1"), + Iteration("-5A-1+6C==6C - 5A -1", "answer=-5A-1+6C", "input=6C - 5A -1"), + Iteration("-W+5Z==5*Z-W", "answer=-W+5Z", "input=5*Z-W"), + Iteration("L+LS-3S==L*S-3S+L", "answer=L+LS-3S", "input=L*S-3S+L"), + Iteration( + "- 4y + (x^2 − x)/3==(x^2 − x)/3 − 4y", "answer=- 4y + (x^2 − x)/3", "input=(x^2 − x)/3 − 4y" + ), + Iteration( + "a^2+ b^2 + c^2 + 2bc + 2a*c + 2a*b==a^2+b^2+c^2+2a*b+2a*c+2bc", + "answer=a^2+ b^2 + c^2 + 2bc + 2a*c + 2a*b", + "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + ) + ) + fun testMatches_assortedExpressions_withMatchingCharacteristics_returnsTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // This verifies a variety of expressions that per the PRD and technical specification should + // pass for this classifier. + assertThat(matches).isTrue() + } + + @Test + @RunParameterized( + Iteration("10!=6 − (− 4)", "answer=10", "input=6 − (− 4)"), + Iteration("6 + 2^2!=6 − (− 4)", "answer=6 + 2^2", "input=6 − (− 4)"), + Iteration("3 * 2 − (− 4)!=6 − (− 4)", "answer=3 * 2 − (− 4)", "input=6 − (− 4)"), + Iteration("100/10!=6 − (− 4)", "answer=100/10", "input=6 − (− 4)"), + Iteration("3/(10 * 10^4)!=3 * 10^-5", "answer=3/(10 * 10^4)", "input=3 * 10^-5"), + Iteration( + "1234.56!=1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=1234.56", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration( + "123456/100!=1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=123456/100", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration( + "61728/50!=1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=61728/50", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration( + "1234 + 56/100!=1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=1234 + 56/100", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration( + "1230 + 4.56!=1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=1230 + 4.56", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration( + "2 * 2 * 3 * 3 * 1!=2 * 2 * 3 * 3", "answer=2 * 2 * 3 * 3 * 1", "input=2 * 2 * 3 * 3" + ), + Iteration("2 * 2 * 9!=2 * 2 * 3 * 3", "answer=2 * 2 * 9", "input=2 * 2 * 3 * 3"), + Iteration("4 * 3^2!=2 * 2 * 3 * 3", "answer=4 * 3^2", "input=2 * 2 * 3 * 3"), + Iteration("8/2 * 3 * 3!=2 * 2 * 3 * 3", "answer=8/2 * 3 * 3", "input=2 * 2 * 3 * 3"), + Iteration("36!=2 * 2 * 3 * 3", "answer=36", "input=2 * 2 * 3 * 3"), + Iteration("sqrt(4-2)!=sqrt(2)", "answer=sqrt(4-2)", "input=sqrt(2)"), + Iteration( + "(a+ b)^2 + c^2 + 2bc + 2a*c!=a^2+b^2+c^2+2a*b+2a*c+2bc", + "answer=(a+ b)^2 + c^2 + 2bc + 2a*c", + "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + ), + Iteration( + "(a+b+c)^2!=a^2+b^2+c^2+2a*b+2a*c+2bc", "answer=(a+b+c)^2", "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + ), + Iteration( + "(-a -b -c)^2!=a^2+b^2+c^2+2a*b+2a*c+2bc", + "answer=(-a -b -c)^2", + "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + ), + Iteration( + "x(x − 1)/3 −4y!=(x^2 − x)/3 − 4y", "answer=x(x − 1)/3 −4y", "input=(x^2 − x)/3 − 4y" + ), + Iteration( + "x^2/3 − x/3 − 4y!=(x^2 − x)/3 − 4y", "answer=x^2/3 − x/3 − 4y", "input=(x^2 − x)/3 − 4y" + ), + Iteration( + "x^2/3 − (x/3 + 4y)!=(x^2 − x)/3 − 4y", "answer=x^2/3 − (x/3 + 4y)", "input=(x^2 − x)/3 − 4y" + ), + Iteration("√(3x −1)4!=(3x-1)^2", "answer=√(3x −1)4", "input=(3x-1)^2"), + Iteration("3x(3x - 2) + 1!=(3x-1)^2", "answer=3x(3x - 2) + 1", "input=(3x-1)^2"), + Iteration("3(3x^2) - 6x +1!=(3x-1)^2", "answer=3(3x^2) - 6x +1", "input=(3x-1)^2"), + Iteration("2x!=sqrt(4x^2)", "answer=2x", "input=sqrt(4x^2)"), + Iteration("x^2+2x+1!=(x+1)^2", "answer=x^2+2x+1", "input=(x+1)^2"), + Iteration("x^2-1!=(x+1)(x-1)", "answer=x^2-1", "input=(x+1)(x-1)"), + Iteration("x+1!=(x^2+2x+1)/(x+1)", "answer=x+1", "input=(x^2+2x+1)/(x+1)"), + Iteration("x-1!=(x^2-1)/(x+1)", "answer=x-1", "input=(x^2-1)/(x+1)"), + Iteration("x+1!=(x^2-1)/(x-1)", "answer=x+1", "input=(x^2-1)/(x-1)"), + Iteration("-3x!=(-27x^3)^(1/3)", "answer=-3x", "input=(-27x^3)^(1/3)"), + Iteration("1!=(x^2-1)/(x^2-1)", "answer=1", "input=(x^2-1)/(x^2-1)"), + Iteration("2*(6+3+4) + 4!=2*(2+6+3+4)", "answer=2*(6+3+4) + 4", "input=2*(2+6+3+4)"), + Iteration("2*(2+6+3) + 8!=2*(2+6+3+4)", "answer=2*(2+6+3) + 8", "input=2*(2+6+3+4)"), + Iteration("15 - 12 + 3!=15 - (6 × 2) + 3", "answer=15 - 12 + 3", "input=15 - (6 × 2) + 3"), + Iteration( + "2 *(50 + 150) + 2*(100 + 25)!=(50 + 150 + 100 + 25) × 2", + "answer=2 *(50 + 150) + 2*(100 + 25)", + "input=(50 + 150 + 100 + 25) × 2" + ), + Iteration("3 * 10^5!=3 * 10^-5", "answer=3 * 10^5", "input=3 * 10^-5"), + Iteration("2 * 10^−5!=3 * 10^-5", "answer=2 * 10^−5", "input=3 * 10^-5"), + Iteration("5 * 10^−3!=3 * 10^-5", "answer=5 * 10^−3", "input=3 * 10^-5"), + Iteration("30 * 10^−6!=3 * 10^-5", "answer=30 * 10^−6", "input=3 * 10^-5"), + Iteration("0.00003!=3 * 10^-5", "answer=0.00003", "input=3 * 10^-5"), + Iteration("3/10^5!=3 * 10^-5", "answer=3/10^5", "input=3 * 10^-5"), + Iteration( + "123456!=1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=123456", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration( + "1000 + 200 + 30!=1000 + 200 + 30 + 4 + 0.5 + 0.06", + "answer=1000 + 200 + 30", + "input=1000 + 200 + 30 + 4 + 0.5 + 0.06" + ), + Iteration("3 *2 – (− 4)!=6 − (− 4)", "answer=3 *2 – (− 4)", "input=6 − (− 4)"), + Iteration("6 − 4!=6 − (− 4)", "answer=6 − 4", "input=6 − (− 4)"), + Iteration("6 + (− 4)!=6 − (− 4)", "answer=6 + (− 4)", "input=6 − (− 4)"), + Iteration("100!=6 − (− 4)", "answer=100", "input=6 − (− 4)"), + Iteration("7!=5+2", "answer=7", "input=5+2"), + Iteration("3+4!=5+2", "answer=3+4", "input=5+2"), + Iteration("2 * 2 * 3!=2 * 2 * 3 * 3", "answer=2 * 2 * 3", "input=2 * 2 * 3 * 3"), + Iteration("2 * 3 * 3 * 3!=2 * 2 * 3 * 3", "answer=2 * 3 * 3 * 3", "input=2 * 2 * 3 * 3"), + Iteration("A!=Z+A-Z", "answer=A", "input=Z+A-Z"), + Iteration("L(1+S)-3S!=L*S-3S+L", "answer=L(1+S)-3S", "input=L*S-3S+L"), + Iteration("S(L-3)+L!=L*S-3S+L", "answer=S(L-3)+L", "input=L*S-3S+L"), + Iteration( + "x(x − 1)/3 − 4y!=(x^2 − x)/3 − 4y", "answer=x(x − 1)/3 − 4y", "input=(x^2 − x)/3 − 4y" + ), + Iteration( + "(x^2 − x) * 3^-1 − 4y!=(x^2 − x)/3 − 4y", + "answer=(x^2 − x) * 3^-1 − 4y", + "input=(x^2 − x)/3 − 4y" + ), + Iteration( + "x(x^2 − x)/3 − 4y!=(x^2 − x)/3 − 4y", "answer=x(x^2 − x)/3 − 4y", "input=(x^2 − x)/3 − 4y" + ), + Iteration( + "(x^2 − x)/3 + 4y!=(x^2 − x)/3 − 4y", "answer=(x^2 − x)/3 + 4y", "input=(x^2 − x)/3 − 4y" + ), + Iteration( + "(x^2 + x)/3 - 4y!=(x^2 − x)/3 − 4y", "answer=(x^2 + x)/3 - 4y", "input=(x^2 − x)/3 − 4y" + ), + Iteration( + "(x^2 − x)*0.33 - 4y!=(x^2 − x)/3 − 4y", + "answer=(x^2 − x)*0.33 - 4y", + "input=(x^2 − x)/3 − 4y" + ), + Iteration( + "a*a + b*b + c*c + 2*a*b + 2*a*c + 2*b*c==a^2+b^2+c^2+2a*b+2a*c+2bc", + "answer=a*a + b*b + c*c + 2*a*b + 2*a*c + 2*b*c", + "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + ), + Iteration( + "a^2 + b^2 + c^2 + 2(a*b + a*c + bc)!=a^2+b^2+c^2+2a*b+2a*c+2bc", + "answer=a^2 + b^2 + c^2 + 2(a*b + a*c + bc)", + "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + ), + Iteration( + "(a + b)^2 + c^2 + 2a*c + 2bc!=a^2+b^2+c^2+2a*b+2a*c+2bc", + "answer=(a + b)^2 + c^2 + 2a*c + 2bc", + "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + ), + Iteration( + "a * a + b * b + c^3/c + 2a*b + 2a*c + 2bc!=a^2+b^2+c^2+2a*b+2a*c+2bc", + "answer=a * a + b * b + c^3/c + 2a*b + 2a*c + 2bc", + "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + ), + Iteration( + "(a + b + c)^3!=a^2+b^2+c^2+2a*b+2a*c+2bc", + "answer=(a + b + c)^3", + "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + ), + Iteration( + "a^2 + b^2 + c^2!=a^2+b^2+c^2+2a*b+2a*c+2bc", + "answer=a^2 + b^2 + c^2", + "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + ), + Iteration( + "a^2 + b^2 + c^2- 2a*b - 2a*c - 2bc!=a^2+b^2+c^2+2a*b+2a*c+2bc", + "answer=a^2 + b^2 + c^2- 2a*b - 2a*c - 2bc", + "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + ), + Iteration("(3x − 1)^2!=9x^2 − 6x + 1", "answer=(3x − 1)^2", "input=9x^2 − 6x + 1"), + Iteration("3x(3x − 2) + 1!=9x^2 − 6x + 1", "answer=3x(3x − 2) + 1", "input=9x^2 − 6x + 1"), + Iteration("3(3x^2 − 2x) + 1!=9x^2 − 6x + 1", "answer=3(3x^2 − 2x) + 1", "input=9x^2 − 6x + 1"), + Iteration("(3x)^2 − 6x + 1!=9x^2 − 6x + 1", "answer=(3x)^2 − 6x + 1", "input=9x^2 − 6x + 1"), + Iteration("c(b-1)!=c*b-c", "answer=c(b-1)", "input=c*b-c"), + Iteration("x(x+4)+y!=x^2+y+4x", "answer=x(x+4)+y", "input=x^2+y+4x"), + Iteration("Y!=Y+5", "answer=Y", "input=Y+5"), + Iteration("5!=Y+5", "answer=5", "input=Y+5"), + Iteration("x+3!=x+1+2", "answer=x+3", "input=x+1+2"), + Iteration("(1 - 3x)^2!=(3x-1)^2", "answer=(1 - 3x)^2", "input=(3x-1)^2"), + Iteration("9x^2 - 6x - 1!=(3x-1)^2", "answer=9x^2 - 6x - 1", "input=(3x-1)^2"), + Iteration("(3x −1)!=(3x-1)^2", "answer=(3x −1)", "input=(3x-1)^2"), + Iteration("2x!=sqrt(2x)^2", "answer=2x", "input=sqrt(2x)^2"), + Iteration("2x!=sqrt(-4x^2)", "answer=2x", "input=sqrt(-4x^2)"), + Iteration("x^2+2x+1!=(x+2)^2", "answer=x^2+2x+1", "input=(x+2)^2"), + Iteration("x^2-1!=(x+1)(1-x)", "answer=x^2-1", "input=(x+1)(1-x)"), + Iteration("x+1!=(x^2+2x+1)/(x-1)", "answer=x+1", "input=(x^2+2x+1)/(x-1)"), + Iteration("x-1!=(x^2-1)/x", "answer=x-1", "input=(x^2-1)/x"), + Iteration("x+1!=(x^2-1)/(x-2)", "answer=x+1", "input=(x^2-1)/(x-2)"), + Iteration("-3x!=(9x^3)^(1/3)", "answer=-3x", "input=(9x^3)^(1/3)") + ) + fun testMatches_assortedExpressions_withoutMatchingCharacteristics_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // This verifies a variety of expressions that per the PRD and technical specification should + // not pass for this classifier. + assertThat(matches).isFalse() + } + + private fun matchesClassifier( + answerExpression: InteractionObject, + inputExpression: InteractionObject, + allowedVariables: List = allPossibleVariables + ): Boolean { + return classifier.matches( + answerExpression, + inputs = mapOf("x" to inputExpression), + classificationContext = ClassificationContext( + customizationArgs = mapOf( + "customOskLetters" to SchemaObject.newBuilder().apply { + schemaObjectList = SchemaObjectList.newBuilder().apply { + addAllSchemaObject( + allowedVariables.map { + SchemaObject.newBuilder().setNormalizedString(it).build() + } + ) + }.build() + }.build() + ) + ) + ) + } + + private fun createMathExpression(rawExpression: String) = InteractionObject.newBuilder().apply { + mathExpression = rawExpression + }.build() + + private fun setUpTestApplicationComponent() { + DaggerAlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest_TestApplicationComponent + .builder() + .setApplication(ApplicationProvider.getApplicationContext()).build().inject(this) + } + + // TODO(#89): Move this to a common test application component. + @Module + class TestModule { + @Provides + @Singleton + fun provideContext(application: Application): Context { + return application + } + } + + // TODO(#89): Move this to a common test application component. + @Singleton + @Component( + modules = [ + TestModule::class, LocaleProdModule::class, FakeOppiaClockModule::class, + TestDispatcherModule::class, LoggerModule::class, RobolectricModule::class + ] + ) + interface TestApplicationComponent { + @Component.Builder + interface Builder { + @BindsInstance + fun setApplication(application: Application): Builder + + fun build(): TestApplicationComponent + } + + fun inject( + test: AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest + ) + } +} diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModuleTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModuleTest.kt new file mode 100644 index 00000000000..92ebc07cc5d --- /dev/null +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModuleTest.kt @@ -0,0 +1,107 @@ +package org.oppia.android.domain.classify.rules.algebraicexpressioninput + +import android.app.Application +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import dagger.BindsInstance +import dagger.Component +import dagger.Module +import dagger.Provides +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.oppia.android.domain.classify.RuleClassifier +import org.oppia.android.testing.robolectric.RobolectricModule +import org.oppia.android.testing.threading.TestDispatcherModule +import org.oppia.android.testing.time.FakeOppiaClockModule +import org.oppia.android.util.locale.LocaleProdModule +import org.oppia.android.util.logging.LoggerModule +import org.robolectric.annotation.Config +import org.robolectric.annotation.LooperMode +import javax.inject.Inject +import javax.inject.Singleton +import org.oppia.android.domain.classify.rules.AlgebraicExpressionInputRules + +/** Tests for [AlgebraicExpressionInputModule]. */ +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") +@RunWith(AndroidJUnit4::class) +@LooperMode(LooperMode.Mode.PAUSED) +@Config(manifest = Config.NONE) +class AlgebraicExpressionInputModuleTest { + @Inject + @AlgebraicExpressionInputRules + lateinit var algebraicExpressionInputClassifiers: Map< + String, @JvmSuppressWildcards RuleClassifier> + + @Before + fun setUp() { + setUpTestApplicationComponent() + } + + @Test + fun testModule_hasAtLeastOneClassifier() { + assertThat(algebraicExpressionInputClassifiers).isNotEmpty() + } + + @Test + fun testModule_hasNoDuplicateClassifiers() { + assertThat(algebraicExpressionInputClassifiers.values.toSet()).hasSize( + algebraicExpressionInputClassifiers.size + ) + } + + @Test + fun testModule_providesMatchesExactlyWithClassifier() { + assertThat(algebraicExpressionInputClassifiers).containsKey("MatchesExactlyWith") + } + + @Test + fun testModule_providesMatchesUpToTrivialManipulationsClassifier() { + assertThat(algebraicExpressionInputClassifiers).containsKey("MatchesUpToTrivialManipulations") + } + + @Test + fun testModule_providesIsEquivalentToClassifier() { + assertThat(algebraicExpressionInputClassifiers).containsKey("MatchesExactlyWith") + } + + private fun setUpTestApplicationComponent() { + DaggerAlgebraicExpressionInputModuleTest_TestApplicationComponent + .builder() + .setApplication(ApplicationProvider.getApplicationContext()).build().inject(this) + } + + // TODO(#89): Move this to a common test application component. + @Module + class TestModule { + @Provides + @Singleton + fun provideContext(application: Application): Context { + return application + } + } + + // TODO(#89): Move this to a common test application component. + @Singleton + @Component( + modules = [ + TestModule::class, LocaleProdModule::class, FakeOppiaClockModule::class, + TestDispatcherModule::class, LoggerModule::class, RobolectricModule::class, + AlgebraicExpressionInputModule::class + ] + ) + interface TestApplicationComponent { + @Component.Builder + interface Builder { + @BindsInstance + fun setApplication(application: Application): Builder + + fun build(): TestApplicationComponent + } + + fun inject(test: AlgebraicExpressionInputModuleTest) + } +} diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/BUILD.bazel b/domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/BUILD.bazel new file mode 100644 index 00000000000..a9bacbd93ba --- /dev/null +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/BUILD.bazel @@ -0,0 +1,105 @@ +""" +Tests for algebraic expression input classifiers. +""" + +load("@dagger//:workspace_defs.bzl", "dagger_rules") +load("//:oppia_android_test.bzl", "oppia_android_test") + +oppia_android_test( + name = "AlgebraicExpressionInputIsEquivalentToRuleClassifierProviderTest", + srcs = ["AlgebraicExpressionInputIsEquivalentToRuleClassifierProviderTest.kt"], + custom_package = "org.oppia.android.domain.classify.rules.algebraicexpressioninput", + test_class = "org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputIsEquivalentToRuleClassifierProviderTest", + test_manifest = "//domain:test_manifest", + deps = [ + ":dagger", + "//domain", + "//model/src/main/proto:interaction_object_java_proto_lite", + "//testing/src/main/java/org/oppia/android/testing/junit:oppia_parameterized_test_runner", + "//testing/src/main/java/org/oppia/android/testing/junit:parameterized_robolectric_test_runner", + "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module", + "//testing/src/main/java/org/oppia/android/testing/threading:test_module", + "//testing/src/main/java/org/oppia/android/testing/time:test_module", + "//third_party:androidx_test_core", + "//third_party:com_google_truth_truth", + "//third_party:junit_junit", + "//third_party:org_robolectric_robolectric", + "//third_party:robolectric_android-all", + "//utility/src/main/java/org/oppia/android/util/locale:prod_module", + "//utility/src/main/java/org/oppia/android/util/logging:prod_module", + ], +) + +oppia_android_test( + name = "AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProviderTest", + srcs = ["AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProviderTest.kt"], + custom_package = "org.oppia.android.domain.classify.rules.algebraicexpressioninput", + test_class = "org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProviderTest", + test_manifest = "//domain:test_manifest", + deps = [ + ":dagger", + "//domain", + "//model/src/main/proto:interaction_object_java_proto_lite", + "//testing/src/main/java/org/oppia/android/testing/junit:oppia_parameterized_test_runner", + "//testing/src/main/java/org/oppia/android/testing/junit:parameterized_robolectric_test_runner", + "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module", + "//testing/src/main/java/org/oppia/android/testing/threading:test_module", + "//testing/src/main/java/org/oppia/android/testing/time:test_module", + "//third_party:androidx_test_core", + "//third_party:com_google_truth_truth", + "//third_party:junit_junit", + "//third_party:org_robolectric_robolectric", + "//third_party:robolectric_android-all", + "//utility/src/main/java/org/oppia/android/util/locale:prod_module", + "//utility/src/main/java/org/oppia/android/util/logging:prod_module", + ], +) + +oppia_android_test( + name = "AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest", + srcs = ["AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt"], + custom_package = "org.oppia.android.domain.classify.rules.algebraicexpressioninput", + test_class = "org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest", + test_manifest = "//domain:test_manifest", + deps = [ + ":dagger", + "//domain", + "//model/src/main/proto:interaction_object_java_proto_lite", + "//testing/src/main/java/org/oppia/android/testing/junit:oppia_parameterized_test_runner", + "//testing/src/main/java/org/oppia/android/testing/junit:parameterized_robolectric_test_runner", + "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module", + "//testing/src/main/java/org/oppia/android/testing/threading:test_module", + "//testing/src/main/java/org/oppia/android/testing/time:test_module", + "//third_party:androidx_test_core", + "//third_party:com_google_truth_truth", + "//third_party:junit_junit", + "//third_party:org_robolectric_robolectric", + "//third_party:robolectric_android-all", + "//utility/src/main/java/org/oppia/android/util/locale:prod_module", + "//utility/src/main/java/org/oppia/android/util/logging:prod_module", + ], +) + +oppia_android_test( + name = "AlgebraicExpressionInputModuleTest", + srcs = ["AlgebraicExpressionInputModuleTest.kt"], + custom_package = "org.oppia.android.domain.classify.rules.algebraicexpressioninput", + test_class = "org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModuleTest", + test_manifest = "//domain:test_manifest", + deps = [ + ":dagger", + "//domain", + "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module", + "//testing/src/main/java/org/oppia/android/testing/threading:test_module", + "//testing/src/main/java/org/oppia/android/testing/time:test_module", + "//third_party:androidx_test_ext_junit", + "//third_party:com_google_truth_truth", + "//third_party:junit_junit", + "//third_party:org_robolectric_robolectric", + "//third_party:robolectric_android-all", + "//utility/src/main/java/org/oppia/android/util/locale:prod_module", + "//utility/src/main/java/org/oppia/android/util/logging:prod_module", + ], +) + +dagger_rules() diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt index beef3fdf622..a9af6bd65fb 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt @@ -34,11 +34,11 @@ import org.oppia.android.domain.classify.rules.numericexpressioninput.DaggerNume import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider as RuleClassifierProvider /** - * Tests for [RuleClassifierProvider]. + * Tests for [NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider]. * * Note that the tests implemented in this suite are specifically set up to verify the cases * outlined in this sheet: - * https://docs.google.com/spreadsheets/dNumericExpressionInputIsEquivalentToRuleClassifierProvider/1u1fQdah2WsmdYKWKGmuXy5TPT7Ot-b8A7O9iZF-j5XE/edit#gid=0. + * https://docs.google.com/spreadsheets/d/1u1fQdah2WsmdYKWKGmuXy5TPT7Ot-b8A7O9iZF-j5XE/edit#gid=0. */ // FunctionName: test names are conventionally named with underscores. @Suppress("FunctionName") diff --git a/utility/src/test/java/org/oppia/android/util/math/PolynomialExtensionsTest.kt b/utility/src/test/java/org/oppia/android/util/math/PolynomialExtensionsTest.kt index b3cc704c4c9..9711b1d0c87 100644 --- a/utility/src/test/java/org/oppia/android/util/math/PolynomialExtensionsTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/PolynomialExtensionsTest.kt @@ -3725,7 +3725,7 @@ class PolynomialExtensionsTest { val result = polynomial1 pow polynomial2 - // (-9x^3)^(1/3)=-3x (demonstrates real number rooting, i.e. support for negative coefficients + // (-27x^3)^(1/3)=-3x (demonstrates real number rooting, i.e. support for negative coefficients // in certain cases). assertThat(result).apply { hasTermCountThat().isEqualTo(1) From 0a7cb6945f563f9b6fa4c64ae8a7438ceb6266c3 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 4 Feb 2022 22:54:34 -0800 Subject: [PATCH 113/162] Lint & static check fixes. --- .../domain/classify/ClassificationContext.kt | 9 +++++++++ ...onInputIsEquivalentToRuleClassifierProvider.kt | 10 +++++++++- ...putMatchesExactlyWithRuleClassifierProvider.kt | 9 ++++++++- ...oTrivialManipulationsRuleClassifierProvider.kt | 11 ++++++++++- ...onInputIsEquivalentToRuleClassifierProvider.kt | 2 +- ...putIsEquivalentToRuleClassifierProviderTest.kt | 7 +++---- ...atchesExactlyWithRuleClassifierProviderTest.kt | 14 ++++++++------ ...vialManipulationsRuleClassifierProviderTest.kt | 15 ++++++++------- .../AlgebraicExpressionInputModuleTest.kt | 2 +- ...putIsEquivalentToRuleClassifierProviderTest.kt | 3 +-- ...atchesExactlyWithRuleClassifierProviderTest.kt | 3 +-- ...vialManipulationsRuleClassifierProviderTest.kt | 3 +-- .../file_content_validation_checks.textproto | 3 +++ scripts/assets/test_file_exemptions.textproto | 1 + 14 files changed, 64 insertions(+), 28 deletions(-) diff --git a/domain/src/main/java/org/oppia/android/domain/classify/ClassificationContext.kt b/domain/src/main/java/org/oppia/android/domain/classify/ClassificationContext.kt index 01415330ce2..5d2ba3b88be 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/ClassificationContext.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/ClassificationContext.kt @@ -3,6 +3,15 @@ package org.oppia.android.domain.classify import org.oppia.android.app.model.SchemaObject import org.oppia.android.app.model.WrittenTranslationContext +/** + * Represents the context provided to classifiers when they're classifying an answer. + * + * This object provides context for the interaction and learner settings to help classifiers + * properly categorize and process answers. + * + * @property writtenTranslationContext the [WrittenTranslationContext] currently used by the learner + * @property customizationArgs the customization arguments defined by the current interaction + */ data class ClassificationContext( val writtenTranslationContext: WrittenTranslationContext = WrittenTranslationContext.getDefaultInstance(), diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputIsEquivalentToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputIsEquivalentToRuleClassifierProvider.kt index 56e771b93c0..fb4f9bf3c8a 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputIsEquivalentToRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputIsEquivalentToRuleClassifierProvider.kt @@ -9,10 +9,18 @@ import org.oppia.android.domain.classify.rules.RuleClassifierProvider import org.oppia.android.util.logging.ConsoleLogger import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult import org.oppia.android.util.math.MathExpressionParser.Companion.parseAlgebraicExpression +import org.oppia.android.util.math.isApproximatelyEqualTo import org.oppia.android.util.math.toPolynomial import javax.inject.Inject -import org.oppia.android.util.math.isApproximatelyEqualTo +/** + * Provider for a classifier that determines whether an algebraic expression is mathematically + * equivalent to the creator-specific expression defined as the input to this interaction. + * + * Note that both expressions are assumed and parsed as polynomials. + * + * See this class's tests for a list of supported cases (both for matching and not matching). + */ class AlgebraicExpressionInputIsEquivalentToRuleClassifierProvider @Inject constructor( private val classifierFactory: GenericRuleClassifier.Factory, private val consoleLogger: ConsoleLogger diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider.kt index 4c20ce52903..c35ba68b7f2 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider.kt @@ -9,9 +9,16 @@ import org.oppia.android.domain.classify.rules.RuleClassifierProvider import org.oppia.android.util.logging.ConsoleLogger import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult import org.oppia.android.util.math.MathExpressionParser.Companion.parseAlgebraicExpression -import javax.inject.Inject import org.oppia.android.util.math.isApproximatelyEqualTo +import javax.inject.Inject +/** + * Provider for a classifier that determines whether an algebraic expression is exactly equal to the + * creator-specific expression defined as the input to this interaction, including any parenthetical + * groups in the expressions and operand order. + * + * See this class's tests for a list of supported cases (both for matching and not matching). + */ class AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider @Inject constructor( private val classifierFactory: GenericRuleClassifier.Factory, private val consoleLogger: ConsoleLogger diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt index 6f618dfc857..27a02bfdd54 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt @@ -1,6 +1,5 @@ package org.oppia.android.domain.classify.rules.algebraicexpressioninput -import javax.inject.Inject import org.oppia.android.app.model.ComparableOperation import org.oppia.android.app.model.InteractionObject import org.oppia.android.domain.classify.ClassificationContext @@ -12,7 +11,17 @@ import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingRes import org.oppia.android.util.math.MathExpressionParser.Companion.parseAlgebraicExpression import org.oppia.android.util.math.isApproximatelyEqualTo import org.oppia.android.util.math.toComparableOperation +import javax.inject.Inject +/** + * Provider for a classifier that determines whether an algebraic expression is equal to the + * creator-specific expression defined as the input to this interaction, with some manipulations. + * + * 'Trivial manipulations' indicates rearranging any operands for commutative operations, or changes + * in resolution order (i.e. associative) without changing the meaning of the expression. + * + * See this class's tests for a list of supported cases (both for matching and not matching). + */ class AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider @Inject constructor( private val classifierFactory: GenericRuleClassifier.Factory, diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProvider.kt index 54202d71aed..058a7bc6e96 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProvider.kt @@ -1,6 +1,5 @@ package org.oppia.android.domain.classify.rules.numericexpressioninput -import javax.inject.Inject import org.oppia.android.app.model.InteractionObject import org.oppia.android.app.model.Real import org.oppia.android.domain.classify.ClassificationContext @@ -12,6 +11,7 @@ import org.oppia.android.util.math.MathExpressionParser import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult import org.oppia.android.util.math.evaluateAsNumericExpression import org.oppia.android.util.math.isApproximatelyEqualTo +import javax.inject.Inject /** * Provider for a classifier that determines whether a numeric expression is numerically equivalent diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputIsEquivalentToRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputIsEquivalentToRuleClassifierProviderTest.kt index 60d09fbf36b..c64a5fc7640 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputIsEquivalentToRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputIsEquivalentToRuleClassifierProviderTest.kt @@ -12,7 +12,9 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.oppia.android.app.model.InteractionObject -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.app.model.SchemaObject +import org.oppia.android.app.model.SchemaObjectList +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.testing.junit.OppiaParameterizedTestRunner import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Iteration @@ -29,9 +31,6 @@ import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode import javax.inject.Inject import javax.inject.Singleton -import org.oppia.android.app.model.SchemaObject -import org.oppia.android.app.model.SchemaObjectList -import org.oppia.android.domain.classify.ClassificationContext /** * Tests for [AlgebraicExpressionInputIsEquivalentToRuleClassifierProvider]. diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProviderTest.kt index 0c6e401a976..0511f4c6d0a 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProviderTest.kt @@ -12,7 +12,9 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.oppia.android.app.model.InteractionObject -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.app.model.SchemaObject +import org.oppia.android.app.model.SchemaObjectList +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.testing.junit.OppiaParameterizedTestRunner import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Iteration @@ -29,9 +31,7 @@ import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode import javax.inject.Inject import javax.inject.Singleton -import org.oppia.android.app.model.SchemaObject -import org.oppia.android.app.model.SchemaObjectList -import org.oppia.android.domain.classify.ClassificationContext +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.DaggerAlgebraicExpressionInputMatchesExactlyWithRuleClassifierProviderTest_TestApplicationComponent as DaggerTestApplicationComponent /** * Tests for [AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider]. @@ -388,7 +388,9 @@ class AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProviderTest { "(a+b+c)^2!=a^2+b^2+c^2+2a*b+2a*c+2bc", "answer=(a+b+c)^2", "input=a^2+b^2+c^2+2a*b+2a*c+2bc" ), Iteration( - "(-a -b -c)^2!=a^2+b^2+c^2+2a*b+2a*c+2bc", "answer=(-a -b -c)^2", "input=a^2+b^2+c^2+2a*b+2a*c+2bc" + "(-a -b -c)^2!=a^2+b^2+c^2+2a*b+2a*c+2bc", + "answer=(-a -b -c)^2", + "input=a^2+b^2+c^2+2a*b+2a*c+2bc" ), Iteration("1 - 6x + 9x^2!=9x^2 − 6x + 1", "answer=1 - 6x + 9x^2", "input=9x^2 − 6x + 1"), Iteration("9x^2 + 1 - 6x!=9x^2 − 6x + 1", "answer=9x^2 + 1 - 6x", "input=9x^2 − 6x + 1"), @@ -612,7 +614,7 @@ class AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProviderTest { }.build() private fun setUpTestApplicationComponent() { - DaggerAlgebraicExpressionInputMatchesExactlyWithRuleClassifierProviderTest_TestApplicationComponent + DaggerTestApplicationComponent .builder() .setApplication(ApplicationProvider.getApplicationContext()).build().inject(this) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt index 19dd3fbc92e..22e3f4b749f 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt @@ -12,7 +12,9 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.oppia.android.app.model.InteractionObject -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.app.model.SchemaObject +import org.oppia.android.app.model.SchemaObjectList +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.testing.junit.OppiaParameterizedTestRunner import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Iteration @@ -29,12 +31,11 @@ import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode import javax.inject.Inject import javax.inject.Singleton -import org.oppia.android.app.model.SchemaObject -import org.oppia.android.app.model.SchemaObjectList -import org.oppia.android.domain.classify.ClassificationContext +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider as RuleClassifierProvider +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.DaggerAlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest_TestApplicationComponent as DaggerTestApplicationComponent /** - * Tests for [AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider]. + * Tests for [RuleClassifierProvider]. * * Note that the tests implemented in this suite are specifically set up to verify the cases * outlined in this sheet: @@ -47,7 +48,7 @@ import org.oppia.android.domain.classify.ClassificationContext @LooperMode(LooperMode.Mode.PAUSED) @Config(manifest = Config.NONE) class AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest { - @Inject internal lateinit var provider: AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider + @Inject internal lateinit var provider: RuleClassifierProvider @Parameter lateinit var answer: String @Parameter lateinit var input: String @@ -627,7 +628,7 @@ class AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvi }.build() private fun setUpTestApplicationComponent() { - DaggerAlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest_TestApplicationComponent + DaggerTestApplicationComponent .builder() .setApplication(ApplicationProvider.getApplicationContext()).build().inject(this) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModuleTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModuleTest.kt index 92ebc07cc5d..9d71d9b2a91 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModuleTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModuleTest.kt @@ -13,6 +13,7 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.oppia.android.domain.classify.RuleClassifier +import org.oppia.android.domain.classify.rules.AlgebraicExpressionInputRules import org.oppia.android.testing.robolectric.RobolectricModule import org.oppia.android.testing.threading.TestDispatcherModule import org.oppia.android.testing.time.FakeOppiaClockModule @@ -22,7 +23,6 @@ import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode import javax.inject.Inject import javax.inject.Singleton -import org.oppia.android.domain.classify.rules.AlgebraicExpressionInputRules /** Tests for [AlgebraicExpressionInputModule]. */ // FunctionName: test names are conventionally named with underscores. diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProviderTest.kt index 4ef5e2928c4..611fcb04b04 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProviderTest.kt @@ -12,7 +12,7 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.oppia.android.app.model.InteractionObject -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.testing.junit.OppiaParameterizedTestRunner import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Iteration @@ -29,7 +29,6 @@ import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode import javax.inject.Inject import javax.inject.Singleton -import org.oppia.android.domain.classify.ClassificationContext /** * Tests for [NumericExpressionInputIsEquivalentToRuleClassifierProvider]. diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest.kt index 228e1e3582e..b091adab625 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest.kt @@ -12,7 +12,7 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.oppia.android.app.model.InteractionObject -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.testing.junit.OppiaParameterizedTestRunner import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Iteration @@ -29,7 +29,6 @@ import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode import javax.inject.Inject import javax.inject.Singleton -import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.rules.numericexpressioninput.DaggerNumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest_TestApplicationComponent as DaggerTestApplicationComponent /** diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt index a9af6bd65fb..9e095d504dd 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt @@ -12,7 +12,7 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.oppia.android.app.model.InteractionObject -import org.oppia.android.app.model.WrittenTranslationContext +import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier import org.oppia.android.testing.junit.OppiaParameterizedTestRunner import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Iteration @@ -29,7 +29,6 @@ import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode import javax.inject.Inject import javax.inject.Singleton -import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.rules.numericexpressioninput.DaggerNumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest_TestApplicationComponent as DaggerTestApplicationComponent import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider as RuleClassifierProvider diff --git a/scripts/assets/file_content_validation_checks.textproto b/scripts/assets/file_content_validation_checks.textproto index 1408521c618..03c575af12c 100644 --- a/scripts/assets/file_content_validation_checks.textproto +++ b/scripts/assets/file_content_validation_checks.textproto @@ -286,6 +286,9 @@ file_content_checks { file_path_regex: ".+?\\.kt" prohibited_content_regex: "OppiaParameterizedTestRunner" failure_message: "To use OppiaParameterizedTestRunner, please add an exemption to file_content_validation_checks.textproto and add an explanation for your use case in your PR description. Note that parameterized tests should only be used in special circumstances where a single behavior can be tested across multiple inputs, or for especially large test suites that can be trivially reduced." + exempted_file_name: "domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputIsEquivalentToRuleClassifierProviderTest.kt" + exempted_file_name: "domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProviderTest.kt" + exempted_file_name: "domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt" exempted_file_name: "domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProviderTest.kt" exempted_file_name: "domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest.kt" exempted_file_name: "domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt" diff --git a/scripts/assets/test_file_exemptions.textproto b/scripts/assets/test_file_exemptions.textproto index c44fadf2533..6b58b289fd9 100644 --- a/scripts/assets/test_file_exemptions.textproto +++ b/scripts/assets/test_file_exemptions.textproto @@ -550,6 +550,7 @@ exempted_file_path: "data/src/main/java/org/oppia/android/data/backends/gae/mode exempted_file_path: "data/src/main/java/org/oppia/android/data/backends/gae/model/GaeVoiceover.kt" exempted_file_path: "data/src/main/java/org/oppia/android/data/backends/gae/model/GaeWrittenTranslation.kt" exempted_file_path: "data/src/main/java/org/oppia/android/data/backends/gae/model/GaeWrittenTranslations.kt" +exempted_file_path: "domain/src/main/java/org/oppia/android/domain/classify/ClassificationContext.kt" exempted_file_path: "domain/src/main/java/org/oppia/android/domain/classify/ClassificationResult.kt" exempted_file_path: "domain/src/main/java/org/oppia/android/domain/classify/GenericInteractionClassifier.kt" exempted_file_path: "domain/src/main/java/org/oppia/android/domain/classify/InteractionClassifier.kt" From fd0c0cc51a98ee6c09c82962189dd56c6baebfb4 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 4 Feb 2022 23:12:16 -0800 Subject: [PATCH 114/162] Fix test on Gradle. --- .../numericexpressioninput/NumericExpressionInputModuleTest.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputModuleTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputModuleTest.kt index b12cc16af95..9a5b411670d 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputModuleTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputModuleTest.kt @@ -31,8 +31,7 @@ import javax.inject.Singleton @LooperMode(LooperMode.Mode.PAUSED) @Config(manifest = Config.NONE) class NumericExpressionInputModuleTest { - @Inject - @NumericExpressionInputRules + @field:[Inject NumericExpressionInputRules] lateinit var numericExpressionInputClassifiers: Map @Before From 812b66e1b0aa34f6109b7ec72c8e1f37c4f4b839 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 4 Feb 2022 23:13:05 -0800 Subject: [PATCH 115/162] Fix test for Gradle. --- .../AlgebraicExpressionInputModuleTest.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModuleTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModuleTest.kt index 9d71d9b2a91..81560a49781 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModuleTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputModuleTest.kt @@ -31,8 +31,7 @@ import javax.inject.Singleton @LooperMode(LooperMode.Mode.PAUSED) @Config(manifest = Config.NONE) class AlgebraicExpressionInputModuleTest { - @Inject - @AlgebraicExpressionInputRules + @field:[Inject AlgebraicExpressionInputRules] lateinit var algebraicExpressionInputClassifiers: Map< String, @JvmSuppressWildcards RuleClassifier> From d8116366a0a84e6b2550440a0418ed77207a2caa Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Sat, 5 Feb 2022 00:35:07 -0800 Subject: [PATCH 116/162] Add tests for math equations. And, post-merge fixes. --- ...putIsEquivalentToRuleClassifierProvider.kt | 22 +- ...atchesExactlyWithRuleClassifierProvider.kt | 6 +- ...vialManipulationsRuleClassifierProvider.kt | 12 +- .../rules/mathequationinput/BUILD.bazel | 105 +++++ ...sEquivalentToRuleClassifierProviderTest.kt | 412 ++++++++++++++++++ ...esExactlyWithRuleClassifierProviderTest.kt | 397 +++++++++++++++++ ...ManipulationsRuleClassifierProviderTest.kt | 412 ++++++++++++++++++ .../MathEquationInputModuleTest.kt | 106 +++++ .../junit/OppiaParameterizedTestRunner.kt | 4 +- 9 files changed, 1462 insertions(+), 14 deletions(-) create mode 100644 domain/src/test/java/org/oppia/android/domain/classify/rules/mathequationinput/BUILD.bazel create mode 100644 domain/src/test/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputIsEquivalentToRuleClassifierProviderTest.kt create mode 100644 domain/src/test/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesExactlyWithRuleClassifierProviderTest.kt create mode 100644 domain/src/test/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt create mode 100644 domain/src/test/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputModuleTest.kt diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputIsEquivalentToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputIsEquivalentToRuleClassifierProvider.kt index 62552bb92b9..de26cf01fea 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputIsEquivalentToRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputIsEquivalentToRuleClassifierProvider.kt @@ -11,7 +11,10 @@ import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingRes import org.oppia.android.util.math.MathExpressionParser.Companion.parseAlgebraicEquation import org.oppia.android.util.math.toPolynomial import javax.inject.Inject -import org.oppia.android.util.math.approximatelyEquals +import org.oppia.android.util.math.isApproximatelyEqualTo +import org.oppia.android.util.math.minus +import org.oppia.android.util.math.sort +import org.oppia.android.util.math.unaryMinus class MathEquationInputIsEquivalentToRuleClassifierProvider @Inject constructor( private val classifierFactory: GenericRuleClassifier.Factory, @@ -34,9 +37,20 @@ class MathEquationInputIsEquivalentToRuleClassifierProvider @Inject constructor( val (answerLhs, answerRhs) = parsePolynomials(answer, allowedVariables) ?: return false val (inputLhs, inputRhs) = parsePolynomials(input, allowedVariables) ?: return false - // Sides may cross-match (i.e. it's fine to reorder around the '='). - return (answerLhs.approximatelyEquals(inputLhs) && answerRhs.approximatelyEquals(inputRhs)) || - (answerLhs.approximatelyEquals(inputRhs) && answerRhs.approximatelyEquals(inputLhs)) + val newAnswerLhs = (answerLhs - answerRhs).sort() + val newInputLhs = (inputLhs - inputRhs).sort() + val negativeAnswerLhs = (-newAnswerLhs).sort() + val negativeInputLhs = (-newInputLhs).sort() + + // By subtracting the right-hand sides of both equations with their left-hand sides, the + // right-hand side becomes zero for both and implicitly equal. If the new simplified left-hand + // sides are equal then the equations are equivalent regardless of how they were originally + // arranged. Furthermore, the '-1' check is correct since the order of the equation can flip + // depending on how it was inputted, and '-1 * 0=0' so the new right-hand side remains + // unaffected. + return newAnswerLhs.isApproximatelyEqualTo(newInputLhs) + || negativeAnswerLhs.isApproximatelyEqualTo(newInputLhs) + || newAnswerLhs.isApproximatelyEqualTo(negativeInputLhs) } private fun parsePolynomials( diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesExactlyWithRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesExactlyWithRuleClassifierProvider.kt index 8c9e79a9e9f..8cbf7b08b96 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesExactlyWithRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesExactlyWithRuleClassifierProvider.kt @@ -10,7 +10,7 @@ import org.oppia.android.util.logging.ConsoleLogger import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult import org.oppia.android.util.math.MathExpressionParser.Companion.parseAlgebraicEquation import javax.inject.Inject -import org.oppia.android.util.math.approximatelyEquals +import org.oppia.android.util.math.isApproximatelyEqualTo class MathEquationInputMatchesExactlyWithRuleClassifierProvider @Inject constructor( private val classifierFactory: GenericRuleClassifier.Factory, @@ -62,8 +62,8 @@ class MathEquationInputMatchesExactlyWithRuleClassifierProvider @Inject construc } private fun MathEquation.approximatelyEquals(other: MathEquation): Boolean { - return leftSide.approximatelyEquals(other.leftSide) - && rightSide.approximatelyEquals(other.rightSide) + return leftSide.isApproximatelyEqualTo(other.leftSide) + && rightSide.isApproximatelyEqualTo(other.rightSide) } } } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt index 690e5f3646b..207161b7c8d 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt @@ -1,6 +1,6 @@ package org.oppia.android.domain.classify.rules.mathequationinput -import org.oppia.android.app.model.ComparableOperationList +import org.oppia.android.app.model.ComparableOperation import org.oppia.android.app.model.InteractionObject import org.oppia.android.domain.classify.ClassificationContext import org.oppia.android.domain.classify.RuleClassifier @@ -9,9 +9,9 @@ import org.oppia.android.domain.classify.rules.RuleClassifierProvider import org.oppia.android.util.logging.ConsoleLogger import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult import org.oppia.android.util.math.MathExpressionParser.Companion.parseAlgebraicEquation -import org.oppia.android.util.math.toComparableOperationList +import org.oppia.android.util.math.toComparableOperation import javax.inject.Inject -import org.oppia.android.util.math.approximatelyEquals +import org.oppia.android.util.math.isApproximatelyEqualTo class MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProvider @Inject constructor( @@ -36,18 +36,18 @@ class MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProvider val (inputLhs, inputRhs) = parseComparableLists(input, allowedVariables) ?: return false // Sides must match (reordering around the '=' is not allowed by this classifier). - return answerLhs.approximatelyEquals(inputLhs) && answerRhs.approximatelyEquals(inputRhs) + return answerLhs.isApproximatelyEqualTo(inputLhs) && answerRhs.isApproximatelyEqualTo(inputRhs) } private fun parseComparableLists( rawEquation: String, allowedVariables: List - ): Pair? { + ): Pair? { return when (val eqResult = parseAlgebraicEquation(rawEquation, allowedVariables)) { is MathParsingResult.Success -> { val lhsExp = eqResult.result.leftSide val rhsExp = eqResult.result.rightSide - lhsExp.toComparableOperationList() to rhsExp.toComparableOperationList() + lhsExp.toComparableOperation() to rhsExp.toComparableOperation() } is MathParsingResult.Failure -> { consoleLogger.e( diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/mathequationinput/BUILD.bazel b/domain/src/test/java/org/oppia/android/domain/classify/rules/mathequationinput/BUILD.bazel new file mode 100644 index 00000000000..9cfc129f250 --- /dev/null +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/mathequationinput/BUILD.bazel @@ -0,0 +1,105 @@ +""" +Tests for math equation input classifiers. +""" + +load("@dagger//:workspace_defs.bzl", "dagger_rules") +load("//:oppia_android_test.bzl", "oppia_android_test") + +oppia_android_test( + name = "MathEquationInputIsEquivalentToRuleClassifierProviderTest", + srcs = ["MathEquationInputIsEquivalentToRuleClassifierProviderTest.kt"], + custom_package = "org.oppia.android.domain.classify.rules.mathequationinput", + test_class = "org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputIsEquivalentToRuleClassifierProviderTest", + test_manifest = "//domain:test_manifest", + deps = [ + ":dagger", + "//domain", + "//model/src/main/proto:interaction_object_java_proto_lite", + "//testing/src/main/java/org/oppia/android/testing/junit:oppia_parameterized_test_runner", + "//testing/src/main/java/org/oppia/android/testing/junit:parameterized_robolectric_test_runner", + "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module", + "//testing/src/main/java/org/oppia/android/testing/threading:test_module", + "//testing/src/main/java/org/oppia/android/testing/time:test_module", + "//third_party:androidx_test_core", + "//third_party:com_google_truth_truth", + "//third_party:junit_junit", + "//third_party:org_robolectric_robolectric", + "//third_party:robolectric_android-all", + "//utility/src/main/java/org/oppia/android/util/locale:prod_module", + "//utility/src/main/java/org/oppia/android/util/logging:prod_module", + ], +) + +oppia_android_test( + name = "MathEquationInputMatchesExactlyWithRuleClassifierProviderTest", + srcs = ["MathEquationInputMatchesExactlyWithRuleClassifierProviderTest.kt"], + custom_package = "org.oppia.android.domain.classify.rules.mathequationinput", + test_class = "org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputMatchesExactlyWithRuleClassifierProviderTest", + test_manifest = "//domain:test_manifest", + deps = [ + ":dagger", + "//domain", + "//model/src/main/proto:interaction_object_java_proto_lite", + "//testing/src/main/java/org/oppia/android/testing/junit:oppia_parameterized_test_runner", + "//testing/src/main/java/org/oppia/android/testing/junit:parameterized_robolectric_test_runner", + "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module", + "//testing/src/main/java/org/oppia/android/testing/threading:test_module", + "//testing/src/main/java/org/oppia/android/testing/time:test_module", + "//third_party:androidx_test_core", + "//third_party:com_google_truth_truth", + "//third_party:junit_junit", + "//third_party:org_robolectric_robolectric", + "//third_party:robolectric_android-all", + "//utility/src/main/java/org/oppia/android/util/locale:prod_module", + "//utility/src/main/java/org/oppia/android/util/logging:prod_module", + ], +) + +oppia_android_test( + name = "MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest", + srcs = ["MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt"], + custom_package = "org.oppia.android.domain.classify.rules.mathequationinput", + test_class = "org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest", + test_manifest = "//domain:test_manifest", + deps = [ + ":dagger", + "//domain", + "//model/src/main/proto:interaction_object_java_proto_lite", + "//testing/src/main/java/org/oppia/android/testing/junit:oppia_parameterized_test_runner", + "//testing/src/main/java/org/oppia/android/testing/junit:parameterized_robolectric_test_runner", + "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module", + "//testing/src/main/java/org/oppia/android/testing/threading:test_module", + "//testing/src/main/java/org/oppia/android/testing/time:test_module", + "//third_party:androidx_test_core", + "//third_party:com_google_truth_truth", + "//third_party:junit_junit", + "//third_party:org_robolectric_robolectric", + "//third_party:robolectric_android-all", + "//utility/src/main/java/org/oppia/android/util/locale:prod_module", + "//utility/src/main/java/org/oppia/android/util/logging:prod_module", + ], +) + +oppia_android_test( + name = "MathEquationInputModuleTest", + srcs = ["MathEquationInputModuleTest.kt"], + custom_package = "org.oppia.android.domain.classify.rules.mathequationinput", + test_class = "org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModuleTest", + test_manifest = "//domain:test_manifest", + deps = [ + ":dagger", + "//domain", + "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module", + "//testing/src/main/java/org/oppia/android/testing/threading:test_module", + "//testing/src/main/java/org/oppia/android/testing/time:test_module", + "//third_party:androidx_test_ext_junit", + "//third_party:com_google_truth_truth", + "//third_party:junit_junit", + "//third_party:org_robolectric_robolectric", + "//third_party:robolectric_android-all", + "//utility/src/main/java/org/oppia/android/util/locale:prod_module", + "//utility/src/main/java/org/oppia/android/util/logging:prod_module", + ], +) + +dagger_rules() diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputIsEquivalentToRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputIsEquivalentToRuleClassifierProviderTest.kt new file mode 100644 index 00000000000..11d8bbbaac1 --- /dev/null +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputIsEquivalentToRuleClassifierProviderTest.kt @@ -0,0 +1,412 @@ +package org.oppia.android.domain.classify.rules.mathequationinput + +import android.app.Application +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import com.google.common.truth.Truth.assertThat +import dagger.BindsInstance +import dagger.Component +import dagger.Module +import dagger.Provides +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.oppia.android.app.model.InteractionObject +import org.oppia.android.app.model.SchemaObject +import org.oppia.android.app.model.SchemaObjectList +import org.oppia.android.domain.classify.ClassificationContext +import org.oppia.android.domain.classify.RuleClassifier +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Iteration +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Parameter +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.RunParameterized +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.SelectRunnerPlatform +import org.oppia.android.testing.junit.ParameterizedRobolectricTestRunner +import org.oppia.android.testing.robolectric.RobolectricModule +import org.oppia.android.testing.threading.TestDispatcherModule +import org.oppia.android.testing.time.FakeOppiaClockModule +import org.oppia.android.util.locale.LocaleProdModule +import org.oppia.android.util.logging.LoggerModule +import org.robolectric.annotation.Config +import org.robolectric.annotation.LooperMode +import javax.inject.Inject +import javax.inject.Singleton + +/** + * Tests for [MathEquationInputIsEquivalentToRuleClassifierProvider]. + * + * Note that the tests implemented in this suite are specifically set up to verify the cases + * outlined in this sheet: + * https://docs.google.com/spreadsheets/d/1u1fQdah2WsmdYKWKGmuXy5TPT7Ot-b8A7O9iZF-j5XE/edit#gid=0. + */ +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") +@RunWith(OppiaParameterizedTestRunner::class) +@SelectRunnerPlatform(ParameterizedRobolectricTestRunner::class) +@LooperMode(LooperMode.Mode.PAUSED) +@Config(manifest = Config.NONE) +class MathEquationInputIsEquivalentToRuleClassifierProviderTest { + @Inject + internal lateinit var provider: MathEquationInputIsEquivalentToRuleClassifierProvider + + @Parameter lateinit var answer: String + @Parameter lateinit var input: String + + private lateinit var classifier: RuleClassifier + private val allPossibleVariables = (('a'..'z') + ('A'..'Z')).toList().map { it.toString() } + + @Before + fun setUp() { + setUpTestApplicationComponent() + classifier = provider.createRuleClassifier() + } + + @Test + @RunParameterized( + Iteration("y=1!=y=1", "answer=y=1", "input=y=1"), + Iteration("1=y!=1=y", "answer=1=y", "input=1=y") + ) + fun testMatches_answerHasDisallowedVariable_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression, allowedVariables = listOf()) + + // Despite the answer otherwise being equal, the variable isn't allowed. This shouldn't actually + // be the case in practice since neither the creator nor the learner would be allowed to input a + // disallowed variable (so this check is mainly a "just-in-case"). + assertThat(matches).isFalse() + } + + @Test + @RunParameterized( + Iteration("0=1==0=1", "answer=0=1", "input=0=1"), + Iteration("y=0==y=0", "answer=y=0", "input=y=0"), + Iteration("y=1==y=1", "answer=y=1", "input=y=1"), + Iteration("0=y==0=y", "answer=0=y", "input=0=y"), + Iteration("1=y==1=y", "answer=1=y", "input=1=y"), + Iteration("y=x==y=x", "answer=y=x", "input=y=x"), + Iteration("x=y==x=y", "answer=x=y", "input=x=y") + ) + fun testMatches_sameSingleTerms_returnsTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // If the two expressions are exactly the same, the classifier should match. + assertThat(matches).isTrue() + } + + @Test + @RunParameterized( + Iteration("y=-x==y=-x", "answer=y=-x", "input=y=-x"), + Iteration("-y=x==-y=x", "answer=-y=x", "input=-y=x"), + Iteration("y=3.14+x==y=3.14+x", "answer=y=3.14+x", "input=y=3.14+x"), + Iteration("y=x+y+z==y=x+y+z", "answer=y=x+y+z", "input=y=x+y+z"), + Iteration("y=x/2/3==y=x/2/3", "answer=y=x/2/3", "input=y=x/2/3"), + Iteration("y=x^2==y=x^2", "answer=y=x^2", "input=y=x^2") + ) + fun testMatches_sameSingleOperations_returnsTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // If the two expressions are exactly the same, the classifier should match. + assertThat(matches).isTrue() + } + + @Test + @RunParameterized( + Iteration("y=x/y/z!=y=x/y/z", "answer=y=x/y/z", "input=y=x/y/z"), + Iteration("y=sqrt(x)!=y=sqrt(x)", "answer=y=sqrt(x)", "input=y=sqrt(x)") + ) + fun testMatches_sameSingleOperations_thatCannotBecomePolynomials_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // Despite the terms being the same, if they can't be converted to a polynomial then the + // classifier won't match. + assertThat(matches).isFalse() + } + + @Test + @RunParameterized( + Iteration("y=1!=y=0", "answer=y=1", "input=y=0"), + Iteration("y=0!=y=1", "answer=y=0", "input=y=1"), + Iteration("y=3.14!=y=1", "answer=y=3.14", "input=y=1"), + Iteration("y=1!=y=3.14", "answer=y=1", "input=y=3.14"), + Iteration("y=x!=y=3.14", "answer=y=x", "input=y=3.14"), + Iteration("y=1!=y=x", "answer=y=1", "input=y=x"), + Iteration("y=3.14!=y=x", "answer=y=3.14", "input=y=x"), + Iteration("y=z!=y=x", "answer=y=z", "input=y=x"), + Iteration("y=x!=y=z", "answer=y=x", "input=y=z") + ) + fun testMatches_differentSingleTerms_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // If the two expressions don't evaluate to the same value then the classifier won't match them. + assertThat(matches).isFalse() + } + + @Test + @RunParameterized( + Iteration("y=1+x==y=x+1", "answer=y=1+x", "input=y=x+1"), + Iteration("y=z+x==y=x+z", "answer=y=z+x", "input=y=x+z"), + Iteration("y+1=x==1+y=x", "answer=y+1=x", "input=1+y=x"), + Iteration("y+z=x==z+y=x", "answer=y+z=x", "input=z+y=x"), + Iteration("x+y=1+z==y+x=z+1", "answer=x+y=1+z", "input=y+x=z+1"), + Iteration("y=-x+1==y=1-x", "answer=y=-x+1", "input=y=1-x"), + Iteration("-y+x=z==x-y=z", "answer=-y+x=z", "input=x-y=z"), + Iteration("y=x*2==y=2x", "answer=y=x*2", "input=y=2x"), + Iteration("y*2=z==2y=z", "answer=y*2=z", "input=2y=z"), + Iteration("y=3*2==y=2*3", "answer=y=3*2", "input=y=2*3"), + Iteration("y=zx==y=xz", "answer=y=zx", "input=y=xz"), + Iteration("yx=z==xy=z", "answer=yx=z", "input=xy=z") + ) + fun testMatches_operationsDiffer_byCommutativity_returnsTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // Reordering terms by commutativity is allowed by this classifier. + assertThat(matches).isTrue() + } + + @Test + @RunParameterized( + Iteration("y=1+(2+3)==y=(1+2)+3", "answer=y=1+(2+3)", "input=y=(1+2)+3"), + Iteration("y=x+(y+z)==y=(x+y)+z", "answer=y=x+(y+z)", "input=y=(x+y)+z"), + Iteration("x+(y+z)=1==(x+y)+z=1", "answer=x+(y+z)=1", "input=(x+y)+z=1"), + Iteration( + "(x+y)+z=1+(2+3)==x+(y+z)=(1+2)+3", "answer=(x+y)+z=1+(2+3)", "input=x+(y+z)=(1+2)+3" + ), + Iteration("y=2*(3*4)==y=(2*3)*4", "answer=y=2*(3*4)", "input=y=(2*3)*4"), + Iteration("y=2*(3x)==y=(2x)*3", "answer=y=2*(3x)", "input=y=(2x)*3"), + Iteration("y=x(yz)==y=(xy)z", "answer=y=x(yz)", "input=y=(xy)z"), + Iteration("x(yz)=2==(xy)z=2", "answer=x(yz)=2", "input=(xy)z=2"), + Iteration("2*(3y)=4==(2y)*3=4", "answer=2*(3y)=4", "input=(2y)*3=4"), + Iteration("x(yz)=(2*3)*4==(xy)z=2*(3*4)", "answer=x(yz)=(2*3)*4", "input=(xy)z=2*(3*4)") + ) + fun testMatches_operationsDiffer_byAssociativity_returnsTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // Changing operation associativity is allowed by this classifier. + assertThat(matches).isTrue() + } + + @Test + @RunParameterized( + Iteration("y=3.14-1!=y=1-3.14", "answer=y=3.14-1", "input=y=1-3.14"), + Iteration("y=1-(2-3)!=y=(1-2)-3", "answer=y=1-(2-3)", "input=y=(1-2)-3"), + Iteration("y-1=3!=1-y=3", "answer=y-1=3", "input=1-y=3"), + Iteration("x-(y-z)=3!=(x-y)-z=3", "answer=x-(y-z)=3", "input=(x-y)-z=3"), + Iteration("y=3.14/x!=y=x/3.14", "answer=y=3.14/x", "input=y=x/3.14"), + Iteration("y/x=2!=x/y=2", "answer=y/x=2", "input=x/y=2"), + Iteration("y=3.14^2!=y=2^3.14", "answer=y=3.14^2", "input=y=2^3.14"), + Iteration("(3.14^2)y=2!=(2^3.14)y=2", "answer=(3.14^2)y=2", "input=(2^3.14)y=2"), + Iteration("y=x/(y/z)!=y=(x/y)/z", "answer=y=x/(y/z)", "input=y=(x/y)/z"), + Iteration("x/(y/z)=2!=(x/y)/z=2", "answer=x/(y/z)=2", "input=(x/y)/z=2") + ) + fun testMatches_operationsDiffer_byNonCommutativeOrAssociativeReordering_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // Non-commutative and non-associative reordering generally results in a different value, so the + // classifier will fail to match. + assertThat(matches).isFalse() + } + + @Test + @RunParameterized( + Iteration("y=1-2==y=-(2-1)", "answer=y=1-2", "input=y=-(2-1)"), + Iteration("y=1+2==y=1+1+1", "answer=y=1+2", "input=y=1+1+1"), + Iteration("y=1+2==y=1-(-2)", "answer=y=1+2", "input=y=1-(-2)"), + Iteration("y=4-6==y=1-2-1", "answer=y=4-6", "input=y=1-2-1"), + Iteration("y=2*3*2*2==y=2*3*4", "answer=y=2*3*2*2", "input=y=2*3*4"), + Iteration("y=-6-2==y=2*-(3+1)", "answer=y=-6-2", "input=y=2*-(3+1)"), + Iteration("y=2/3/2/2==y=2/3/4", "answer=y=2/3/2/2", "input=y=2/3/4"), + Iteration("y=2^(2+1)==y=2^3", "answer=y=2^(2+1)", "input=y=2^3"), + Iteration("y=2^(-1)==y=1/2", "answer=y=2^(-1)", "input=y=1/2"), + Iteration("z=x-y==z=-(y-x)", "answer=z=x-y", "input=z=-(y-x)"), + Iteration("y=2+x==y=1+x+1", "answer=y=2+x", "input=y=1+x+1"), + Iteration("y=1+x==y=1-(-x)", "answer=y=1+x", "input=y=1-(-x)"), + Iteration("y=-x==y=1-x-1", "answer=y=-x", "input=y=1-x-1"), + Iteration("y=4x==y=2*2*x", "answer=y=4x", "input=y=2*2*x"), + Iteration("y=2-6x==y=2*(-3x+1)", "answer=y=2-6x", "input=y=2*(-3x+1)"), + Iteration("y=x/4==y=x/2/2", "answer=y=x/4", "input=y=x/2/2"), + Iteration("y=x^(2+1)==y=x^3", "answer=y=x^(2+1)", "input=y=x^3"), + Iteration("y=x*(2^(-1))==y=x/2", "answer=y=x*(2^(-1))", "input=y=x/2"), + Iteration("y+2=x==1+1+y=x", "answer=y+2=x", "input=1+1+y=x"), + Iteration("(2^2)y=x+2==4y=x+2", "answer=(2^2)y=x+2", "input=4y=x+2"), + Iteration("y^(4-2)=3x==y^2=3x", "answer=y^(4-2)=3x", "input=y^2=3x"), + Iteration("y/2/2=3x==y/4=3x", "answer=y/2/2=3x", "input=y/4=3x") + ) + fun testMatches_operationsDiffer_byDistributionAndCombining_returnTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // This classifier supports any distribution or combining of terms. + assertThat(matches).isTrue() + } + + @Test + @RunParameterized( + Iteration("2+x=y==y=2+x", "answer=2+x=y", "input=y=2+x"), + Iteration("-3x^3=2y^2==2y^2=-3x^3", "answer=-3x^3=2y^2", "input=2y^2=-3x^3"), + Iteration("-4+x=2+y+1-1==2+y=x-4", "answer=-4+x=2+y+1-1", "input=2+y=x-4"), + Iteration("y=x-6==2+y=x-4", "answer=y=x-6", "input=2+y=x-4"), + Iteration("(1+1+1)*x=2*y/4==y/2=3x", "answer=(1+1+1)*x=2*y/4", "input=y/2=3x") + ) + fun testMatches_sidesRearrangedAroundEqualsSign_returnsTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // The classifier should match regardless of how the equation is laid out so long as it's equal + // without any multiples. + assertThat(matches).isTrue() + } + + @Test + @RunParameterized( + Iteration("y = 3x^2 - 4==y = 3x^2 - 4", "answer=y = 3x^2 - 4", "input=y = 3x^2 - 4"), + Iteration("y = -4 + 3x^2==y = 3x^2 - 4", "answer=y = -4 + 3x^2", "input=y = 3x^2 - 4"), + Iteration("y = x^2*3 - 4==y = 3x^2 - 4", "answer=y = x^2*3 - 4", "input=y = 3x^2 - 4"), + Iteration("y+4=3x^2==y = 3x^2 - 4", "answer=y+4=3x^2", "input=y = 3x^2 - 4"), + Iteration("y-3x^2=-4==y = 3x^2 - 4", "answer=y-3x^2=-4", "input=y = 3x^2 - 4"), + Iteration("-4=y-3x^2==y = 3x^2 - 4", "answer=-4=y-3x^2", "input=y = 3x^2 - 4"), + Iteration("3x^2-y=4==y = 3x^2 - 4", "answer=3x^2-y=4", "input=y = 3x^2 - 4"), + Iteration("3x^2=4+y==y = 3x^2 - 4", "answer=3x^2=4+y", "input=y = 3x^2 - 4"), + Iteration("y-x^2=2x^2-4==y = 3x^2 - 4", "answer=y-x^2=2x^2-4", "input=y = 3x^2 - 4"), + Iteration("y=x*(2^(1/2))==y=sqrt(2)x", "answer=y=x*(2^(1/2))", "input=y=sqrt(2)x"), + Iteration("y − 3x^2 = -4==y = 3x^2 - 4", "answer=y − 3x^2 = -4", "input=y = 3x^2 - 4"), + Iteration("3x^2 - 4 = y==y = 3x^2 - 4", "answer=3x^2 - 4 = y", "input=y = 3x^2 - 4"), + Iteration("y − 3x^2 + 4 = 0==y = 3x^2 - 4", "answer=y − 3x^2 + 4 = 0", "input=y = 3x^2 - 4"), + Iteration("y = (3x^3 - 4x)/x==y = 3x^2 - 4", "answer=y = (3x^3 - 4x)/x", "input=y = 3x^2 - 4"), + Iteration("y^2/y = 3x^2 - 4==y = 3x^2 - 4", "answer=y^2/y = 3x^2 - 4", "input=y = 3x^2 - 4") + ) + fun testMatches_assortedExpressions_withMatchingCharacteristics_returnsTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // This verifies a variety of expressions that per the PRD and technical specification should + // pass for this classifier. + assertThat(matches).isTrue() + } + + @Test + @RunParameterized( + Iteration("y = 3x^2 - 7!=y = 3x^2 - 4", "answer=y = 3x^2 - 7", "input=y = 3x^2 - 4"), + Iteration("x^2 = 3y - 4!=y = 3x^2 - 4", "answer=x^2 = 3y - 4", "input=y = 3x^2 - 4"), + Iteration("y/(3x^2 - 4) = 1!=y = 3x^2 - 4", "answer=y/(3x^2 - 4) = 1", "input=y = 3x^2 - 4"), + Iteration("y + 3x^2 - 4 = 0!=y = 3x^2 - 4", "answer=y + 3x^2 - 4 = 0", "input=y = 3x^2 - 4"), + Iteration("y^2 = 3x^2y - 4y!=y = 3x^2 - 4", "answer=y^2 = 3x^2y - 4y", "input=y = 3x^2 - 4"), + Iteration( + "y^2 * y^−1 = -12x^2!=y = 3x^2 - 4", "answer=y^2 * y^−1 = -12x^2", "input=y = 3x^2 - 4" + ), + Iteration("2 − 3 = -4!=y = 3x^2 - 4", "answer=2 − 3 = -4", "input=y = 3x^2 - 4"), + Iteration("y = 3x^2 + 4!=y = 3x^2 - 4", "answer=y = 3x^2 + 4", "input=y = 3x^2 - 4"), + Iteration("y - 4 = 3x^2!=y = 3x^2 - 4", "answer=y - 4 = 3x^2", "input=y = 3x^2 - 4"), + Iteration("y − 3x^2 - 4 = 0!=y = 3x^2 - 4", "answer=y − 3x^2 - 4 = 0", "input=y = 3x^2 - 4"), + Iteration("0 = y + 3x^2 - 4!=y = 3x^2 - 4", "answer=0 = y + 3x^2 - 4", "input=y = 3x^2 - 4"), + Iteration("2 = 0!=y = 3x^2 - 4", "answer=2 = 0", "input=y = 3x^2 - 4"), + Iteration("y=3x-4!=y = 3x^2 - 4", "answer=y=3x-4", "input=y = 3x^2 - 4"), + Iteration("y - x^2 = -4!=y = 3x^2 - 4", "answer=y - x^2 = -4", "input=y = 3x^2 - 4"), + Iteration("y/sqrt(2)=x!=y=sqrt(2)x", "answer=y/sqrt(2)=x", "input=y=sqrt(2)x"), + Iteration("y/4=x!=y=4x", "answer=y/4=x", "input=y=4x"), + Iteration("y/4=16x!=y=4x", "answer=y/4=16x", "input=y=4x"), + Iteration("xy=x^2!=y=x", "answer=xy=x^2", "input=y=x") + ) + fun testMatches_assortedExpressions_withoutMatchingCharacteristics_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // This verifies a variety of expressions that per the PRD and technical specification should + // not pass for this classifier. + assertThat(matches).isFalse() + } + + private fun matchesClassifier( + answerExpression: InteractionObject, + inputExpression: InteractionObject, + allowedVariables: List = allPossibleVariables + ): Boolean { + return classifier.matches( + answerExpression, + inputs = mapOf("x" to inputExpression), + classificationContext = ClassificationContext( + customizationArgs = mapOf( + "customOskLetters" to SchemaObject.newBuilder().apply { + schemaObjectList = SchemaObjectList.newBuilder().apply { + addAllSchemaObject( + allowedVariables.map { + SchemaObject.newBuilder().setNormalizedString(it).build() + } + ) + }.build() + }.build() + ) + ) + ) + } + + private fun createMathExpression(rawExpression: String) = InteractionObject.newBuilder().apply { + mathExpression = rawExpression + }.build() + + private fun setUpTestApplicationComponent() { + DaggerMathEquationInputIsEquivalentToRuleClassifierProviderTest_TestApplicationComponent + .builder() + .setApplication(ApplicationProvider.getApplicationContext()).build().inject(this) + } + + // TODO(#89): Move this to a common test application component. + @Module + class TestModule { + @Provides + @Singleton + fun provideContext(application: Application): Context { + return application + } + } + + // TODO(#89): Move this to a common test application component. + @Singleton + @Component( + modules = [ + TestModule::class, LocaleProdModule::class, FakeOppiaClockModule::class, + TestDispatcherModule::class, LoggerModule::class, RobolectricModule::class + ] + ) + interface TestApplicationComponent { + @Component.Builder + interface Builder { + @BindsInstance + fun setApplication(application: Application): Builder + + fun build(): TestApplicationComponent + } + + fun inject(test: MathEquationInputIsEquivalentToRuleClassifierProviderTest) + } +} diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesExactlyWithRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesExactlyWithRuleClassifierProviderTest.kt new file mode 100644 index 00000000000..7c8a7d596d1 --- /dev/null +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesExactlyWithRuleClassifierProviderTest.kt @@ -0,0 +1,397 @@ +package org.oppia.android.domain.classify.rules.mathequationinput + +import android.app.Application +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import com.google.common.truth.Truth.assertThat +import dagger.BindsInstance +import dagger.Component +import dagger.Module +import dagger.Provides +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.oppia.android.app.model.InteractionObject +import org.oppia.android.app.model.SchemaObject +import org.oppia.android.app.model.SchemaObjectList +import org.oppia.android.domain.classify.ClassificationContext +import org.oppia.android.domain.classify.RuleClassifier +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Iteration +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Parameter +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.RunParameterized +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.SelectRunnerPlatform +import org.oppia.android.testing.junit.ParameterizedRobolectricTestRunner +import org.oppia.android.testing.robolectric.RobolectricModule +import org.oppia.android.testing.threading.TestDispatcherModule +import org.oppia.android.testing.time.FakeOppiaClockModule +import org.oppia.android.util.locale.LocaleProdModule +import org.oppia.android.util.logging.LoggerModule +import org.robolectric.annotation.Config +import org.robolectric.annotation.LooperMode +import javax.inject.Inject +import javax.inject.Singleton + +/** + * Tests for [MathEquationInputMatchesExactlyWithRuleClassifierProvider]. + * + * Note that the tests implemented in this suite are specifically set up to verify the cases + * outlined in this sheet: + * https://docs.google.com/spreadsheets/d/1u1fQdah2WsmdYKWKGmuXy5TPT7Ot-b8A7O9iZF-j5XE/edit#gid=0. + */ +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") +@RunWith(OppiaParameterizedTestRunner::class) +@SelectRunnerPlatform(ParameterizedRobolectricTestRunner::class) +@LooperMode(LooperMode.Mode.PAUSED) +@Config(manifest = Config.NONE) +class MathEquationInputMatchesExactlyWithRuleClassifierProviderTest { + @Inject + internal lateinit var provider: MathEquationInputMatchesExactlyWithRuleClassifierProvider + + @Parameter lateinit var answer: String + @Parameter lateinit var input: String + + private lateinit var classifier: RuleClassifier + private val allPossibleVariables = (('a'..'z') + ('A'..'Z')).toList().map { it.toString() } + + @Before + fun setUp() { + setUpTestApplicationComponent() + classifier = provider.createRuleClassifier() + } + + @Test + @RunParameterized( + Iteration("y=1!=y=1", "answer=y=1", "input=y=1"), + Iteration("1=y!=1=y", "answer=1=y", "input=1=y") + ) + fun testMatches_answerHasDisallowedVariable_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression, allowedVariables = listOf()) + + // Despite the answer otherwise being equal, the variable isn't allowed. This shouldn't actually + // be the case in practice since neither the creator nor the learner would be allowed to input a + // disallowed variable (so this check is mainly a "just-in-case"). + assertThat(matches).isFalse() + } + + @Test + @RunParameterized( + Iteration("0=1==0=1", "answer=0=1", "input=0=1"), + Iteration("y=0==y=0", "answer=y=0", "input=y=0"), + Iteration("y=1==y=1", "answer=y=1", "input=y=1"), + Iteration("0=y==0=y", "answer=0=y", "input=0=y"), + Iteration("1=y==1=y", "answer=1=y", "input=1=y"), + Iteration("y=x==y=x", "answer=y=x", "input=y=x"), + Iteration("x=y==x=y", "answer=x=y", "input=x=y") + ) + fun testMatches_sameSingleTerms_returnsTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // If the two expressions are exactly the same, the classifier should match. + assertThat(matches).isTrue() + } + + @Test + @RunParameterized( + Iteration("y=-x==y=-x", "answer=y=-x", "input=y=-x"), + Iteration("-y=x==-y=x", "answer=-y=x", "input=-y=x"), + Iteration("y=3.14+x==y=3.14+x", "answer=y=3.14+x", "input=y=3.14+x"), + Iteration("y=x+y+z==y=x+y+z", "answer=y=x+y+z", "input=y=x+y+z"), + Iteration("y=x/y/z==y=x/y/z", "answer=y=x/y/z", "input=y=x/y/z"), + Iteration("y=x/2/3==y=x/2/3", "answer=y=x/2/3", "input=y=x/2/3"), + Iteration("y=x^2==y=x^2", "answer=y=x^2", "input=y=x^2"), + Iteration("y=sqrt(x)==y=sqrt(x)", "answer=y=sqrt(x)", "input=y=sqrt(x)") + ) + fun testMatches_sameSingleOperations_returnsTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // If the two expressions are exactly the same, the classifier should match. + assertThat(matches).isTrue() + } + + @Test + @RunParameterized( + Iteration("y=1!=y=0", "answer=y=1", "input=y=0"), + Iteration("y=0!=y=1", "answer=y=0", "input=y=1"), + Iteration("y=3.14!=y=1", "answer=y=3.14", "input=y=1"), + Iteration("y=1!=y=3.14", "answer=y=1", "input=y=3.14"), + Iteration("y=x!=y=3.14", "answer=y=x", "input=y=3.14"), + Iteration("y=1!=y=x", "answer=y=1", "input=y=x"), + Iteration("y=3.14!=y=x", "answer=y=3.14", "input=y=x"), + Iteration("y=z!=y=x", "answer=y=z", "input=y=x"), + Iteration("y=x!=y=z", "answer=y=x", "input=y=z") + ) + fun testMatches_differentSingleTerms_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // If the two expressions aren't exactly the same (minus whitespace), they won't match. + assertThat(matches).isFalse() + } + + @Test + @RunParameterized( + Iteration("y=1+x!=y=x+1", "answer=y=1+x", "input=y=x+1"), + Iteration("y=z+x!=y=x+z", "answer=y=z+x", "input=y=x+z"), + Iteration("y+1=x!=1+y=x", "answer=y+1=x", "input=1+y=x"), + Iteration("y+z=x!=z+y=x", "answer=y+z=x", "input=z+y=x"), + Iteration("x+y=1+z!=y+x=z+1", "answer=x+y=1+z", "input=y+x=z+1"), + Iteration("y=-x+1!=y=1-x", "answer=y=-x+1", "input=y=1-x"), + Iteration("-y+x=z!=x-y=z", "answer=-y+x=z", "input=x-y=z"), + Iteration("y=x*2!=y=2x", "answer=y=x*2", "input=y=2x"), + Iteration("y*2=z!=2y=z", "answer=y*2=z", "input=2y=z"), + Iteration("y=3*2!=y=2*3", "answer=y=3*2", "input=y=2*3"), + Iteration("y=zx!=y=xz", "answer=y=zx", "input=y=xz"), + Iteration("yx=z!=xy=z", "answer=yx=z", "input=xy=z") + ) + fun testMatches_operationsDiffer_byCommutativity_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // This classifier expects commutativity to be retained. + assertThat(matches).isFalse() + } + + @Test + @RunParameterized( + Iteration("y=1+(2+3)!=y=(1+2)+3", "answer=y=1+(2+3)", "input=y=(1+2)+3"), + Iteration("y=x+(y+z)!=y=(x+y)+z", "answer=y=x+(y+z)", "input=y=(x+y)+z"), + Iteration("x+(y+z)=1!=(x+y)+z=1", "answer=x+(y+z)=1", "input=(x+y)+z=1"), + Iteration( + "(x+y)+z=1+(2+3)!=x+(y+z)=(1+2)+3", "answer=(x+y)+z=1+(2+3)", "input=x+(y+z)=(1+2)+3" + ), + Iteration("y=2*(3*4)!=y=(2*3)*4", "answer=y=2*(3*4)", "input=y=(2*3)*4"), + Iteration("y=2*(3x)!=y=(2x)*3", "answer=y=2*(3x)", "input=y=(2x)*3"), + Iteration("y=x(yz)!=y=(xy)z", "answer=y=x(yz)", "input=y=(xy)z"), + Iteration("x(yz)=2!=(xy)z=2", "answer=x(yz)=2", "input=(xy)z=2"), + Iteration("2*(3y)=4!=(2y)*3=4", "answer=2*(3y)=4", "input=(2y)*3=4"), + Iteration("x(yz)=(2*3)*4!=(xy)z=2*(3*4)", "answer=x(yz)=(2*3)*4", "input=(xy)z=2*(3*4)") + ) + fun testMatches_operationsDiffer_byAssociativity_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // This classifier expects associativity to be retained. + assertThat(matches).isFalse() + } + + @Test + @RunParameterized( + Iteration("y=3.14-1!=y=1-3.14", "answer=y=3.14-1", "input=y=1-3.14"), + Iteration("y=1-(2-3)!=y=(1-2)-3", "answer=y=1-(2-3)", "input=y=(1-2)-3"), + Iteration("y-1=3!=1-y=3", "answer=y-1=3", "input=1-y=3"), + Iteration("x-(y-z)=3!=(x-y)-z=3", "answer=x-(y-z)=3", "input=(x-y)-z=3"), + Iteration("y=3.14/x!=y=x/3.14", "answer=y=3.14/x", "input=y=x/3.14"), + Iteration("y/x=2!=x/y=2", "answer=y/x=2", "input=x/y=2"), + Iteration("y=3.14^2!=y=2^3.14", "answer=y=3.14^2", "input=y=2^3.14"), + Iteration("(3.14^2)y=2!=(2^3.14)y=2", "answer=(3.14^2)y=2", "input=(2^3.14)y=2"), + Iteration("y=x/(y/z)!=y=(x/y)/z", "answer=y=x/(y/z)", "input=y=(x/y)/z"), + Iteration("x/(y/z)=2!=(x/y)/z=2", "answer=x/(y/z)=2", "input=(x/y)/z=2") + ) + fun testMatches_operationsDiffer_byNonCommutativeOrAssociativeReordering_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // Non-commutative and non-associative reordering generally results in a different value, so the + // classifier will fail to match. + assertThat(matches).isFalse() + } + + @Test + @RunParameterized( + Iteration("y=1-2!=y=-(2-1)", "answer=y=1-2", "input=y=-(2-1)"), + Iteration("y=1+2!=y=1+1+1", "answer=y=1+2", "input=y=1+1+1"), + Iteration("y=1+2!=y=1-(-2)", "answer=y=1+2", "input=y=1-(-2)"), + Iteration("y=4-6!=y=1-2-1", "answer=y=4-6", "input=y=1-2-1"), + Iteration("y=2*3*2*2!=y=2*3*4", "answer=y=2*3*2*2", "input=y=2*3*4"), + Iteration("y=-6-2!=y=2*-(3+1)", "answer=y=-6-2", "input=y=2*-(3+1)"), + Iteration("y=2/3/2/2!=y=2/3/4", "answer=y=2/3/2/2", "input=y=2/3/4"), + Iteration("y=2^(2+1)!=y=2^3", "answer=y=2^(2+1)", "input=y=2^3"), + Iteration("y=2^(-1)!=y=1/2", "answer=y=2^(-1)", "input=y=1/2"), + Iteration("z=x-y!=z=-(y-x)", "answer=z=x-y", "input=z=-(y-x)"), + Iteration("y=2+x!=y=1+x+1", "answer=y=2+x", "input=y=1+x+1"), + Iteration("y=1+x!=y=1-(-x)", "answer=y=1+x", "input=y=1-(-x)"), + Iteration("y=-x!=y=1-x-1", "answer=y=-x", "input=y=1-x-1"), + Iteration("y=4x!=y=2*2*x", "answer=y=4x", "input=y=2*2*x"), + Iteration("y=2-6x!=y=2*(-3x+1)", "answer=y=2-6x", "input=y=2*(-3x+1)"), + Iteration("y=x/4!=y=x/2/2", "answer=y=x/4", "input=y=x/2/2"), + Iteration("y=x^(2+1)!=y=x^3", "answer=y=x^(2+1)", "input=y=x^3"), + Iteration("y=x*(2^(-1))!=y=x/2", "answer=y=x*(2^(-1))", "input=y=x/2"), + Iteration("y+2=x!=1+1+y=x", "answer=y+2=x", "input=1+1+y=x"), + Iteration("(2^2)y=x+2!=4y=x+2", "answer=(2^2)y=x+2", "input=4y=x+2"), + Iteration("y^(4-2)=3x!=y^2=3x", "answer=y^(4-2)=3x", "input=y^2=3x"), + Iteration("y/2/2=3x!=y/4=3x", "answer=y/2/2=3x", "input=y/4=3x") + ) + fun testMatches_operationsDiffer_byDistributionAndCombining_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // This classifier doesn't support distributing or combining terms. + assertThat(matches).isFalse() + } + + @Test + @RunParameterized( + Iteration("2+x=y!=y=2+x", "answer=2+x=y", "input=y=2+x"), + Iteration("-3x^3=2y^2!=2y^2=-3x^3", "answer=-3x^3=2y^2", "input=2y^2=-3x^3"), + Iteration("-4+x=2+y+1-1!=2+y=x-4", "answer=-4+x=2+y+1-1", "input=2+y=x-4"), + Iteration("y=x-6!=2+y=x-4", "answer=y=x-6", "input=2+y=x-4"), + Iteration("(1+1+1)*x=2*y/4!=y/2=3x", "answer=(1+1+1)*x=2*y/4", "input=y/2=3x") + ) + fun testMatches_sidesRearrangedAroundEqualsSign_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // This classifier doesn't support rearranging the left or right-hand sides of the equation. + assertThat(matches).isFalse() + } + + @Test + @RunParameterized( + Iteration("y = 3x^2 - 4==y = 3x^2 - 4", "answer=y = 3x^2 - 4", "input=y = 3x^2 - 4") + ) + fun testMatches_assortedExpressions_withMatchingCharacteristics_returnsTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // This verifies a variety of expressions that per the PRD and technical specification should + // pass for this classifier. + assertThat(matches).isTrue() + } + + @Test + @RunParameterized( + Iteration("y = -4 + 3x^2!=y = 3x^2 - 4", "answer=y = -4 + 3x^2", "input=y = 3x^2 - 4"), + Iteration("y = x^2*3 - 4!=y = 3x^2 - 4", "answer=y = x^2*3 - 4", "input=y = 3x^2 - 4"), + Iteration("y+4=3x^2!=y = 3x^2 - 4", "answer=y+4=3x^2", "input=y = 3x^2 - 4"), + Iteration("y-3x^2=-4!=y = 3x^2 - 4", "answer=y-3x^2=-4", "input=y = 3x^2 - 4"), + Iteration("-4=y-3x^2!=y = 3x^2 - 4", "answer=-4=y-3x^2", "input=y = 3x^2 - 4"), + Iteration("3x^2-y=4!=y = 3x^2 - 4", "answer=3x^2-y=4", "input=y = 3x^2 - 4"), + Iteration("3x^2=4+y!=y = 3x^2 - 4", "answer=3x^2=4+y", "input=y = 3x^2 - 4"), + Iteration("y-x^2=2x^2-4!=y = 3x^2 - 4", "answer=y-x^2=2x^2-4", "input=y = 3x^2 - 4"), + Iteration("y=x*(2^(1/2))!=y=sqrt(2)x", "answer=y=x*(2^(1/2))", "input=y=sqrt(2)x"), + Iteration("y − 3x^2 = -4!=y = 3x^2 - 4", "answer=y − 3x^2 = -4", "input=y = 3x^2 - 4"), + Iteration("3x^2 - 4 = y!=y = 3x^2 - 4", "answer=3x^2 - 4 = y", "input=y = 3x^2 - 4"), + Iteration("y = 3x^2 - 7!=y = 3x^2 - 4", "answer=y = 3x^2 - 7", "input=y = 3x^2 - 4"), + Iteration("x^2 = 3y - 4!=y = 3x^2 - 4", "answer=x^2 = 3y - 4", "input=y = 3x^2 - 4"), + Iteration("y/(3x^2 - 4) = 1!=y = 3x^2 - 4", "answer=y/(3x^2 - 4) = 1", "input=y = 3x^2 - 4"), + Iteration("y + 3x^2 - 4 = 0!=y = 3x^2 - 4", "answer=y + 3x^2 - 4 = 0", "input=y = 3x^2 - 4"), + Iteration("y − 3x^2 + 4 = 0!=y = 3x^2 - 4", "answer=y − 3x^2 + 4 = 0", "input=y = 3x^2 - 4"), + Iteration("y^2 = 3x^2y - 4y!=y = 3x^2 - 4", "answer=y^2 = 3x^2y - 4y", "input=y = 3x^2 - 4"), + Iteration("y = (3x^3 - 4x)/x!=y = 3x^2 - 4", "answer=y = (3x^3 - 4x)/x", "input=y = 3x^2 - 4"), + Iteration( + "y^2 * y^−1 = -12x^2!=y = 3x^2 - 4", "answer=y^2 * y^−1 = -12x^2", "input=y = 3x^2 - 4" + ), + Iteration("y^2/y = 3x^2 - 4!=y = 3x^2 - 4", "answer=y^2/y = 3x^2 - 4", "input=y = 3x^2 - 4"), + Iteration("2 − 3 = -4!=y = 3x^2 - 4", "answer=2 − 3 = -4", "input=y = 3x^2 - 4"), + Iteration("y = 3x^2 + 4!=y = 3x^2 - 4", "answer=y = 3x^2 + 4", "input=y = 3x^2 - 4"), + Iteration("y - 4 = 3x^2!=y = 3x^2 - 4", "answer=y - 4 = 3x^2", "input=y = 3x^2 - 4"), + Iteration("y − 3x^2 - 4 = 0!=y = 3x^2 - 4", "answer=y − 3x^2 - 4 = 0", "input=y = 3x^2 - 4"), + Iteration("0 = y + 3x^2 - 4!=y = 3x^2 - 4", "answer=0 = y + 3x^2 - 4", "input=y = 3x^2 - 4"), + Iteration("2 = 0!=y = 3x^2 - 4", "answer=2 = 0", "input=y = 3x^2 - 4"), + Iteration("y - x^2 = -4!=y = 3x^2 - 4", "answer=y - x^2 = -4", "input=y = 3x^2 - 4"), + Iteration("y=3x-4!=y = 3x^2 - 4", "answer=y=3x-4", "input=y = 3x^2 - 4"), + Iteration("y/sqrt(2)=x!=y=sqrt(2)x", "answer=y/sqrt(2)=x", "input=y=sqrt(2)x"), + Iteration("y/4=x!=y=4x", "answer=y/4=x", "input=y=4x"), + Iteration("y/4=16x!=y=4x", "answer=y/4=16x", "input=y=4x"), + Iteration("xy=x^2!=y=x", "answer=xy=x^2", "input=y=x") + ) + fun testMatches_assortedExpressions_withoutMatchingCharacteristics_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // This verifies a variety of expressions that per the PRD and technical specification should + // not pass for this classifier. + assertThat(matches).isFalse() + } + + private fun matchesClassifier( + answerExpression: InteractionObject, + inputExpression: InteractionObject, + allowedVariables: List = allPossibleVariables + ): Boolean { + return classifier.matches( + answerExpression, + inputs = mapOf("x" to inputExpression), + classificationContext = ClassificationContext( + customizationArgs = mapOf( + "customOskLetters" to SchemaObject.newBuilder().apply { + schemaObjectList = SchemaObjectList.newBuilder().apply { + addAllSchemaObject( + allowedVariables.map { + SchemaObject.newBuilder().setNormalizedString(it).build() + } + ) + }.build() + }.build() + ) + ) + ) + } + + private fun createMathExpression(rawExpression: String) = InteractionObject.newBuilder().apply { + mathExpression = rawExpression + }.build() + + private fun setUpTestApplicationComponent() { + DaggerMathEquationInputMatchesExactlyWithRuleClassifierProviderTest_TestApplicationComponent + .builder() + .setApplication(ApplicationProvider.getApplicationContext()).build().inject(this) + } + + // TODO(#89): Move this to a common test application component. + @Module + class TestModule { + @Provides + @Singleton + fun provideContext(application: Application): Context { + return application + } + } + + // TODO(#89): Move this to a common test application component. + @Singleton + @Component( + modules = [ + TestModule::class, LocaleProdModule::class, FakeOppiaClockModule::class, + TestDispatcherModule::class, LoggerModule::class, RobolectricModule::class + ] + ) + interface TestApplicationComponent { + @Component.Builder + interface Builder { + @BindsInstance + fun setApplication(application: Application): Builder + + fun build(): TestApplicationComponent + } + + fun inject(test: MathEquationInputMatchesExactlyWithRuleClassifierProviderTest) + } +} diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt new file mode 100644 index 00000000000..604ab230cb3 --- /dev/null +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt @@ -0,0 +1,412 @@ +package org.oppia.android.domain.classify.rules.mathequationinput + +import android.app.Application +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import com.google.common.truth.Truth.assertThat +import dagger.BindsInstance +import dagger.Component +import dagger.Module +import dagger.Provides +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.oppia.android.app.model.InteractionObject +import org.oppia.android.app.model.SchemaObject +import org.oppia.android.app.model.SchemaObjectList +import org.oppia.android.domain.classify.ClassificationContext +import org.oppia.android.domain.classify.RuleClassifier +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Iteration +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Parameter +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.RunParameterized +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.SelectRunnerPlatform +import org.oppia.android.testing.junit.ParameterizedRobolectricTestRunner +import org.oppia.android.testing.robolectric.RobolectricModule +import org.oppia.android.testing.threading.TestDispatcherModule +import org.oppia.android.testing.time.FakeOppiaClockModule +import org.oppia.android.util.locale.LocaleProdModule +import org.oppia.android.util.logging.LoggerModule +import org.robolectric.annotation.Config +import org.robolectric.annotation.LooperMode +import javax.inject.Inject +import javax.inject.Singleton + +/** + * Tests for [MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProvider]. + * + * Note that the tests implemented in this suite are specifically set up to verify the cases + * outlined in this sheet: + * https://docs.google.com/spreadsheets/d/1u1fQdah2WsmdYKWKGmuXy5TPT7Ot-b8A7O9iZF-j5XE/edit#gid=0. + */ +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") +@RunWith(OppiaParameterizedTestRunner::class) +@SelectRunnerPlatform(ParameterizedRobolectricTestRunner::class) +@LooperMode(LooperMode.Mode.PAUSED) +@Config(manifest = Config.NONE) +class MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest { + @Inject internal lateinit var provider: MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProvider + + @Parameter lateinit var answer: String + @Parameter lateinit var input: String + + private lateinit var classifier: RuleClassifier + private val allPossibleVariables = (('a'..'z') + ('A'..'Z')).toList().map { it.toString() } + + @Before + fun setUp() { + setUpTestApplicationComponent() + classifier = provider.createRuleClassifier() + } + + @Test + @RunParameterized( + Iteration("y=1!=y=1", "answer=y=1", "input=y=1"), + Iteration("1=y!=1=y", "answer=1=y", "input=1=y") + ) + fun testMatches_answerHasDisallowedVariable_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression, allowedVariables = listOf()) + + // Despite the answer otherwise being equal, the variable isn't allowed. This shouldn't actually + // be the case in practice since neither the creator nor the learner would be allowed to input a + // disallowed variable (so this check is mainly a "just-in-case"). + assertThat(matches).isFalse() + } + + @Test + @RunParameterized( + Iteration("0=1==0=1", "answer=0=1", "input=0=1"), + Iteration("y=0==y=0", "answer=y=0", "input=y=0"), + Iteration("y=1==y=1", "answer=y=1", "input=y=1"), + Iteration("0=y==0=y", "answer=0=y", "input=0=y"), + Iteration("1=y==1=y", "answer=1=y", "input=1=y"), + Iteration("y=x==y=x", "answer=y=x", "input=y=x"), + Iteration("x=y==x=y", "answer=x=y", "input=x=y") + ) + fun testMatches_sameSingleTerms_returnsTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // If the two expressions are exactly the same, the classifier should match. + assertThat(matches).isTrue() + } + + @Test + @RunParameterized( + Iteration("y=-x==y=-x", "answer=y=-x", "input=y=-x"), + Iteration("-y=x==-y=x", "answer=-y=x", "input=-y=x"), + Iteration("y=3.14+x==y=3.14+x", "answer=y=3.14+x", "input=y=3.14+x"), + Iteration("y=x+y+z==y=x+y+z", "answer=y=x+y+z", "input=y=x+y+z"), + Iteration("y=x/y/z==y=x/y/z", "answer=y=x/y/z", "input=y=x/y/z"), + Iteration("y=x/2/3==y=x/2/3", "answer=y=x/2/3", "input=y=x/2/3"), + Iteration("y=x^2==y=x^2", "answer=y=x^2", "input=y=x^2"), + Iteration("y=sqrt(x)==y=sqrt(x)", "answer=y=sqrt(x)", "input=y=sqrt(x)") + ) + fun testMatches_sameSingleOperations_returnsTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // If the two expressions are exactly the same, the classifier should match. + assertThat(matches).isTrue() + } + + @Test + @RunParameterized( + Iteration("y=1!=y=0", "answer=y=1", "input=y=0"), + Iteration("y=0!=y=1", "answer=y=0", "input=y=1"), + Iteration("y=3.14!=y=1", "answer=y=3.14", "input=y=1"), + Iteration("y=1!=y=3.14", "answer=y=1", "input=y=3.14"), + Iteration("y=x!=y=3.14", "answer=y=x", "input=y=3.14"), + Iteration("y=1!=y=x", "answer=y=1", "input=y=x"), + Iteration("y=3.14!=y=x", "answer=y=3.14", "input=y=x"), + Iteration("y=z!=y=x", "answer=y=z", "input=y=x"), + Iteration("y=x!=y=z", "answer=y=x", "input=y=z") + ) + fun testMatches_differentSingleTerms_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // If the two expressions aren't exactly the same (minus whitespace and some minor term + // reordering), they won't match. + assertThat(matches).isFalse() + } + + @Test + @RunParameterized( + Iteration("y=1+x==y=x+1", "answer=y=1+x", "input=y=x+1"), + Iteration("y=z+x==y=x+z", "answer=y=z+x", "input=y=x+z"), + Iteration("y+1=x==1+y=x", "answer=y+1=x", "input=1+y=x"), + Iteration("y+z=x==z+y=x", "answer=y+z=x", "input=z+y=x"), + Iteration("x+y=1+z==y+x=z+1", "answer=x+y=1+z", "input=y+x=z+1"), + Iteration("y=-x+1==y=1-x", "answer=y=-x+1", "input=y=1-x"), + Iteration("-y+x=z==x-y=z", "answer=-y+x=z", "input=x-y=z"), + Iteration("y=x*2==y=2x", "answer=y=x*2", "input=y=2x"), + Iteration("y*2=z==2y=z", "answer=y*2=z", "input=2y=z"), + Iteration("y=3*2==y=2*3", "answer=y=3*2", "input=y=2*3"), + Iteration("y=zx==y=xz", "answer=y=zx", "input=y=xz"), + Iteration("yx=z==xy=z", "answer=yx=z", "input=xy=z") + ) + fun testMatches_operationsDiffer_byCommutativity_returnsTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // Reordering terms by commutativity is allowed by this classifier. + assertThat(matches).isTrue() + } + + @Test + @RunParameterized( + Iteration("y=1+(2+3)==y=(1+2)+3", "answer=y=1+(2+3)", "input=y=(1+2)+3"), + Iteration("y=x+(y+z)==y=(x+y)+z", "answer=y=x+(y+z)", "input=y=(x+y)+z"), + Iteration("x+(y+z)=1==(x+y)+z=1", "answer=x+(y+z)=1", "input=(x+y)+z=1"), + Iteration( + "(x+y)+z=1+(2+3)==x+(y+z)=(1+2)+3", "answer=(x+y)+z=1+(2+3)", "input=x+(y+z)=(1+2)+3" + ), + Iteration("y=2*(3*4)==y=(2*3)*4", "answer=y=2*(3*4)", "input=y=(2*3)*4"), + Iteration("y=2*(3x)==y=(2x)*3", "answer=y=2*(3x)", "input=y=(2x)*3"), + Iteration("y=x(yz)==y=(xy)z", "answer=y=x(yz)", "input=y=(xy)z"), + Iteration("x(yz)=2==(xy)z=2", "answer=x(yz)=2", "input=(xy)z=2"), + Iteration("2*(3y)=4==(2y)*3=4", "answer=2*(3y)=4", "input=(2y)*3=4"), + Iteration("x(yz)=(2*3)*4==(xy)z=2*(3*4)", "answer=x(yz)=(2*3)*4", "input=(xy)z=2*(3*4)") + ) + fun testMatches_operationsDiffer_byAssociativity_returnsTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // Changing operation associativity is allowed by this classifier. + assertThat(matches).isTrue() + } + + @Test + @RunParameterized( + Iteration("y=3.14-1!=y=1-3.14", "answer=y=3.14-1", "input=y=1-3.14"), + Iteration("y=1-(2-3)!=y=(1-2)-3", "answer=y=1-(2-3)", "input=y=(1-2)-3"), + Iteration("y-1=3!=1-y=3", "answer=y-1=3", "input=1-y=3"), + Iteration("x-(y-z)=3!=(x-y)-z=3", "answer=x-(y-z)=3", "input=(x-y)-z=3"), + Iteration("y=3.14/x!=y=x/3.14", "answer=y=3.14/x", "input=y=x/3.14"), + Iteration("y/x=2!=x/y=2", "answer=y/x=2", "input=x/y=2"), + Iteration("y=3.14^2!=y=2^3.14", "answer=y=3.14^2", "input=y=2^3.14"), + Iteration("(3.14^2)y=2!=(2^3.14)y=2", "answer=(3.14^2)y=2", "input=(2^3.14)y=2"), + Iteration("y=x/(y/z)!=y=(x/y)/z", "answer=y=x/(y/z)", "input=y=(x/y)/z"), + Iteration("x/(y/z)=2!=(x/y)/z=2", "answer=x/(y/z)=2", "input=(x/y)/z=2") + ) + fun testMatches_operationsDiffer_byNonCommutativeOrAssociativeReordering_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // Non-commutative and non-associative reordering generally results in a different value, so the + // classifier will fail to match. + assertThat(matches).isFalse() + } + + @Test + @RunParameterized( + Iteration("y=1+2==y=1-(-2)", "answer=y=1+2", "input=y=1-(-2)"), + Iteration("y=1+x==y=1-(-x)", "answer=y=1+x", "input=y=1-(-x)") + ) + fun testMatches_operationsDiffer_byDistributingNegation_returnsTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // The classifier does support distributing negations (e.g. a*cross groups). + assertThat(matches).isTrue() + } + + @Test + @RunParameterized( + Iteration("y=1-2!=y=-(2-1)", "answer=y=1-2", "input=y=-(2-1)"), + Iteration("z=x-y!=z=-(y-x)", "answer=z=x-y", "input=z=-(y-x)"), + Iteration("y=1+2!=y=1+1+1", "answer=y=1+2", "input=y=1+1+1"), + Iteration("y=4-6!=y=1-2-1", "answer=y=4-6", "input=y=1-2-1"), + Iteration("y=2*3*2*2!=y=2*3*4", "answer=y=2*3*2*2", "input=y=2*3*4"), + Iteration("y=-6-2!=y=2*-(3+1)", "answer=y=-6-2", "input=y=2*-(3+1)"), + Iteration("y=2/3/2/2!=y=2/3/4", "answer=y=2/3/2/2", "input=y=2/3/4"), + Iteration("y=2^(2+1)!=y=2^3", "answer=y=2^(2+1)", "input=y=2^3"), + Iteration("y=2^(-1)!=y=1/2", "answer=y=2^(-1)", "input=y=1/2"), + Iteration("y=2+x!=y=1+x+1", "answer=y=2+x", "input=y=1+x+1"), + Iteration("y=-x!=y=1-x-1", "answer=y=-x", "input=y=1-x-1"), + Iteration("y=4x!=y=2*2*x", "answer=y=4x", "input=y=2*2*x"), + Iteration("y=2-6x!=y=2*(-3x+1)", "answer=y=2-6x", "input=y=2*(-3x+1)"), + Iteration("y=x/4!=y=x/2/2", "answer=y=x/4", "input=y=x/2/2"), + Iteration("y=x^(2+1)!=y=x^3", "answer=y=x^(2+1)", "input=y=x^3"), + Iteration("y=x*(2^(-1))!=y=x/2", "answer=y=x*(2^(-1))", "input=y=x/2"), + Iteration("y+2=x!=1+1+y=x", "answer=y+2=x", "input=1+1+y=x"), + Iteration("(2^2)y=x+2!=4y=x+2", "answer=(2^2)y=x+2", "input=4y=x+2"), + Iteration("y^(4-2)=3x!=y^2=3x", "answer=y^(4-2)=3x", "input=y^2=3x"), + Iteration("y/2/2=3x!=y/4=3x", "answer=y/2/2=3x", "input=y/4=3x") + ) + fun testMatches_operationsDiffer_byDistributionAndCombining_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // This classifier doesn't support broadly distributing or combining terms. + assertThat(matches).isFalse() + } + + @Test + @RunParameterized( + Iteration("2+x=y!=y=2+x", "answer=2+x=y", "input=y=2+x"), + Iteration("-3x^3=2y^2!=2y^2=-3x^3", "answer=-3x^3=2y^2", "input=2y^2=-3x^3"), + Iteration("-4+x=2+y+1-1!=2+y=x-4", "answer=-4+x=2+y+1-1", "input=2+y=x-4"), + Iteration("y=x-6!=2+y=x-4", "answer=y=x-6", "input=2+y=x-4"), + Iteration("(1+1+1)*x=2*y/4!=y/2=3x", "answer=(1+1+1)*x=2*y/4", "input=y/2=3x") + ) + fun testMatches_sidesRearrangedAroundEqualsSign_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // This classifier doesn't support rearranging the left or right-hand sides of the equation. + assertThat(matches).isFalse() + } + + @Test + @RunParameterized( + Iteration("y = 3x^2 - 4==y = 3x^2 - 4", "answer=y = 3x^2 - 4", "input=y = 3x^2 - 4"), + Iteration("y = -4 + 3x^2==y = 3x^2 - 4", "answer=y = -4 + 3x^2", "input=y = 3x^2 - 4"), + Iteration("y = x^2*3 - 4==y = 3x^2 - 4", "answer=y = x^2*3 - 4", "input=y = 3x^2 - 4") + ) + fun testMatches_assortedExpressions_withMatchingCharacteristics_returnsTrue() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // This verifies a variety of expressions that per the PRD and technical specification should + // pass for this classifier. + assertThat(matches).isTrue() + } + + @Test + @RunParameterized( + Iteration("y+4=3x^2!=y = 3x^2 - 4", "answer=y+4=3x^2", "input=y = 3x^2 - 4"), + Iteration("y-3x^2=-4!=y = 3x^2 - 4", "answer=y-3x^2=-4", "input=y = 3x^2 - 4"), + Iteration("-4=y-3x^2!=y = 3x^2 - 4", "answer=-4=y-3x^2", "input=y = 3x^2 - 4"), + Iteration("3x^2-y=4!=y = 3x^2 - 4", "answer=3x^2-y=4", "input=y = 3x^2 - 4"), + Iteration("3x^2=4+y!=y = 3x^2 - 4", "answer=3x^2=4+y", "input=y = 3x^2 - 4"), + Iteration("y-x^2=2x^2-4!=y = 3x^2 - 4", "answer=y-x^2=2x^2-4", "input=y = 3x^2 - 4"), + Iteration("y=x*(2^(1/2))!=y=sqrt(2)x", "answer=y=x*(2^(1/2))", "input=y=sqrt(2)x"), + Iteration("y − 3x^2 = -4!=y = 3x^2 - 4", "answer=y − 3x^2 = -4", "input=y = 3x^2 - 4"), + Iteration("3x^2 - 4 = y!=y = 3x^2 - 4", "answer=3x^2 - 4 = y", "input=y = 3x^2 - 4"), + Iteration("y = 3x^2 - 7!=y = 3x^2 - 4", "answer=y = 3x^2 - 7", "input=y = 3x^2 - 4"), + Iteration("x^2 = 3y - 4!=y = 3x^2 - 4", "answer=x^2 = 3y - 4", "input=y = 3x^2 - 4"), + Iteration("y/(3x^2 - 4) = 1!=y = 3x^2 - 4", "answer=y/(3x^2 - 4) = 1", "input=y = 3x^2 - 4"), + Iteration("y + 3x^2 - 4 = 0!=y = 3x^2 - 4", "answer=y + 3x^2 - 4 = 0", "input=y = 3x^2 - 4"), + Iteration("y − 3x^2 + 4 = 0!=y = 3x^2 - 4", "answer=y − 3x^2 + 4 = 0", "input=y = 3x^2 - 4"), + Iteration("y^2 = 3x^2y - 4y!=y = 3x^2 - 4", "answer=y^2 = 3x^2y - 4y", "input=y = 3x^2 - 4"), + Iteration("y = (3x^3 - 4x)/x!=y = 3x^2 - 4", "answer=y = (3x^3 - 4x)/x", "input=y = 3x^2 - 4"), + Iteration( + "y^2 * y^−1 = -12x^2!=y = 3x^2 - 4", "answer=y^2 * y^−1 = -12x^2", "input=y = 3x^2 - 4" + ), + Iteration("y^2/y = 3x^2 - 4!=y = 3x^2 - 4", "answer=y^2/y = 3x^2 - 4", "input=y = 3x^2 - 4"), + Iteration("2 − 3 = -4!=y = 3x^2 - 4", "answer=2 − 3 = -4", "input=y = 3x^2 - 4"), + Iteration("y = 3x^2 + 4!=y = 3x^2 - 4", "answer=y = 3x^2 + 4", "input=y = 3x^2 - 4"), + Iteration("y - 4 = 3x^2!=y = 3x^2 - 4", "answer=y - 4 = 3x^2", "input=y = 3x^2 - 4"), + Iteration("y − 3x^2 - 4 = 0!=y = 3x^2 - 4", "answer=y − 3x^2 - 4 = 0", "input=y = 3x^2 - 4"), + Iteration("0 = y + 3x^2 - 4!=y = 3x^2 - 4", "answer=0 = y + 3x^2 - 4", "input=y = 3x^2 - 4"), + Iteration("2 = 0!=y = 3x^2 - 4", "answer=2 = 0", "input=y = 3x^2 - 4"), + Iteration("y - x^2 = -4!=y = 3x^2 - 4", "answer=y - x^2 = -4", "input=y = 3x^2 - 4"), + Iteration("y=3x-4!=y = 3x^2 - 4", "answer=y=3x-4", "input=y = 3x^2 - 4"), + Iteration("y/sqrt(2)=x!=y=sqrt(2)x", "answer=y/sqrt(2)=x", "input=y=sqrt(2)x"), + Iteration("y/4=x!=y=4x", "answer=y/4=x", "input=y=4x"), + Iteration("y/4=16x!=y=4x", "answer=y/4=16x", "input=y=4x"), + Iteration("xy=x^2!=y=x", "answer=xy=x^2", "input=y=x") + ) + fun testMatches_assortedExpressions_withoutMatchingCharacteristics_returnsFalse() { + val answerExpression = createMathExpression(answer) + val inputExpression = createMathExpression(input) + + val matches = matchesClassifier(answerExpression, inputExpression) + + // This verifies a variety of expressions that per the PRD and technical specification should + // not pass for this classifier. + assertThat(matches).isFalse() + } + + private fun matchesClassifier( + answerExpression: InteractionObject, + inputExpression: InteractionObject, + allowedVariables: List = allPossibleVariables + ): Boolean { + return classifier.matches( + answerExpression, + inputs = mapOf("x" to inputExpression), + classificationContext = ClassificationContext( + customizationArgs = mapOf( + "customOskLetters" to SchemaObject.newBuilder().apply { + schemaObjectList = SchemaObjectList.newBuilder().apply { + addAllSchemaObject( + allowedVariables.map { + SchemaObject.newBuilder().setNormalizedString(it).build() + } + ) + }.build() + }.build() + ) + ) + ) + } + + private fun createMathExpression(rawExpression: String) = InteractionObject.newBuilder().apply { + mathExpression = rawExpression + }.build() + + private fun setUpTestApplicationComponent() { + DaggerMathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest_TestApplicationComponent + .builder() + .setApplication(ApplicationProvider.getApplicationContext()).build().inject(this) + } + + // TODO(#89): Move this to a common test application component. + @Module + class TestModule { + @Provides + @Singleton + fun provideContext(application: Application): Context { + return application + } + } + + // TODO(#89): Move this to a common test application component. + @Singleton + @Component( + modules = [ + TestModule::class, LocaleProdModule::class, FakeOppiaClockModule::class, + TestDispatcherModule::class, LoggerModule::class, RobolectricModule::class + ] + ) + interface TestApplicationComponent { + @Component.Builder + interface Builder { + @BindsInstance + fun setApplication(application: Application): Builder + + fun build(): TestApplicationComponent + } + + fun inject( + test: MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest + ) + } +} diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputModuleTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputModuleTest.kt new file mode 100644 index 00000000000..c3be8b53b1a --- /dev/null +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputModuleTest.kt @@ -0,0 +1,106 @@ +package org.oppia.android.domain.classify.rules.mathequationinput + +import android.app.Application +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import dagger.BindsInstance +import dagger.Component +import dagger.Module +import dagger.Provides +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.oppia.android.domain.classify.RuleClassifier +import org.oppia.android.testing.robolectric.RobolectricModule +import org.oppia.android.testing.threading.TestDispatcherModule +import org.oppia.android.testing.time.FakeOppiaClockModule +import org.oppia.android.util.locale.LocaleProdModule +import org.oppia.android.util.logging.LoggerModule +import org.robolectric.annotation.Config +import org.robolectric.annotation.LooperMode +import javax.inject.Inject +import javax.inject.Singleton +import org.oppia.android.domain.classify.rules.MathEquationInputRules + +/** Tests for [MathEquationInputModule]. */ +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") +@RunWith(AndroidJUnit4::class) +@LooperMode(LooperMode.Mode.PAUSED) +@Config(manifest = Config.NONE) +class MathEquationInputModuleTest { + @field:[Inject MathEquationInputRules] + lateinit var mathEquationInputClassifiers: Map< + String, @JvmSuppressWildcards RuleClassifier> + + @Before + fun setUp() { + setUpTestApplicationComponent() + } + + @Test + fun testModule_hasAtLeastOneClassifier() { + assertThat(mathEquationInputClassifiers).isNotEmpty() + } + + @Test + fun testModule_hasNoDuplicateClassifiers() { + assertThat(mathEquationInputClassifiers.values.toSet()).hasSize( + mathEquationInputClassifiers.size + ) + } + + @Test + fun testModule_providesMatchesExactlyWithClassifier() { + assertThat(mathEquationInputClassifiers).containsKey("MatchesExactlyWith") + } + + @Test + fun testModule_providesMatchesUpToTrivialManipulationsClassifier() { + assertThat(mathEquationInputClassifiers).containsKey("MatchesUpToTrivialManipulations") + } + + @Test + fun testModule_providesIsEquivalentToClassifier() { + assertThat(mathEquationInputClassifiers).containsKey("MatchesExactlyWith") + } + + private fun setUpTestApplicationComponent() { + DaggerMathEquationInputModuleTest_TestApplicationComponent + .builder() + .setApplication(ApplicationProvider.getApplicationContext()).build().inject(this) + } + + // TODO(#89): Move this to a common test application component. + @Module + class TestModule { + @Provides + @Singleton + fun provideContext(application: Application): Context { + return application + } + } + + // TODO(#89): Move this to a common test application component. + @Singleton + @Component( + modules = [ + TestModule::class, LocaleProdModule::class, FakeOppiaClockModule::class, + TestDispatcherModule::class, LoggerModule::class, RobolectricModule::class, + MathEquationInputModule::class + ] + ) + interface TestApplicationComponent { + @Component.Builder + interface Builder { + @BindsInstance + fun setApplication(application: Application): Builder + + fun build(): TestApplicationComponent + } + + fun inject(test: MathEquationInputModuleTest) + } +} diff --git a/testing/src/main/java/org/oppia/android/testing/junit/OppiaParameterizedTestRunner.kt b/testing/src/main/java/org/oppia/android/testing/junit/OppiaParameterizedTestRunner.kt index 6638a801db0..9b0c5fe9c5c 100644 --- a/testing/src/main/java/org/oppia/android/testing/junit/OppiaParameterizedTestRunner.kt +++ b/testing/src/main/java/org/oppia/android/testing/junit/OppiaParameterizedTestRunner.kt @@ -111,7 +111,9 @@ class OppiaParameterizedTestRunner(private val testClass: Class<*>) : Suite(test " $rawValuePair)" } - val (fieldName, rawValue) = rawValuePair.split('=') + // Use substringBefore/After since values should be allowed to contain '='. + val fieldName = rawValuePair.substringBefore(delimiter = '=') + val rawValue = rawValuePair.substringAfter(delimiter = '=') check(fieldName in fieldsAndParsers) { "Property key does not correspond to any class fields: $fieldName (available:" + " ${fieldsAndParsers.keys})" From 0c00467d00847add59e260539c1e2290cd5ea4bd Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Sat, 5 Feb 2022 00:41:47 -0800 Subject: [PATCH 117/162] Static check & lint fixes. --- ...putIsEquivalentToRuleClassifierProvider.kt | 20 ++++++++++++++----- ...atchesExactlyWithRuleClassifierProvider.kt | 13 +++++++++--- ...vialManipulationsRuleClassifierProvider.kt | 11 +++++++++- ...ManipulationsRuleClassifierProviderTest.kt | 6 ++++-- .../MathEquationInputModuleTest.kt | 2 +- .../file_content_validation_checks.textproto | 3 +++ 6 files changed, 43 insertions(+), 12 deletions(-) diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputIsEquivalentToRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputIsEquivalentToRuleClassifierProvider.kt index de26cf01fea..1bcbdcf21f6 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputIsEquivalentToRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputIsEquivalentToRuleClassifierProvider.kt @@ -9,13 +9,23 @@ import org.oppia.android.domain.classify.rules.RuleClassifierProvider import org.oppia.android.util.logging.ConsoleLogger import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult import org.oppia.android.util.math.MathExpressionParser.Companion.parseAlgebraicEquation -import org.oppia.android.util.math.toPolynomial -import javax.inject.Inject import org.oppia.android.util.math.isApproximatelyEqualTo import org.oppia.android.util.math.minus import org.oppia.android.util.math.sort +import org.oppia.android.util.math.toPolynomial import org.oppia.android.util.math.unaryMinus +import javax.inject.Inject +/** + * Provider for a classifier that determines whether a math equation expression is mathematically + * equivalent to the creator-specific equation defined as the input to this interaction. + * + * Note that both equations are assumed and parsed as polynomial equations. Furthermore, this + * classifier allows the two sides of the equations to be rearranged in any way on either side of + * the '=' sign (but they can't be multiples of each other). + * + * See this class's tests for a list of supported cases (both for matching and not matching). + */ class MathEquationInputIsEquivalentToRuleClassifierProvider @Inject constructor( private val classifierFactory: GenericRuleClassifier.Factory, private val consoleLogger: ConsoleLogger @@ -48,9 +58,9 @@ class MathEquationInputIsEquivalentToRuleClassifierProvider @Inject constructor( // arranged. Furthermore, the '-1' check is correct since the order of the equation can flip // depending on how it was inputted, and '-1 * 0=0' so the new right-hand side remains // unaffected. - return newAnswerLhs.isApproximatelyEqualTo(newInputLhs) - || negativeAnswerLhs.isApproximatelyEqualTo(newInputLhs) - || newAnswerLhs.isApproximatelyEqualTo(negativeInputLhs) + return newAnswerLhs.isApproximatelyEqualTo(newInputLhs) || + negativeAnswerLhs.isApproximatelyEqualTo(newInputLhs) || + newAnswerLhs.isApproximatelyEqualTo(negativeInputLhs) } private fun parsePolynomials( diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesExactlyWithRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesExactlyWithRuleClassifierProvider.kt index 8cbf7b08b96..6ea32f60e48 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesExactlyWithRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesExactlyWithRuleClassifierProvider.kt @@ -9,9 +9,16 @@ import org.oppia.android.domain.classify.rules.RuleClassifierProvider import org.oppia.android.util.logging.ConsoleLogger import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult import org.oppia.android.util.math.MathExpressionParser.Companion.parseAlgebraicEquation -import javax.inject.Inject import org.oppia.android.util.math.isApproximatelyEqualTo +import javax.inject.Inject +/** + * Provider for a classifier that determines whether a math equation is exactly equal to the + * creator-specific equation defined as the input to this interaction, including any parenthetical + * groups in the equations and operand order. + * + * See this class's tests for a list of supported cases (both for matching and not matching). + */ class MathEquationInputMatchesExactlyWithRuleClassifierProvider @Inject constructor( private val classifierFactory: GenericRuleClassifier.Factory, private val consoleLogger: ConsoleLogger @@ -62,8 +69,8 @@ class MathEquationInputMatchesExactlyWithRuleClassifierProvider @Inject construc } private fun MathEquation.approximatelyEquals(other: MathEquation): Boolean { - return leftSide.isApproximatelyEqualTo(other.leftSide) - && rightSide.isApproximatelyEqualTo(other.rightSide) + return leftSide.isApproximatelyEqualTo(other.leftSide) && + rightSide.isApproximatelyEqualTo(other.rightSide) } } } diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt b/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt index 207161b7c8d..db463880ab0 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt @@ -9,10 +9,19 @@ import org.oppia.android.domain.classify.rules.RuleClassifierProvider import org.oppia.android.util.logging.ConsoleLogger import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult import org.oppia.android.util.math.MathExpressionParser.Companion.parseAlgebraicEquation +import org.oppia.android.util.math.isApproximatelyEqualTo import org.oppia.android.util.math.toComparableOperation import javax.inject.Inject -import org.oppia.android.util.math.isApproximatelyEqualTo +/** + * Provider for a classifier that determines whether a math equation is equal to the + * creator-specific equation defined as the input to this interaction, with some manipulations. + * + * 'Trivial manipulations' indicates rearranging any operands for commutative operations, or changes + * in resolution order (i.e. associative) without changing the meaning of the equation. + * + * See this class's tests for a list of supported cases (both for matching and not matching). + */ class MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProvider @Inject constructor( private val classifierFactory: GenericRuleClassifier.Factory, diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt index 604ab230cb3..9af344ecb91 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt @@ -31,6 +31,8 @@ import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode import javax.inject.Inject import javax.inject.Singleton +import org.oppia.android.domain.classify.rules.mathequationinput.DaggerMathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest_TestApplicationComponent as DaggerTestApplicationComponent +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProvider as RuleClassifierProvider /** * Tests for [MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProvider]. @@ -46,7 +48,7 @@ import javax.inject.Singleton @LooperMode(LooperMode.Mode.PAUSED) @Config(manifest = Config.NONE) class MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest { - @Inject internal lateinit var provider: MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProvider + @Inject internal lateinit var provider: RuleClassifierProvider @Parameter lateinit var answer: String @Parameter lateinit var input: String @@ -373,7 +375,7 @@ class MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest }.build() private fun setUpTestApplicationComponent() { - DaggerMathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest_TestApplicationComponent + DaggerTestApplicationComponent .builder() .setApplication(ApplicationProvider.getApplicationContext()).build().inject(this) } diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputModuleTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputModuleTest.kt index c3be8b53b1a..4958d6d0fb4 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputModuleTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputModuleTest.kt @@ -13,6 +13,7 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.oppia.android.domain.classify.RuleClassifier +import org.oppia.android.domain.classify.rules.MathEquationInputRules import org.oppia.android.testing.robolectric.RobolectricModule import org.oppia.android.testing.threading.TestDispatcherModule import org.oppia.android.testing.time.FakeOppiaClockModule @@ -22,7 +23,6 @@ import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode import javax.inject.Inject import javax.inject.Singleton -import org.oppia.android.domain.classify.rules.MathEquationInputRules /** Tests for [MathEquationInputModule]. */ // FunctionName: test names are conventionally named with underscores. diff --git a/scripts/assets/file_content_validation_checks.textproto b/scripts/assets/file_content_validation_checks.textproto index 03c575af12c..70a2d35df7a 100644 --- a/scripts/assets/file_content_validation_checks.textproto +++ b/scripts/assets/file_content_validation_checks.textproto @@ -289,6 +289,9 @@ file_content_checks { exempted_file_name: "domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputIsEquivalentToRuleClassifierProviderTest.kt" exempted_file_name: "domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProviderTest.kt" exempted_file_name: "domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt" + exempted_file_name: "domain/src/test/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputIsEquivalentToRuleClassifierProviderTest.kt" + exempted_file_name: "domain/src/test/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesExactlyWithRuleClassifierProviderTest.kt" + exempted_file_name: "domain/src/test/java/org/oppia/android/domain/classify/rules/mathequationinput/MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt" exempted_file_name: "domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputIsEquivalentToRuleClassifierProviderTest.kt" exempted_file_name: "domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesExactlyWithRuleClassifierProviderTest.kt" exempted_file_name: "domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt" From d026506cf487086dbfb7a90e8b0d5e52b077ddc2 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Sat, 5 Feb 2022 02:00:53 -0800 Subject: [PATCH 118/162] Post-merge fixes. Verified CI checks & all unit tests are passing. --- .../AdministratorControlsFragmentTest.kt | 7 ++++++- .../android/app/mydownloads/MyDownloadsActivityTest.kt | 7 ++++++- .../app/settings/profile/ProfileResetPinFragmentTest.kt | 6 +++++- .../oppia/android/app/parser/FractionParsingUiErrorTest.kt | 7 ++++++- 4 files changed, 23 insertions(+), 4 deletions(-) diff --git a/app/src/sharedTest/java/org/oppia/android/app/administratorcontrols/AdministratorControlsFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/administratorcontrols/AdministratorControlsFragmentTest.kt index a80d9166392..b4cd0fe29fe 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/administratorcontrols/AdministratorControlsFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/administratorcontrols/AdministratorControlsFragmentTest.kt @@ -39,13 +39,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -174,7 +177,9 @@ class AdministratorControlsFragmentTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/mydownloads/MyDownloadsActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/mydownloads/MyDownloadsActivityTest.kt index 4ac6a451c69..e609ec731b7 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/mydownloads/MyDownloadsActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/mydownloads/MyDownloadsActivityTest.kt @@ -28,13 +28,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -130,7 +133,9 @@ class MyDownloadsActivityTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileResetPinFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileResetPinFragmentTest.kt index 4308d16cfd3..d54d1ee4c83 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileResetPinFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileResetPinFragmentTest.kt @@ -47,13 +47,16 @@ import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientati import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -1015,7 +1018,8 @@ class ProfileResetPinFragmentTest { ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class + PlatformParameterSingletonModule::class, NumericExpressionInputModule::class, + AlgebraicExpressionInputModule::class, MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { diff --git a/app/src/test/java/org/oppia/android/app/parser/FractionParsingUiErrorTest.kt b/app/src/test/java/org/oppia/android/app/parser/FractionParsingUiErrorTest.kt index 0342d7e3539..54f532fd57f 100644 --- a/app/src/test/java/org/oppia/android/app/parser/FractionParsingUiErrorTest.kt +++ b/app/src/test/java/org/oppia/android/app/parser/FractionParsingUiErrorTest.kt @@ -27,13 +27,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -241,7 +244,9 @@ class FractionParsingUiErrorTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { From c4d53ddcad0859ebf6e153cb6f583a241d57ee90 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Sat, 5 Feb 2022 02:36:06 -0800 Subject: [PATCH 119/162] Split up tests. Also, adds dedicated BUILD.bazel file for new test. --- .../android/app/utility/math/BUILD.bazel | 30 + .../MathExpressionAccessibilityUtilTest.kt | 838 ++++++++++++------ 2 files changed, 598 insertions(+), 270 deletions(-) create mode 100644 app/src/test/java/org/oppia/android/app/utility/math/BUILD.bazel diff --git a/app/src/test/java/org/oppia/android/app/utility/math/BUILD.bazel b/app/src/test/java/org/oppia/android/app/utility/math/BUILD.bazel new file mode 100644 index 00000000000..efbfba1af52 --- /dev/null +++ b/app/src/test/java/org/oppia/android/app/utility/math/BUILD.bazel @@ -0,0 +1,30 @@ +""" +Tests for UI-specific math utilities. +""" + +load("@dagger//:workspace_defs.bzl", "dagger_rules") +load("//:oppia_android_test.bzl", "oppia_android_test") + +oppia_android_test( + name = "MathExpressionAccessibilityUtilTest", + srcs = ["MathExpressionAccessibilityUtilTest.kt"], + custom_package = "org.oppia.android.app.utility.math", + test_class = "org.oppia.android.app.utility.math.MathExpressionAccessibilityUtilTest", + test_manifest = "//app:test_manifest", + deps = [ + ":dagger", + "//app/src/main/java/org/oppia/android/app/utility/math:math_expression_accessibility_util", + "//model/src/main/proto:languages_java_proto_lite", + "//model/src/main/proto:math_java_proto_lite", + "//testing/src/main/java/org/oppia/android/testing/math:math_equation_subject", + "//testing/src/main/java/org/oppia/android/testing/math:math_expression_subject", + "//third_party:androidx_test_ext_junit", + "//third_party:com_google_truth_truth", + "//third_party:junit_junit", + "//third_party:org_robolectric_robolectric", + "//third_party:robolectric_android-all", + "//utility/src/main/java/org/oppia/android/util/math:math_expression_parser", + ], +) + +dagger_rules() diff --git a/app/src/test/java/org/oppia/android/app/utility/math/MathExpressionAccessibilityUtilTest.kt b/app/src/test/java/org/oppia/android/app/utility/math/MathExpressionAccessibilityUtilTest.kt index 6ada50c67ff..c536a5b8c03 100644 --- a/app/src/test/java/org/oppia/android/app/utility/math/MathExpressionAccessibilityUtilTest.kt +++ b/app/src/test/java/org/oppia/android/app/utility/math/MathExpressionAccessibilityUtilTest.kt @@ -1,7 +1,6 @@ package org.oppia.android.app.utility.math import android.app.Application -import androidx.appcompat.app.AppCompatActivity import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.StringSubject @@ -9,20 +8,11 @@ import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage import dagger.BindsInstance import dagger.Component -import dagger.Module -import dagger.Provides +import javax.inject.Inject +import javax.inject.Singleton import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.activity.ActivityComponent -import org.oppia.android.app.activity.ActivityComponentFactory -import org.oppia.android.app.application.ApplicationComponent -import org.oppia.android.app.application.ApplicationInjector -import org.oppia.android.app.application.ApplicationInjectorProvider -import org.oppia.android.app.application.ApplicationModule -import org.oppia.android.app.application.ApplicationStartupListenerModule -import org.oppia.android.app.devoptions.DeveloperOptionsModule -import org.oppia.android.app.devoptions.DeveloperOptionsStarterModule import org.oppia.android.app.model.MathEquation import org.oppia.android.app.model.MathExpression import org.oppia.android.app.model.OppiaLanguage @@ -34,70 +24,25 @@ import org.oppia.android.app.model.OppiaLanguage.HINGLISH import org.oppia.android.app.model.OppiaLanguage.LANGUAGE_UNSPECIFIED import org.oppia.android.app.model.OppiaLanguage.PORTUGUESE import org.oppia.android.app.model.OppiaLanguage.UNRECOGNIZED -import org.oppia.android.app.topic.PracticeTabModule -import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule -import org.oppia.android.data.backends.gae.NetworkConfigProdModule -import org.oppia.android.data.backends.gae.NetworkModule -import org.oppia.android.domain.classify.InteractionsModule -import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule -import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule -import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule -import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule -import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule -import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule -import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule -import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule -import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule -import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule -import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule -import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule -import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule -import org.oppia.android.domain.exploration.lightweightcheckpointing.ExplorationStorageModule -import org.oppia.android.domain.hintsandsolution.HintsAndSolutionConfigModule -import org.oppia.android.domain.hintsandsolution.HintsAndSolutionProdModule -import org.oppia.android.domain.onboarding.ExpirationMetaDataRetrieverModule -import org.oppia.android.domain.oppialogger.LogStorageModule -import org.oppia.android.domain.platformparameter.PlatformParameterModule -import org.oppia.android.domain.platformparameter.PlatformParameterSingletonModule -import org.oppia.android.domain.question.QuestionModule -import org.oppia.android.domain.topic.PrimeTopicAssetsControllerModule -import org.oppia.android.domain.workmanager.WorkManagerConfigurationModule -import org.oppia.android.testing.TestLogReportingModule import org.oppia.android.testing.math.MathEquationSubject import org.oppia.android.testing.math.MathEquationSubject.Companion.assertThat import org.oppia.android.testing.math.MathExpressionSubject import org.oppia.android.testing.math.MathExpressionSubject.Companion.assertThat -import org.oppia.android.testing.robolectric.RobolectricModule -import org.oppia.android.testing.threading.TestDispatcherModule -import org.oppia.android.testing.time.FakeOppiaClockModule -import org.oppia.android.util.accessibility.AccessibilityTestModule -import org.oppia.android.util.caching.AssetModule -import org.oppia.android.util.caching.testing.CachingTestModule -import org.oppia.android.util.gcsresource.GcsResourceModule -import org.oppia.android.util.locale.LocaleProdModule -import org.oppia.android.util.logging.EnableConsoleLog -import org.oppia.android.util.logging.EnableFileLog -import org.oppia.android.util.logging.GlobalLogLevel -import org.oppia.android.util.logging.LogLevel import org.oppia.android.util.math.MathExpressionParser import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult -import org.oppia.android.util.networking.NetworkConnectionDebugUtilModule -import org.oppia.android.util.networking.NetworkConnectionUtilDebugModule -import org.oppia.android.util.parser.html.HtmlParserEntityTypeModule -import org.oppia.android.util.parser.image.GlideImageLoaderModule -import org.oppia.android.util.parser.image.ImageParsingModule import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode -import javax.inject.Inject -import javax.inject.Singleton /** Tests for [MathExpressionAccessibilityUtil]. */ @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) @Config(application = MathExpressionAccessibilityUtilTest.TestApplication::class) class MathExpressionAccessibilityUtilTest { - @Inject lateinit var util: MathExpressionAccessibilityUtil + @Inject + lateinit var util: MathExpressionAccessibilityUtil + + // TODO: finish tests @Before fun setUp() { @@ -105,113 +50,274 @@ class MathExpressionAccessibilityUtilTest { } @Test - fun testHumanReadableString() { - // TODO: split up & move to separate test suites. Finish test cases (if anymore are needed). - - val exp1 = parseNumericExpressionSuccessfullyWithAllErrors("1") - assertThat(exp1).forHumanReadable(ARABIC).doesNotConvertToString() + fun test1() { + // TODO: do something with this test. + val exp = parseNumericExpressionSuccessfullyWithAllErrors("1") + assertThat(exp).forHumanReadable(ARABIC).doesNotConvertToString() + } - assertThat(exp1).forHumanReadable(HINDI).doesNotConvertToString() + @Test + fun test2() { + // TODO: do something with this test. + val exp = parseNumericExpressionSuccessfullyWithAllErrors("1") + assertThat(exp).forHumanReadable(HINDI).doesNotConvertToString() + } - assertThat(exp1).forHumanReadable(HINGLISH).doesNotConvertToString() + @Test + fun test3() { + // TODO: do something with this test. + val exp = parseNumericExpressionSuccessfullyWithAllErrors("1") + assertThat(exp).forHumanReadable(HINGLISH).doesNotConvertToString() + } - assertThat(exp1).forHumanReadable(PORTUGUESE).doesNotConvertToString() + @Test + fun test4() { + // TODO: do something with this test. + val exp = parseNumericExpressionSuccessfullyWithAllErrors("1") + assertThat(exp).forHumanReadable(PORTUGUESE).doesNotConvertToString() + } - assertThat(exp1).forHumanReadable(BRAZILIAN_PORTUGUESE).doesNotConvertToString() + @Test + fun test5() { + // TODO: do something with this test. + val exp = parseNumericExpressionSuccessfullyWithAllErrors("1") + assertThat(exp).forHumanReadable(BRAZILIAN_PORTUGUESE).doesNotConvertToString() + } - assertThat(exp1).forHumanReadable(LANGUAGE_UNSPECIFIED).doesNotConvertToString() + @Test + fun test6() { + // TODO: do something with this test. + val exp = parseNumericExpressionSuccessfullyWithAllErrors("1") + assertThat(exp).forHumanReadable(LANGUAGE_UNSPECIFIED).doesNotConvertToString() + } - assertThat(exp1).forHumanReadable(UNRECOGNIZED).doesNotConvertToString() + @Test + fun test7() { + // TODO: do something with this test. + val exp = parseNumericExpressionSuccessfullyWithAllErrors("1") + assertThat(exp).forHumanReadable(UNRECOGNIZED).doesNotConvertToString() + } - val exp2 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x") - assertThat(exp2).forHumanReadable(ARABIC).doesNotConvertToString() + @Test + fun test8() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("x") + assertThat(exp).forHumanReadable(ARABIC).doesNotConvertToString() + } - assertThat(exp2).forHumanReadable(HINDI).doesNotConvertToString() + @Test + fun test9() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("x") + assertThat(exp).forHumanReadable(HINDI).doesNotConvertToString() + } - assertThat(exp2).forHumanReadable(HINGLISH).doesNotConvertToString() + @Test + fun test10() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("x") + assertThat(exp).forHumanReadable(HINGLISH).doesNotConvertToString() + } - assertThat(exp2).forHumanReadable(PORTUGUESE).doesNotConvertToString() + @Test + fun test11() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("x") + assertThat(exp).forHumanReadable(PORTUGUESE).doesNotConvertToString() + } - assertThat(exp2).forHumanReadable(BRAZILIAN_PORTUGUESE).doesNotConvertToString() + @Test + fun test12() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("x") + assertThat(exp).forHumanReadable(BRAZILIAN_PORTUGUESE).doesNotConvertToString() + } - assertThat(exp2).forHumanReadable(LANGUAGE_UNSPECIFIED).doesNotConvertToString() + @Test + fun test13() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("x") + assertThat(exp).forHumanReadable(LANGUAGE_UNSPECIFIED).doesNotConvertToString() + } - assertThat(exp2).forHumanReadable(UNRECOGNIZED).doesNotConvertToString() + @Test + fun test14() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("x") + assertThat(exp).forHumanReadable(UNRECOGNIZED).doesNotConvertToString() + } - val eq1 = parseAlgebraicEquationSuccessfullyWithAllErrors("x=1") - assertThat(eq1).forHumanReadable(ARABIC).doesNotConvertToString() + @Test + fun test15() { + // TODO: do something with this test. + val eq = parseAlgebraicEquationSuccessfullyWithAllErrors("x=1") + assertThat(eq).forHumanReadable(ARABIC).doesNotConvertToString() + } - assertThat(eq1).forHumanReadable(HINDI).doesNotConvertToString() + @Test + fun test16() { + // TODO: do something with this test. + val eq = parseAlgebraicEquationSuccessfullyWithAllErrors("x=1") + assertThat(eq).forHumanReadable(HINDI).doesNotConvertToString() + } - assertThat(eq1).forHumanReadable(HINGLISH).doesNotConvertToString() + @Test + fun test17() { + // TODO: do something with this test. + val eq = parseAlgebraicEquationSuccessfullyWithAllErrors("x=1") + assertThat(eq).forHumanReadable(HINGLISH).doesNotConvertToString() + } - assertThat(eq1).forHumanReadable(PORTUGUESE).doesNotConvertToString() + @Test + fun test18() { + // TODO: do something with this test. + val eq = parseAlgebraicEquationSuccessfullyWithAllErrors("x=1") + assertThat(eq).forHumanReadable(PORTUGUESE).doesNotConvertToString() + } - assertThat(eq1).forHumanReadable(BRAZILIAN_PORTUGUESE).doesNotConvertToString() + @Test + fun test19() { + // TODO: do something with this test. + val eq = parseAlgebraicEquationSuccessfullyWithAllErrors("x=1") + assertThat(eq).forHumanReadable(BRAZILIAN_PORTUGUESE).doesNotConvertToString() + } - assertThat(eq1).forHumanReadable(LANGUAGE_UNSPECIFIED).doesNotConvertToString() + @Test + fun test20() { + // TODO: do something with this test. + val eq = parseAlgebraicEquationSuccessfullyWithAllErrors("x=1") + assertThat(eq).forHumanReadable(LANGUAGE_UNSPECIFIED).doesNotConvertToString() + } - assertThat(eq1).forHumanReadable(UNRECOGNIZED).doesNotConvertToString() + @Test + fun test21() { + // TODO: do something with this test. + val eq = parseAlgebraicEquationSuccessfullyWithAllErrors("x=1") + assertThat(eq).forHumanReadable(UNRECOGNIZED).doesNotConvertToString() + } + @Test + fun test22() { + // TODO: do something with this test. // specific cases (from rules & other cases): - val exp3 = parseNumericExpressionSuccessfullyWithAllErrors("1") - assertThat(exp3).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1") - assertThat(exp3).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1") + val exp = parseNumericExpressionSuccessfullyWithAllErrors("1") + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1") + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1") + } - val exp49 = parseNumericExpressionSuccessfullyWithAllErrors("-1") - assertThat(exp49).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("negative 1") + @Test + fun test23() { + // TODO: do something with this test. + val exp = parseNumericExpressionSuccessfullyWithAllErrors("-1") + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("negative 1") + } - val exp50 = parseNumericExpressionSuccessfullyWithAllErrors("+1") - assertThat(exp50).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("positive 1") + @Test + fun test24() { + // TODO: do something with this test. + val exp = parseNumericExpressionSuccessfullyWithoutOptionalErrors("+1") + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("positive 1") + } - val exp4 = parseNumericExpressionSuccessfullyWithoutOptionalErrors("((1))") - assertThat(exp4).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1") + @Test + fun test25() { + // TODO: do something with this test. + val exp = parseNumericExpressionSuccessfullyWithoutOptionalErrors("((1))") + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1") + } - val exp5 = parseNumericExpressionSuccessfullyWithAllErrors("1+2") - assertThat(exp5).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 plus 2") + @Test + fun test26() { + // TODO: do something with this test. + val exp = parseNumericExpressionSuccessfullyWithAllErrors("1+2") + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 plus 2") + } - val exp6 = parseNumericExpressionSuccessfullyWithAllErrors("1-2") - assertThat(exp6).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 minus 2") + @Test + fun test27() { + // TODO: do something with this test. + val exp = parseNumericExpressionSuccessfullyWithAllErrors("1-2") + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 minus 2") + } - val exp7 = parseNumericExpressionSuccessfullyWithAllErrors("1*2") - assertThat(exp7).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 times 2") + @Test + fun test28() { + // TODO: do something with this test. + val exp = parseNumericExpressionSuccessfullyWithAllErrors("1*2") + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 times 2") + } - val exp8 = parseNumericExpressionSuccessfullyWithAllErrors("1/2") - assertThat(exp8).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 divided by 2") + @Test + fun test29() { + // TODO: do something with this test. + val exp = parseNumericExpressionSuccessfullyWithAllErrors("1/2") + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 divided by 2") + } - val exp9 = parseNumericExpressionSuccessfullyWithAllErrors("1+(1-2)") - assertThat(exp9) + @Test + fun test30() { + // TODO: do something with this test. + val exp = parseNumericExpressionSuccessfullyWithAllErrors("1+(1-2)") + assertThat(exp) .forHumanReadable(ENGLISH) .convertsToStringThat() .isEqualTo("1 plus open parenthesis 1 minus 2 close parenthesis") + } - val exp10 = parseNumericExpressionSuccessfullyWithAllErrors("2^3") - assertThat(exp10) + @Test + fun test31() { + // TODO: do something with this test. + val exp = parseNumericExpressionSuccessfullyWithAllErrors("2^3") + assertThat(exp) .forHumanReadable(ENGLISH) .convertsToStringThat() .isEqualTo("2 raised to the power of 3") + } - val exp11 = parseNumericExpressionSuccessfullyWithAllErrors("2^(1+2)") - assertThat(exp11) + @Test + fun test32() { + // TODO: do something with this test. + val exp = parseNumericExpressionSuccessfullyWithAllErrors("2^(1+2)") + assertThat(exp) .forHumanReadable(ENGLISH) .convertsToStringThat() .isEqualTo("2 raised to the power of open parenthesis 1 plus 2 close parenthesis") + } - val exp12 = parseNumericExpressionSuccessfullyWithAllErrors("100000*2") - assertThat(exp12).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("100,000 times 2") + @Test + fun test33() { + // TODO: do something with this test. + val exp = parseNumericExpressionSuccessfullyWithAllErrors("100000*2") + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("100,000 times 2") + } - val exp13 = parseNumericExpressionSuccessfullyWithAllErrors("sqrt(2)") - assertThat(exp13).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("square root of 2") + @Test + fun test34() { + // TODO: do something with this test. + val exp = parseNumericExpressionSuccessfullyWithAllErrors("sqrt(2)") + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("square root of 2") + } - val exp14 = parseNumericExpressionSuccessfullyWithAllErrors("√2") - assertThat(exp14).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("square root of 2") + @Test + fun test35() { + // TODO: do something with this test. + val exp = parseNumericExpressionSuccessfullyWithAllErrors("√2") + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("square root of 2") + } - val exp15 = parseNumericExpressionSuccessfullyWithAllErrors("sqrt(1+2)") - assertThat(exp15) + @Test + fun test36() { + // TODO: do something with this test. + val exp = parseNumericExpressionSuccessfullyWithAllErrors("sqrt(1+2)") + assertThat(exp) .forHumanReadable(ENGLISH) .convertsToStringThat() .isEqualTo("start square root 1 plus 2 end square root") + } + @Test + fun test37() { + // TODO: do something with this test. val singularOrdinalNames = mapOf( 1 to "oneth", 2 to "half", @@ -238,296 +344,528 @@ class MathExpressionAccessibilityUtilTest { ) for (denominatorToCheck in 1..10) { for (numeratorToCheck in 0..denominatorToCheck) { - val exp16 = + val exp = parseNumericExpressionSuccessfullyWithAllErrors("$numeratorToCheck/$denominatorToCheck") val ordinalName = if (numeratorToCheck == 1) { singularOrdinalNames.getValue(denominatorToCheck) } else pluralOrdinalNames.getValue(denominatorToCheck) - assertThat(exp16) + assertThat(exp) .forHumanReadable(ENGLISH) .convertsWithFractionsToStringThat() .isEqualTo("$numeratorToCheck $ordinalName") } } + } - val exp17 = parseNumericExpressionSuccessfullyWithAllErrors("-1/3") - assertThat(exp17) + @Test + fun test38() { + // TODO: do something with this test. + val exp = parseNumericExpressionSuccessfullyWithAllErrors("-1/3") + assertThat(exp) .forHumanReadable(ENGLISH) .convertsWithFractionsToStringThat() .isEqualTo("negative 1 third") + } - val exp18 = parseNumericExpressionSuccessfullyWithAllErrors("-2/3") - assertThat(exp18) + @Test + fun test39() { + // TODO: do something with this test. + val exp = parseNumericExpressionSuccessfullyWithAllErrors("-2/3") + assertThat(exp) .forHumanReadable(ENGLISH) .convertsWithFractionsToStringThat() .isEqualTo("negative 2 thirds") + } - val exp19 = parseNumericExpressionSuccessfullyWithAllErrors("10/11") - assertThat(exp19) + @Test + fun test40() { + // TODO: do something with this test. + val exp = parseNumericExpressionSuccessfullyWithAllErrors("10/11") + assertThat(exp) .forHumanReadable(ENGLISH) .convertsWithFractionsToStringThat() .isEqualTo("10 over 11") + } - val exp20 = parseNumericExpressionSuccessfullyWithAllErrors("121/7986") - assertThat(exp20) + @Test + fun test41() { + // TODO: do something with this test. + val exp = parseNumericExpressionSuccessfullyWithAllErrors("121/7986") + assertThat(exp) .forHumanReadable(ENGLISH) .convertsWithFractionsToStringThat() .isEqualTo("121 over 7,986") + } - val exp21 = parseNumericExpressionSuccessfullyWithAllErrors("8/7") - assertThat(exp21) + @Test + fun test42() { + // TODO: do something with this test. + val exp = parseNumericExpressionSuccessfullyWithAllErrors("8/7") + assertThat(exp) .forHumanReadable(ENGLISH) - .convertsWithFractionsToStringThat() + .convertsToStringThat() .isEqualTo("8 over 7") + } - val exp22 = parseNumericExpressionSuccessfullyWithAllErrors("-10/-30") - assertThat(exp22) + @Test + fun test43() { + // TODO: do something with this test. + val exp = parseNumericExpressionSuccessfullyWithAllErrors("-10/-30") + assertThat(exp) .forHumanReadable(ENGLISH) .convertsWithFractionsToStringThat() .isEqualTo("negative the fraction with numerator 10 and denominator negative 30") + } - val exp23 = parseAlgebraicExpressionSuccessfullyWithAllErrors("1") - assertThat(exp23).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1") + @Test + fun test44() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("1") + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1") + } - val exp24 = parseAlgebraicExpressionSuccessfullyWithoutOptionalErrors("((1))") - assertThat(exp24).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1") + @Test + fun test45() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithoutOptionalErrors("((1))") + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1") + } - val exp25 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x") - assertThat(exp25).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("x") + @Test + fun test46() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("x") + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("x") + } - val exp26 = parseAlgebraicExpressionSuccessfullyWithoutOptionalErrors("((x))") - assertThat(exp26).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("x") + @Test + fun test47() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithoutOptionalErrors("((x))") + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("x") + } - val exp51 = parseAlgebraicExpressionSuccessfullyWithAllErrors("-x") - assertThat(exp51).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("negative x") + @Test + fun test48() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("-x") + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("negative x") + } - val exp52 = parseAlgebraicExpressionSuccessfullyWithAllErrors("+x") - assertThat(exp52).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("positive x") + @Test + fun test49() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("+x") + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("positive x") + } - val exp27 = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+x") - assertThat(exp27).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 plus x") + @Test + fun test50() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+x") + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 plus x") + } - val exp28 = parseAlgebraicExpressionSuccessfullyWithAllErrors("1-x") - assertThat(exp28).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 minus x") + @Test + fun test51() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("1-x") + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 minus x") + } - val exp29 = parseAlgebraicExpressionSuccessfullyWithAllErrors("1*x") - assertThat(exp29).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 times x") + @Test + fun test52() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("1*x") + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 times x") + } - val exp30 = parseAlgebraicExpressionSuccessfullyWithAllErrors("1/x") - assertThat(exp30).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 divided by x") + @Test + fun test53() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("1/x") + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 divided by x") + } - val exp31 = parseAlgebraicExpressionSuccessfullyWithAllErrors("1/x") - assertThat(exp31) + @Test + fun test54() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("1/x") + assertThat(exp) .forHumanReadable(ENGLISH) .convertsWithFractionsToStringThat() .isEqualTo("the fraction with numerator 1 and denominator x") + } - val exp32 = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+(1-x)") - assertThat(exp32) + @Test + fun test55() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+(1-x)") + assertThat(exp) .forHumanReadable(ENGLISH) .convertsToStringThat() .isEqualTo("1 plus open parenthesis 1 minus x close parenthesis") + } - val exp33 = parseAlgebraicExpressionSuccessfullyWithAllErrors("2x") - assertThat(exp33).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("2 x") + @Test + fun test56() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("2x") + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("2 x") + } - val exp34 = parseAlgebraicExpressionSuccessfullyWithAllErrors("xy") - assertThat(exp34).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("x times y") + @Test + fun test57() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("xy") + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("x times y") + } - val exp35 = parseAlgebraicExpressionSuccessfullyWithAllErrors("z") - assertThat(exp35).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("zed") + @Test + fun test58() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("z") + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("zed") + } - val exp36 = parseAlgebraicExpressionSuccessfullyWithAllErrors("2xz") - assertThat(exp36).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("2 x times zed") + @Test + fun test59() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("2xz") + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("2 x times zed") + } - val exp37 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x^2") - assertThat(exp37) + @Test + fun test60() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("x^2") + assertThat(exp) .forHumanReadable(ENGLISH) .convertsToStringThat() .isEqualTo("x raised to the power of 2") + } - val exp38 = parseAlgebraicExpressionSuccessfullyWithoutOptionalErrors("x^(1+x)") - assertThat(exp38) + @Test + fun test61() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithoutOptionalErrors("x^(1+x)") + assertThat(exp) .forHumanReadable(ENGLISH) .convertsToStringThat() .isEqualTo("x raised to the power of open parenthesis 1 plus x close parenthesis") + } - val exp39 = parseAlgebraicExpressionSuccessfullyWithAllErrors("100000*2") - assertThat(exp39).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("100,000 times 2") + @Test + fun test62() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("100000*2") + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("100,000 times 2") + } - val exp40 = parseAlgebraicExpressionSuccessfullyWithAllErrors("sqrt(2)") - assertThat(exp40).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("square root of 2") + @Test + fun test63() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("sqrt(2)") + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("square root of 2") + } - val exp41 = parseAlgebraicExpressionSuccessfullyWithAllErrors("sqrt(x)") - assertThat(exp41).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("square root of x") + @Test + fun test64() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("sqrt(x)") + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("square root of x") + } - val exp42 = parseAlgebraicExpressionSuccessfullyWithAllErrors("√2") - assertThat(exp42).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("square root of 2") + @Test + fun test65() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("√2") + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("square root of 2") + } - val exp43 = parseAlgebraicExpressionSuccessfullyWithAllErrors("√x") - assertThat(exp43).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("square root of x") + @Test + fun test66() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("√x") + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("square root of x") + } - val exp44 = parseAlgebraicExpressionSuccessfullyWithAllErrors("sqrt(1+2)") - assertThat(exp44) + @Test + fun test67() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("sqrt(1+2)") + assertThat(exp) .forHumanReadable(ENGLISH) .convertsToStringThat() .isEqualTo("start square root 1 plus 2 end square root") + } - val exp45 = parseAlgebraicExpressionSuccessfullyWithAllErrors("sqrt(1+x)") - assertThat(exp45) + @Test + fun test68() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("sqrt(1+x)") + assertThat(exp) .forHumanReadable(ENGLISH) .convertsToStringThat() .isEqualTo("start square root 1 plus x end square root") + } - val exp46 = parseAlgebraicExpressionSuccessfullyWithAllErrors("√(1+x)") - assertThat(exp46) + @Test + fun test69() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("√(1+x)") + assertThat(exp) .forHumanReadable(ENGLISH) .convertsToStringThat() .isEqualTo("start square root open parenthesis 1 plus x close parenthesis end square root") + } + @Test + fun test70() { + // TODO: do something with this test. + val singularOrdinalNames = mapOf( + 1 to "oneth", + 2 to "half", + 3 to "third", + 4 to "fourth", + 5 to "fifth", + 6 to "sixth", + 7 to "seventh", + 8 to "eighth", + 9 to "ninth", + 10 to "tenth", + ) + val pluralOrdinalNames = mapOf( + 1 to "oneths", + 2 to "halves", + 3 to "thirds", + 4 to "fourths", + 5 to "fifths", + 6 to "sixths", + 7 to "sevenths", + 8 to "eighths", + 9 to "ninths", + 10 to "tenths", + ) for (denominatorToCheck in 1..10) { for (numeratorToCheck in 0..denominatorToCheck) { - val exp16 = + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("$numeratorToCheck/$denominatorToCheck") val ordinalName = if (numeratorToCheck == 1) { singularOrdinalNames.getValue(denominatorToCheck) } else pluralOrdinalNames.getValue(denominatorToCheck) - assertThat(exp16) + assertThat(exp) .forHumanReadable(ENGLISH) .convertsWithFractionsToStringThat() .isEqualTo("$numeratorToCheck $ordinalName") } } + } - val exp47 = parseAlgebraicExpressionSuccessfullyWithAllErrors("1") - assertThat(exp47).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1") + @Test + fun test71() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("1") + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1") + } - val exp48 = parseAlgebraicExpressionSuccessfullyWithAllErrors("x(5-y)") - assertThat(exp48) + @Test + fun test72() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("x(5-y)") + assertThat(exp) .forHumanReadable(ENGLISH) .convertsToStringThat() .isEqualTo("x times open parenthesis 5 minus y close parenthesis") + } - val eq2 = parseAlgebraicEquationSuccessfullyWithAllErrors("x=1/y") - assertThat(eq2) + @Test + fun test73() { + // TODO: do something with this test. + val eq = parseAlgebraicEquationSuccessfullyWithAllErrors("x=1/y") + assertThat(eq) .forHumanReadable(ENGLISH) .convertsToStringThat() .isEqualTo("x equals 1 divided by y") + } - val eq3 = parseAlgebraicEquationSuccessfullyWithAllErrors("x=1/2") - assertThat(eq3) + @Test + fun test74() { + // TODO: do something with this test. + val eq = parseAlgebraicEquationSuccessfullyWithAllErrors("x=1/2") + assertThat(eq) .forHumanReadable(ENGLISH) .convertsToStringThat() .isEqualTo("x equals 1 divided by 2") + } - val eq4 = parseAlgebraicEquationSuccessfullyWithAllErrors("x=1/y") - assertThat(eq4) + @Test + fun test75() { + // TODO: do something with this test. + val eq = parseAlgebraicEquationSuccessfullyWithAllErrors("x=1/y") + assertThat(eq) .forHumanReadable(ENGLISH) .convertsWithFractionsToStringThat() .isEqualTo("x equals the fraction with numerator 1 and denominator y") + } - val eq5 = parseAlgebraicEquationSuccessfullyWithAllErrors("x=1/2") - assertThat(eq5) + @Test + fun test76() { + // TODO: do something with this test. + val eq = parseAlgebraicEquationSuccessfullyWithAllErrors("x=1/2") + assertThat(eq) .forHumanReadable(ENGLISH) .convertsWithFractionsToStringThat() .isEqualTo("x equals 1 half") + } + @Test + fun test77() { + // TODO: do something with this test. // Tests from examples in the PRD - val eq6 = parseAlgebraicEquationSuccessfullyWithAllErrors("3x^2+4y=62") - assertThat(eq6) + val eq = parseAlgebraicEquationSuccessfullyWithAllErrors("3x^2+4y=62") + assertThat(eq) .forHumanReadable(ENGLISH) .convertsToStringThat() .isEqualTo("3 x raised to the power of 2 plus 4 y equals 62") + } - val exp53 = parseAlgebraicExpressionSuccessfullyWithAllErrors("(x+6)/(x-4)") - assertThat(exp53) + @Test + fun test78() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("(x+6)/(x-4)") + assertThat(exp) .forHumanReadable(ENGLISH) .convertsWithFractionsToStringThat() .isEqualTo( "the fraction with numerator open parenthesis x plus 6 close parenthesis and denominator" + " open parenthesis x minus 4 close parenthesis" ) + } - val exp54 = parseAlgebraicExpressionSuccessfullyWithoutOptionalErrors("4*(x)^(2)+20x") - assertThat(exp54) + @Test + fun test79() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithoutOptionalErrors("4*(x)^(2)+20x") + assertThat(exp) .forHumanReadable(ENGLISH) .convertsToStringThat() .isEqualTo("4 times x raised to the power of 2 plus 20 x") + } - val exp55 = parseAlgebraicExpressionSuccessfullyWithAllErrors("3+x-5") - assertThat(exp55).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("3 plus x minus 5") + @Test + fun test80() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("3+x-5") + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("3 plus x minus 5") + } - val exp56 = + @Test + fun test81() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors( "Z+A-Z", allowedVariables = listOf("A", "Z") ) - assertThat(exp56).forHumanReadable(ENGLISH) + assertThat(exp).forHumanReadable(ENGLISH) .convertsToStringThat() .isEqualTo("Zed plus A minus Zed") + } - val exp57 = + @Test + fun test82() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors( "6C-5A-1", allowedVariables = listOf("A", "C") ) - assertThat(exp57) + assertThat(exp) .forHumanReadable(ENGLISH) .convertsToStringThat() .isEqualTo("6 C minus 5 A minus 1") + } - val exp58 = + @Test + fun test83() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors( "5*Z-w", allowedVariables = listOf("Z", "w") ) - assertThat(exp58) + assertThat(exp) .forHumanReadable(ENGLISH) .convertsToStringThat() .isEqualTo("5 times Zed minus w") + } - val exp59 = + @Test + fun test84() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors( "L*S-3S+L", allowedVariables = listOf("L", "S") ) - assertThat(exp59) + assertThat(exp) .forHumanReadable(ENGLISH) .convertsToStringThat() .isEqualTo("L times S minus 3 S plus L") + } - val exp60 = parseAlgebraicExpressionSuccessfullyWithAllErrors("2*(2+6+3+4)") - assertThat(exp60) + @Test + fun test85() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("2*(2+6+3+4)") + assertThat(exp) .forHumanReadable(ENGLISH) .convertsToStringThat() .isEqualTo("2 times open parenthesis 2 plus 6 plus 3 plus 4 close parenthesis") + } - val exp61 = parseAlgebraicExpressionSuccessfullyWithAllErrors("sqrt(64)") - assertThat(exp61) + @Test + fun test86() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("sqrt(64)") + assertThat(exp) .forHumanReadable(ENGLISH) .convertsToStringThat() .isEqualTo("square root of 64") + } - val exp62 = + @Test + fun test87() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors( "√(a+b)", allowedVariables = listOf("a", "b") ) - assertThat(exp62) + assertThat(exp) .forHumanReadable(ENGLISH) .convertsToStringThat() .isEqualTo("start square root open parenthesis a plus b close parenthesis end square root") + } - val exp63 = parseAlgebraicExpressionSuccessfullyWithAllErrors("3*10^-5") - assertThat(exp63) + @Test + fun test88() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("3*10^-5") + assertThat(exp) .forHumanReadable(ENGLISH) .convertsToStringThat() .isEqualTo("3 times 10 raised to the power of negative 5") + } - val exp64 = + @Test + fun test89() { + // TODO: do something with this test. + val exp = parseAlgebraicExpressionSuccessfullyWithoutOptionalErrors( "((x+2y)+5*(a-2b)+z)", allowedVariables = listOf("x", "y", "a", "b", "z") ) - assertThat(exp64) + assertThat(exp) .forHumanReadable(ENGLISH) .convertsToStringThat() .isEqualTo( @@ -582,49 +920,13 @@ class MathExpressionAccessibilityUtilTest { } } - // TODO(#89): Move this to a common test application component. - @Module - class TestModule { - // TODO(#59): Either isolate these to their own shared test module, or use the real logging - // module in tests to avoid needing to specify these settings for tests. - @EnableConsoleLog - @Provides - fun provideEnableConsoleLog(): Boolean = true - - @EnableFileLog - @Provides - fun provideEnableFileLog(): Boolean = false - - @GlobalLogLevel - @Provides - fun provideGlobalLogLevel(): LogLevel = LogLevel.VERBOSE - } - // TODO(#89): Move this to a common test application component. @Singleton @Component( modules = [ - TestModule::class, RobolectricModule::class, FakeOppiaClockModule::class, - TestLogReportingModule::class, TestDispatcherModule::class, ApplicationModule::class, - ApplicationStartupListenerModule::class, WorkManagerConfigurationModule::class, - ImageParsingModule::class, AccessibilityTestModule::class, PracticeTabModule::class, - GcsResourceModule::class, NetworkConnectionUtilDebugModule::class, LogStorageModule::class, - NetworkModule::class, PlatformParameterModule::class, HintsAndSolutionProdModule::class, - CachingTestModule::class, InteractionsModule::class, ExplorationStorageModule::class, - QuestionModule::class, NetworkConfigProdModule::class, ContinueModule::class, - FractionInputModule::class, ItemSelectionInputModule::class, MultipleChoiceInputModule::class, - NumberWithUnitsRuleModule::class, NumericInputRuleModule::class, TextInputRuleModule::class, - DragDropSortInputModule::class, ImageClickInputModule::class, RatioInputModule::class, - HintsAndSolutionConfigModule::class, ExpirationMetaDataRetrieverModule::class, - GlideImageLoaderModule::class, PrimeTopicAssetsControllerModule::class, - HtmlParserEntityTypeModule::class, NetworkConnectionDebugUtilModule::class, - DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, AssetModule::class, - LocaleProdModule::class, ActivityRecreatorTestModule::class, - PlatformParameterSingletonModule::class, NumericExpressionInputModule::class, - AlgebraicExpressionInputModule::class, MathEquationInputModule::class ] ) - interface TestApplicationComponent : ApplicationComponent { + interface TestApplicationComponent { @Component.Builder interface Builder { @BindsInstance @@ -636,7 +938,7 @@ class MathExpressionAccessibilityUtilTest { fun inject(test: MathExpressionAccessibilityUtilTest) } - class TestApplication : Application(), ActivityComponentFactory, ApplicationInjectorProvider { + class TestApplication : Application() { private val component: TestApplicationComponent by lazy { DaggerMathExpressionAccessibilityUtilTest_TestApplicationComponent.builder() .setApplication(this) @@ -646,15 +948,11 @@ class MathExpressionAccessibilityUtilTest { fun inject(test: MathExpressionAccessibilityUtilTest) { component.inject(test) } - - override fun createActivityComponent(activity: AppCompatActivity): ActivityComponent { - return component.getActivityComponentBuilderProvider().get().setActivity(activity).build() - } - - override fun getApplicationInjector(): ApplicationInjector = component } private companion object { + // TODO: finalize this API. + private fun parseNumericExpressionSuccessfullyWithAllErrors( expression: String ): MathExpression { From 15d9d25baa8602d15366e69e1bf763ea9347ceb4 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Mon, 7 Feb 2022 13:09:12 -0800 Subject: [PATCH 120/162] Add missing test in Bazel, and fix it. --- .../oppia/android/app/databinding/BUILD.bazel | 42 +++++++++++++++++++ .../DrawableBindingAdaptersTest.kt | 7 +++- 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/app/src/sharedTest/java/org/oppia/android/app/databinding/BUILD.bazel b/app/src/sharedTest/java/org/oppia/android/app/databinding/BUILD.bazel index 64cac000417..26bfefccfd9 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/databinding/BUILD.bazel +++ b/app/src/sharedTest/java/org/oppia/android/app/databinding/BUILD.bazel @@ -39,6 +39,48 @@ genrule( """, ) +genrule( + name = "update_DrawableBindingAdaptersTest", + srcs = ["DrawableBindingAdaptersTest.kt"], + outs = ["DrawableBindingAdaptersTest_updated.kt"], + cmd = """ + cat $(SRCS) | + sed 's/import org.oppia.android.R/import org.oppia.android.app.databinding.R/g' | + sed 's/import org.oppia.android.databinding./import org.oppia.android.app.databinding.databinding./g' | + sed 's/import org.oppia.android.app.databinding.DrawableBindingAdapters./import org.oppia.android.app.databinding.DrawableBindingAdapters_updated./g' > $(OUTS) + """, +) + +oppia_android_test( + name = "DrawableBindingAdaptersTest", + srcs = ["DrawableBindingAdaptersTest_updated.kt"], + custom_package = "org.oppia.android.app.databinding", + test_class = "org.oppia.android.app.databinding.DrawableBindingAdaptersTest", + test_manifest = "//app:test_manifest", + deps = [ + ":dagger", + "//app", + "//app:test_deps", + "//app/src/main/java/org/oppia/android/app/activity:activity_intent_factories_shim", + "//app/src/main/java/org/oppia/android/app/translation/testing:test_module", + "//domain", + "//testing", + "//testing/src/main/java/org/oppia/android/testing/junit:initialize_default_locale_rule", + "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module", + "//testing/src/main/java/org/oppia/android/testing/threading:test_module", + "//testing/src/main/java/org/oppia/android/testing/time:test_module", + "//third_party:androidx_test_ext_junit", + "//third_party:androidx_test_ext_truth", + "//third_party:junit_junit", + "//third_party:org_robolectric_robolectric", + "//third_party:robolectric_android-all", + "//utility/src/main/java/org/oppia/android/util/accessibility:test_module", + "//utility/src/main/java/org/oppia/android/util/caching/testing:caching_test_module", + "//utility/src/main/java/org/oppia/android/util/data:data_providers", + "//utility/src/main/java/org/oppia/android/util/logging:prod_module", + ], +) + oppia_android_test( name = "ImageViewBindingAdaptersTest", srcs = ["ImageViewBindingAdaptersTest_updated.kt"], diff --git a/app/src/sharedTest/java/org/oppia/android/app/databinding/DrawableBindingAdaptersTest.kt b/app/src/sharedTest/java/org/oppia/android/app/databinding/DrawableBindingAdaptersTest.kt index 0cc4311d565..11b533cb940 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/databinding/DrawableBindingAdaptersTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/databinding/DrawableBindingAdaptersTest.kt @@ -38,13 +38,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -171,7 +174,9 @@ class DrawableBindingAdaptersTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { From b63883e2fd3d4fe17fc0aa619d7342b38f52b58b Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Mon, 7 Feb 2022 13:10:14 -0800 Subject: [PATCH 121/162] Correct order for genrule. --- .../oppia/android/app/databinding/BUILD.bazel | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/app/src/sharedTest/java/org/oppia/android/app/databinding/BUILD.bazel b/app/src/sharedTest/java/org/oppia/android/app/databinding/BUILD.bazel index 26bfefccfd9..341b24b5d13 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/databinding/BUILD.bazel +++ b/app/src/sharedTest/java/org/oppia/android/app/databinding/BUILD.bazel @@ -28,26 +28,26 @@ _TEST_FILES = [ ] genrule( - name = "update_ImageViewBindingAdaptersTest", - srcs = ["ImageViewBindingAdaptersTest.kt"], - outs = ["ImageViewBindingAdaptersTest_updated.kt"], + name = "update_DrawableBindingAdaptersTest", + srcs = ["DrawableBindingAdaptersTest.kt"], + outs = ["DrawableBindingAdaptersTest_updated.kt"], cmd = """ cat $(SRCS) | sed 's/import org.oppia.android.R/import org.oppia.android.app.databinding.R/g' | sed 's/import org.oppia.android.databinding./import org.oppia.android.app.databinding.databinding./g' | - sed 's/import org.oppia.android.app.databinding.ImageViewBindingAdapters./import org.oppia.android.app.databinding.ImageViewBindingAdapters_updated./g' > $(OUTS) + sed 's/import org.oppia.android.app.databinding.DrawableBindingAdapters./import org.oppia.android.app.databinding.DrawableBindingAdapters_updated./g' > $(OUTS) """, ) genrule( - name = "update_DrawableBindingAdaptersTest", - srcs = ["DrawableBindingAdaptersTest.kt"], - outs = ["DrawableBindingAdaptersTest_updated.kt"], + name = "update_ImageViewBindingAdaptersTest", + srcs = ["ImageViewBindingAdaptersTest.kt"], + outs = ["ImageViewBindingAdaptersTest_updated.kt"], cmd = """ cat $(SRCS) | sed 's/import org.oppia.android.R/import org.oppia.android.app.databinding.R/g' | sed 's/import org.oppia.android.databinding./import org.oppia.android.app.databinding.databinding./g' | - sed 's/import org.oppia.android.app.databinding.DrawableBindingAdapters./import org.oppia.android.app.databinding.DrawableBindingAdapters_updated./g' > $(OUTS) + sed 's/import org.oppia.android.app.databinding.ImageViewBindingAdapters./import org.oppia.android.app.databinding.ImageViewBindingAdapters_updated./g' > $(OUTS) """, ) From 06427575cdbfbd5ab2e2505d9c7598ad63c5cac2 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Mon, 7 Feb 2022 18:52:23 -0800 Subject: [PATCH 122/162] Add full test suite. --- .../math/MathExpressionAccessibilityUtil.kt | 87 +- .../android/app/utility/math/BUILD.bazel | 4 +- .../MathExpressionAccessibilityUtilTest.kt | 1774 ++++++++++------- 3 files changed, 1060 insertions(+), 805 deletions(-) diff --git a/app/src/main/java/org/oppia/android/app/utility/math/MathExpressionAccessibilityUtil.kt b/app/src/main/java/org/oppia/android/app/utility/math/MathExpressionAccessibilityUtil.kt index c353b625048..efd658d12b1 100644 --- a/app/src/main/java/org/oppia/android/app/utility/math/MathExpressionAccessibilityUtil.kt +++ b/app/src/main/java/org/oppia/android/app/utility/math/MathExpressionAccessibilityUtil.kt @@ -30,33 +30,37 @@ import org.oppia.android.app.model.OppiaLanguage.LANGUAGE_UNSPECIFIED import org.oppia.android.app.model.OppiaLanguage.PORTUGUESE import org.oppia.android.app.model.OppiaLanguage.UNRECOGNIZED import org.oppia.android.app.model.Real.RealTypeCase.INTEGER -import org.oppia.android.util.math.toPlainText import java.text.NumberFormat import java.util.Locale import javax.inject.Inject import org.oppia.android.app.model.MathBinaryOperation.Operator as BinaryOperator import org.oppia.android.app.model.MathUnaryOperation.Operator as UnaryOperator +import org.oppia.android.app.model.Real.RealTypeCase.IRRATIONAL +import org.oppia.android.app.model.Real.RealTypeCase.RATIONAL +import org.oppia.android.app.model.Real.RealTypeCase.REALTYPE_NOT_SET class MathExpressionAccessibilityUtil @Inject constructor() { + // TODO: document that rationals aren't supported, and that irrationals are rounded during formatting (and maybe that ints are also formatted?). + fun convertToHumanReadableString( - equation: MathEquation, + expression: MathExpression, language: OppiaLanguage, divAsFraction: Boolean ): String? { return when (language) { - ENGLISH -> equation.toHumanReadableEnglishString(divAsFraction) + ENGLISH -> expression.toHumanReadableEnglishString(divAsFraction) ARABIC, HINDI, HINGLISH, PORTUGUESE, BRAZILIAN_PORTUGUESE, LANGUAGE_UNSPECIFIED, UNRECOGNIZED -> null } } fun convertToHumanReadableString( - expression: MathExpression, + equation: MathEquation, language: OppiaLanguage, divAsFraction: Boolean ): String? { return when (language) { - ENGLISH -> expression.toHumanReadableEnglishString(divAsFraction) + ENGLISH -> equation.toHumanReadableEnglishString(divAsFraction) ARABIC, HINDI, HINGLISH, PORTUGUESE, BRAZILIAN_PORTUGUESE, LANGUAGE_UNSPECIFIED, UNRECOGNIZED -> null } @@ -65,30 +69,6 @@ class MathExpressionAccessibilityUtil @Inject constructor() { private companion object { // TODO: move these to the UI layer & have them utilize non-translatable strings. private val numberFormat by lazy { NumberFormat.getNumberInstance(Locale.US) } - private val singularOrdinalNames = mapOf( - 1 to "oneth", - 2 to "half", - 3 to "third", - 4 to "fourth", - 5 to "fifth", - 6 to "sixth", - 7 to "seventh", - 8 to "eighth", - 9 to "ninth", - 10 to "tenth", - ) - private val pluralOrdinalNames = mapOf( - 1 to "oneths", - 2 to "halves", - 3 to "thirds", - 4 to "fourths", - 5 to "fifths", - 6 to "sixths", - 7 to "sevenths", - 8 to "eighths", - 9 to "ninths", - 10 to "tenths", - ) private fun MathEquation.toHumanReadableEnglishString(divAsFraction: Boolean): String? { val lhsStr = leftSide.toHumanReadableEnglishString(divAsFraction) @@ -100,9 +80,13 @@ class MathExpressionAccessibilityUtil @Inject constructor() { // Reference: // https://docs.google.com/document/d/1P-dldXQ08O-02ZRG978paiWOSz0dsvcKpDgiV_rKH_Y/view. return when (expressionTypeCase) { - CONSTANT -> if (constant.realTypeCase == INTEGER) { - numberFormat.format(constant.integer.toLong()) - } else constant.toPlainText() + CONSTANT -> when (constant.realTypeCase) { + IRRATIONAL -> numberFormat.format(constant.irrational) + INTEGER -> numberFormat.format(constant.integer.toLong()) + // Note that rational types should not actually be encountered in raw expressions, so + // there's no explicit support for reading them out. + RATIONAL, REALTYPE_NOT_SET, null -> null + } VARIABLE -> when (variable) { "z" -> "zed" "Z" -> "Zed" @@ -122,20 +106,13 @@ class MathExpressionAccessibilityUtil @Inject constructor() { "$lhsStr $rhsStr" } else "$lhsStr times $rhsStr" } - DIVIDE -> { - if (divAsFraction && lhs.isConstantInteger() && rhs.isConstantInteger()) { - val numerator = lhs.constant.integer - val denominator = rhs.constant.integer - if (numerator in 0..10 && denominator in 1..10 && denominator >= numerator) { - val ordinalName = - if (numerator == 1) { - singularOrdinalNames.getValue(denominator) - } else pluralOrdinalNames.getValue(denominator) - "$numerator $ordinalName" - } else "$lhsStr over $rhsStr" - } else if (divAsFraction) { - "the fraction with numerator $lhsStr and denominator $rhsStr" - } else "$lhsStr divided by $rhsStr" + DIVIDE -> when { + divAsFraction -> when { + binaryOperation.isOneHalf() -> "one half" + binaryOperation.isSimpleFraction() -> "$lhsStr over $rhsStr" + else -> "the fraction with numerator $lhsStr and denominator $rhsStr" + } + else -> "$lhsStr divided by $rhsStr" } EXPONENTIATE -> "$lhsStr raised to the power of $rhsStr" BinaryOperator.OPERATOR_UNSPECIFIED, BinaryOperator.UNRECOGNIZED, null -> null @@ -176,8 +153,16 @@ class MathExpressionAccessibilityUtil @Inject constructor() { return rightOperand.isVariable() || rightOperand.isExponentiation() } - private fun MathExpression.isConstantInteger(): Boolean = - expressionTypeCase == CONSTANT && constant.realTypeCase == INTEGER + private fun MathBinaryOperation.isSimpleFraction(): Boolean { + // 'Simple' fractions are those with single term numerators and denominators (which are + // subsequently easier to read out), and whose constant numerator/denonominators are integers. + return leftOperand.isSimpleFractionTerm() && rightOperand.isSimpleFractionTerm() + } + + private fun MathBinaryOperation.isOneHalf(): Boolean { + // If the either operand isn't an integer it will default to 0 per proto3 rules. + return leftOperand.constant.integer == 1 && rightOperand.constant.integer == 2 + } private fun MathExpression.isConstant(): Boolean = expressionTypeCase == CONSTANT @@ -192,5 +177,11 @@ class MathExpressionAccessibilityUtil @Inject constructor() { GROUP -> group.isSingleTerm() EXPRESSIONTYPE_NOT_SET, null -> false } + + private fun MathExpression.isSimpleFractionTerm(): Boolean = when (expressionTypeCase) { + CONSTANT -> constant.realTypeCase == INTEGER + VARIABLE -> true + BINARY_OPERATION, UNARY_OPERATION, FUNCTION_CALL, GROUP, EXPRESSIONTYPE_NOT_SET, null -> false + } } } diff --git a/app/src/test/java/org/oppia/android/app/utility/math/BUILD.bazel b/app/src/test/java/org/oppia/android/app/utility/math/BUILD.bazel index efbfba1af52..48908b50a8c 100644 --- a/app/src/test/java/org/oppia/android/app/utility/math/BUILD.bazel +++ b/app/src/test/java/org/oppia/android/app/utility/math/BUILD.bazel @@ -16,9 +16,11 @@ oppia_android_test( "//app/src/main/java/org/oppia/android/app/utility/math:math_expression_accessibility_util", "//model/src/main/proto:languages_java_proto_lite", "//model/src/main/proto:math_java_proto_lite", + "//testing/src/main/java/org/oppia/android/testing/junit:oppia_parameterized_test_runner", + "//testing/src/main/java/org/oppia/android/testing/junit:parameterized_robolectric_test_runner", "//testing/src/main/java/org/oppia/android/testing/math:math_equation_subject", "//testing/src/main/java/org/oppia/android/testing/math:math_expression_subject", - "//third_party:androidx_test_ext_junit", + "//third_party:androidx_test_core", "//third_party:com_google_truth_truth", "//third_party:junit_junit", "//third_party:org_robolectric_robolectric", diff --git a/app/src/test/java/org/oppia/android/app/utility/math/MathExpressionAccessibilityUtilTest.kt b/app/src/test/java/org/oppia/android/app/utility/math/MathExpressionAccessibilityUtilTest.kt index c536a5b8c03..baa7ea46a6e 100644 --- a/app/src/test/java/org/oppia/android/app/utility/math/MathExpressionAccessibilityUtilTest.kt +++ b/app/src/test/java/org/oppia/android/app/utility/math/MathExpressionAccessibilityUtilTest.kt @@ -2,7 +2,6 @@ package org.oppia.android.app.utility.math import android.app.Application import androidx.test.core.app.ApplicationProvider -import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.StringSubject import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage @@ -13,8 +12,11 @@ import javax.inject.Singleton import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.oppia.android.app.model.MathBinaryOperation import org.oppia.android.app.model.MathEquation import org.oppia.android.app.model.MathExpression +import org.oppia.android.app.model.MathFunctionCall +import org.oppia.android.app.model.MathUnaryOperation import org.oppia.android.app.model.OppiaLanguage import org.oppia.android.app.model.OppiaLanguage.ARABIC import org.oppia.android.app.model.OppiaLanguage.BRAZILIAN_PORTUGUESE @@ -24,854 +26,1160 @@ import org.oppia.android.app.model.OppiaLanguage.HINGLISH import org.oppia.android.app.model.OppiaLanguage.LANGUAGE_UNSPECIFIED import org.oppia.android.app.model.OppiaLanguage.PORTUGUESE import org.oppia.android.app.model.OppiaLanguage.UNRECOGNIZED +import org.oppia.android.app.model.Real +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Iteration +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Parameter +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.RunParameterized +import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.SelectRunnerPlatform +import org.oppia.android.testing.junit.ParameterizedRobolectricTestRunner import org.oppia.android.testing.math.MathEquationSubject import org.oppia.android.testing.math.MathEquationSubject.Companion.assertThat import org.oppia.android.testing.math.MathExpressionSubject import org.oppia.android.testing.math.MathExpressionSubject.Companion.assertThat import org.oppia.android.util.math.MathExpressionParser import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode +import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode.ALL_ERRORS +import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode.REQUIRED_ONLY import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult +import org.oppia.android.util.math.ONE_HALF import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode -/** Tests for [MathExpressionAccessibilityUtil]. */ -@RunWith(AndroidJUnit4::class) +/** + * Tests for [MathExpressionAccessibilityUtil]. + * + * Note that this test suite does not make an effort to differentiate tests for numeric and + * algebraic expressions since it's mainly testing [MathExpression] and [MathEquation] structures, + * and relies on other test suites to verify that raw numeric expressions can be correctly converted + * to [MathExpression]s. + */ +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") +@RunWith(OppiaParameterizedTestRunner::class) +@SelectRunnerPlatform(ParameterizedRobolectricTestRunner::class) @LooperMode(LooperMode.Mode.PAUSED) @Config(application = MathExpressionAccessibilityUtilTest.TestApplication::class) class MathExpressionAccessibilityUtilTest { - @Inject - lateinit var util: MathExpressionAccessibilityUtil + @Inject lateinit var util: MathExpressionAccessibilityUtil - // TODO: finish tests - - @Before - fun setUp() { - setUpTestApplicationComponent() - } - - @Test - fun test1() { - // TODO: do something with this test. - val exp = parseNumericExpressionSuccessfullyWithAllErrors("1") - assertThat(exp).forHumanReadable(ARABIC).doesNotConvertToString() - } - - @Test - fun test2() { - // TODO: do something with this test. - val exp = parseNumericExpressionSuccessfullyWithAllErrors("1") - assertThat(exp).forHumanReadable(HINDI).doesNotConvertToString() - } - - @Test - fun test3() { - // TODO: do something with this test. - val exp = parseNumericExpressionSuccessfullyWithAllErrors("1") - assertThat(exp).forHumanReadable(HINGLISH).doesNotConvertToString() - } - - @Test - fun test4() { - // TODO: do something with this test. - val exp = parseNumericExpressionSuccessfullyWithAllErrors("1") - assertThat(exp).forHumanReadable(PORTUGUESE).doesNotConvertToString() - } + @Parameter lateinit var language: String + @Parameter lateinit var expression: String + @Parameter lateinit var equation: String + @Parameter lateinit var a11yStr: String @Test - fun test5() { - // TODO: do something with this test. - val exp = parseNumericExpressionSuccessfullyWithAllErrors("1") - assertThat(exp).forHumanReadable(BRAZILIAN_PORTUGUESE).doesNotConvertToString() - } + fun testConvertToString_defaultExp_english_returnsNull() { + val exp = MathExpression.getDefaultInstance() - @Test - fun test6() { - // TODO: do something with this test. - val exp = parseNumericExpressionSuccessfullyWithAllErrors("1") - assertThat(exp).forHumanReadable(LANGUAGE_UNSPECIFIED).doesNotConvertToString() + assertThat(exp).forHumanReadable(ENGLISH).doesNotConvertToString() } @Test - fun test7() { - // TODO: do something with this test. - val exp = parseNumericExpressionSuccessfullyWithAllErrors("1") - assertThat(exp).forHumanReadable(UNRECOGNIZED).doesNotConvertToString() - } + fun testConvertToString_defaultEq_english_returnsNull() { + val eq = MathEquation.getDefaultInstance() - @Test - fun test8() { - // TODO: do something with this test. - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("x") - assertThat(exp).forHumanReadable(ARABIC).doesNotConvertToString() + assertThat(eq).forHumanReadable(ENGLISH).doesNotConvertToString() } @Test - fun test9() { - // TODO: do something with this test. - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("x") - assertThat(exp).forHumanReadable(HINDI).doesNotConvertToString() - } - - @Test - fun test10() { - // TODO: do something with this test. - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("x") - assertThat(exp).forHumanReadable(HINGLISH).doesNotConvertToString() - } - - @Test - fun test11() { - // TODO: do something with this test. - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("x") - assertThat(exp).forHumanReadable(PORTUGUESE).doesNotConvertToString() - } + @RunParameterized( + Iteration("LANGUAGE_UNSPECIFIED", "language=LANGUAGE_UNSPECIFIED"), + Iteration("ARABIC", "language=ARABIC"), + Iteration("HINDI", "language=HINDI"), + Iteration("HINGLISH", "language=HINGLISH"), + Iteration("PORTUGUESE", "language=PORTUGUESE"), + Iteration("BRAZILIAN_PORTUGUESE", "language=BRAZILIAN_PORTUGUESE"), + Iteration("UNRECOGNIZED", "language=UNRECOGNIZED") + ) + fun testConvertToString_constExp_unsupportedLanguage_returnsNull() { + val exp = parseAlgebraicExpression("2") + val language = OppiaLanguage.valueOf(language) - @Test - fun test12() { - // TODO: do something with this test. - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("x") - assertThat(exp).forHumanReadable(BRAZILIAN_PORTUGUESE).doesNotConvertToString() + assertThat(exp).forHumanReadable(language).doesNotConvertToString() } @Test - fun test13() { - // TODO: do something with this test. - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("x") - assertThat(exp).forHumanReadable(LANGUAGE_UNSPECIFIED).doesNotConvertToString() - } + @RunParameterized( + Iteration("LANGUAGE_UNSPECIFIED", "language=LANGUAGE_UNSPECIFIED"), + Iteration("ARABIC", "language=ARABIC"), + Iteration("HINDI", "language=HINDI"), + Iteration("HINGLISH", "language=HINGLISH"), + Iteration("PORTUGUESE", "language=PORTUGUESE"), + Iteration("BRAZILIAN_PORTUGUESE", "language=BRAZILIAN_PORTUGUESE"), + Iteration("UNRECOGNIZED", "language=UNRECOGNIZED") + ) + fun testConvertToString_constEq_unsupportedLanguage_returnsNull() { + val eq = parseAlgebraicEquation("x=2") + val language = OppiaLanguage.valueOf(language) - @Test - fun test14() { - // TODO: do something with this test. - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("x") - assertThat(exp).forHumanReadable(UNRECOGNIZED).doesNotConvertToString() + assertThat(eq).forHumanReadable(language).doesNotConvertToString() } @Test - fun test15() { - // TODO: do something with this test. - val eq = parseAlgebraicEquationSuccessfullyWithAllErrors("x=1") - assertThat(eq).forHumanReadable(ARABIC).doesNotConvertToString() + fun testTestSuite_verifyLanguageCoverage_allLanguagesCovered() { + // NOTE TO DEVELOPERS: This is a meta test to verify that the tests above are covering all + // supported languages. If this test ever fails, please make sure to update both the list below + // and other relevant tests in this suite. + assertThat(OppiaLanguage.values()) + .asList() + .containsExactly( + LANGUAGE_UNSPECIFIED, ENGLISH, ARABIC, HINDI, HINGLISH, PORTUGUESE, BRAZILIAN_PORTUGUESE, + UNRECOGNIZED + ) } @Test - fun test16() { - // TODO: do something with this test. - val eq = parseAlgebraicEquationSuccessfullyWithAllErrors("x=1") - assertThat(eq).forHumanReadable(HINDI).doesNotConvertToString() - } + @RunParameterized( + Iteration("2", "expression=2", "a11yStr=2"), + Iteration("123", "expression=123", "a11yStr=123"), + Iteration("1234", "expression=1234", "a11yStr=1,234"), + Iteration("12345", "expression=12345", "a11yStr=12,345"), + Iteration("123456", "expression=123456", "a11yStr=123,456"), + Iteration("1234567", "expression=1234567", "a11yStr=1,234,567") + ) + fun testConvertToString_eng_constIntExp_returnsIntegerConvertedString() { + val exp = parseAlgebraicExpression(expression) + + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo(a11yStr) + } + + @Test + @RunParameterized( + // Note that some rounding occurs when formatting doubles with decimals. + Iteration("2.0", "expression=2.0", "a11yStr=2"), + Iteration("3.14", "expression=3.14", "a11yStr=3.14"), + Iteration( + "long_pi", "expression=3.14159265358979323846264338327950288419716939937510", "a11yStr=3.142" + ), + Iteration("1234.0", "expression=1234.0", "a11yStr=1,234"), + Iteration("12345.0", "expression=12345.0", "a11yStr=12,345"), + Iteration("123456.0", "expression=123456.0", "a11yStr=123,456"), + Iteration("1234567.0", "expression=1234567.0", "a11yStr=1,234,567"), + Iteration("1234567.987654321", "expression=1234567.987654321", "a11yStr=1,234,567.988"), + // Verify that scientific notation isn't used. + Iteration("small_number", "expression=0.000000000000000000001", "a11yStr=0"), + Iteration( + "large_number", "expression=123456789101112131415.0", "a11yStr=123,456,789,101,112,130,000" + ) + ) + fun testConvertToString_eng_constDoubleExp_returnsDoubleConvertedString() { + val exp = parseAlgebraicExpression(expression) - @Test - fun test17() { - // TODO: do something with this test. - val eq = parseAlgebraicEquationSuccessfullyWithAllErrors("x=1") - assertThat(eq).forHumanReadable(HINGLISH).doesNotConvertToString() + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo(a11yStr) } @Test - fun test18() { - // TODO: do something with this test. - val eq = parseAlgebraicEquationSuccessfullyWithAllErrors("x=1") - assertThat(eq).forHumanReadable(PORTUGUESE).doesNotConvertToString() - } + @RunParameterized( + Iteration("x", "expression=x", "a11yStr=x"), + Iteration("y", "expression=y", "a11yStr=y"), + Iteration("z", "expression=z", "a11yStr=zed"), + Iteration("X", "expression=X", "a11yStr=X"), + Iteration("Y", "expression=Y", "a11yStr=Y"), + Iteration("Z", "expression=Z", "a11yStr=Zed"), + Iteration("a", "expression=a", "a11yStr=a") + ) + fun testConvertToString_eng_variableExp_returnsVariableNameWithZed() { + val allowedVariables = listOf("a", "x", "y", "z", "X", "Y", "Z") + val exp = parseAlgebraicExpression(expression, allowedVariables) - @Test - fun test19() { - // TODO: do something with this test. - val eq = parseAlgebraicEquationSuccessfullyWithAllErrors("x=1") - assertThat(eq).forHumanReadable(BRAZILIAN_PORTUGUESE).doesNotConvertToString() + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo(a11yStr) } @Test - fun test20() { - // TODO: do something with this test. - val eq = parseAlgebraicEquationSuccessfullyWithAllErrors("x=1") - assertThat(eq).forHumanReadable(LANGUAGE_UNSPECIFIED).doesNotConvertToString() - } + @RunParameterized( + Iteration("1+2", "expression=1+2", "a11yStr=1 plus 2"), + Iteration("1+x", "expression=1+x", "a11yStr=1 plus x"), + Iteration("z+1234", "expression=z+1234", "a11yStr=zed plus 1,234"), + Iteration("z+3.14", "expression=z+3.14", "a11yStr=zed plus 3.14"), + Iteration("x+z", "expression=x+z", "a11yStr=x plus zed") + ) + fun testConvertToString_eng_addition_returnsLeftPlusRightString() { + val exp = parseAlgebraicExpression(expression) - @Test - fun test21() { - // TODO: do something with this test. - val eq = parseAlgebraicEquationSuccessfullyWithAllErrors("x=1") - assertThat(eq).forHumanReadable(UNRECOGNIZED).doesNotConvertToString() + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo(a11yStr) } @Test - fun test22() { - // TODO: do something with this test. - // specific cases (from rules & other cases): - val exp = parseNumericExpressionSuccessfullyWithAllErrors("1") - assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1") - assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1") - } + @RunParameterized( + Iteration("1-2", "expression=1-2", "a11yStr=1 minus 2"), + Iteration("1-x", "expression=1-x", "a11yStr=1 minus x"), + Iteration("z-1234", "expression=z-1234", "a11yStr=zed minus 1,234"), + Iteration("z-3.14", "expression=z-3.14", "a11yStr=zed minus 3.14"), + Iteration("x-z", "expression=x-z", "a11yStr=x minus zed") + ) + fun testConvertToString_eng_subtraction_returnsLeftMinusRightString() { + val exp = parseAlgebraicExpression(expression) - @Test - fun test23() { - // TODO: do something with this test. - val exp = parseNumericExpressionSuccessfullyWithAllErrors("-1") - assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("negative 1") + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo(a11yStr) } @Test - fun test24() { - // TODO: do something with this test. - val exp = parseNumericExpressionSuccessfullyWithoutOptionalErrors("+1") - assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("positive 1") - } + @RunParameterized( + Iteration("1*2", "expression=1*2", "a11yStr=1 times 2"), + Iteration("1*x", "expression=1*x", "a11yStr=1 times x"), + Iteration("z*1234", "expression=z*1234", "a11yStr=zed times 1,234"), + Iteration("z*3.14", "expression=z*3.14", "a11yStr=zed times 3.14"), + Iteration("x*z", "expression=x*z", "a11yStr=x times zed") + ) + fun testConvertToString_eng_multiplication_returnsLeftTimesRightString() { + val exp = parseAlgebraicExpression(expression) - @Test - fun test25() { - // TODO: do something with this test. - val exp = parseNumericExpressionSuccessfullyWithoutOptionalErrors("((1))") - assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1") + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo(a11yStr) } @Test - fun test26() { - // TODO: do something with this test. - val exp = parseNumericExpressionSuccessfullyWithAllErrors("1+2") - assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 plus 2") - } + @RunParameterized( + Iteration("1/2", "expression=1/2", "a11yStr=1 divided by 2"), + Iteration("1/x", "expression=1/x", "a11yStr=1 divided by x"), + Iteration("z/1234", "expression=z/1234", "a11yStr=zed divided by 1,234"), + Iteration("z/3.14", "expression=z/3.14", "a11yStr=zed divided by 3.14"), + Iteration("x/z", "expression=x/z", "a11yStr=x divided by zed") + ) + fun testConvertToString_eng_division_returnsLeftDividedByRightString() { + val exp = parseAlgebraicExpression(expression) - @Test - fun test27() { - // TODO: do something with this test. - val exp = parseNumericExpressionSuccessfullyWithAllErrors("1-2") - assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 minus 2") + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo(a11yStr) } @Test - fun test28() { - // TODO: do something with this test. - val exp = parseNumericExpressionSuccessfullyWithAllErrors("1*2") - assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 times 2") - } + @RunParameterized( + Iteration("1^2", "expression=1^2", "a11yStr=1 raised to the power of 2"), + Iteration("1^x", "expression=1^x", "a11yStr=1 raised to the power of x"), + Iteration("z^1234", "expression=z^1234", "a11yStr=zed raised to the power of 1,234"), + Iteration("z^3.14", "expression=z^3.14", "a11yStr=zed raised to the power of 3.14"), + Iteration("x^z", "expression=x^z", "a11yStr=x raised to the power of zed") + ) + fun testConvertToString_eng_exponentiation_returnsLeftRaisedToThePowerOfRightString() { + // Some expressions may include variable terms as exponents (which normally isn't allowed). + val exp = parseAlgebraicExpression(expression, errorCheckingMode = REQUIRED_ONLY) - @Test - fun test29() { - // TODO: do something with this test. - val exp = parseNumericExpressionSuccessfullyWithAllErrors("1/2") - assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 divided by 2") + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo(a11yStr) } @Test - fun test30() { - // TODO: do something with this test. - val exp = parseNumericExpressionSuccessfullyWithAllErrors("1+(1-2)") - assertThat(exp) - .forHumanReadable(ENGLISH) - .convertsToStringThat() - .isEqualTo("1 plus open parenthesis 1 minus 2 close parenthesis") - } + @RunParameterized( + Iteration("-2", "expression=-2", "a11yStr=negative 2"), + Iteration("-x", "expression=-x", "a11yStr=negative x"), + Iteration("-1234", "expression=-1234", "a11yStr=negative 1,234"), + Iteration("-3.14", "expression=-3.14", "a11yStr=negative 3.14"), + Iteration("-z", "expression=-z", "a11yStr=negative zed") + ) + fun testConvertToString_eng_negation_returnsNegativeOperandString() { + val exp = parseAlgebraicExpression(expression) - @Test - fun test31() { - // TODO: do something with this test. - val exp = parseNumericExpressionSuccessfullyWithAllErrors("2^3") - assertThat(exp) - .forHumanReadable(ENGLISH) - .convertsToStringThat() - .isEqualTo("2 raised to the power of 3") + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo(a11yStr) } @Test - fun test32() { - // TODO: do something with this test. - val exp = parseNumericExpressionSuccessfullyWithAllErrors("2^(1+2)") - assertThat(exp) - .forHumanReadable(ENGLISH) - .convertsToStringThat() - .isEqualTo("2 raised to the power of open parenthesis 1 plus 2 close parenthesis") - } + @RunParameterized( + Iteration("+2", "expression=+2", "a11yStr=positive 2"), + Iteration("+x", "expression=+x", "a11yStr=positive x"), + Iteration("+1234", "expression=+1234", "a11yStr=positive 1,234"), + Iteration("+3.14", "expression=+3.14", "a11yStr=positive 3.14"), + Iteration("+z", "expression=+z", "a11yStr=positive zed") + ) + fun testConvertToString_eng_positiveUnary_returnsPositiveOperandString() { + // Allow positive unary operations to verify this case. + val exp = parseAlgebraicExpression(expression, errorCheckingMode = REQUIRED_ONLY) - @Test - fun test33() { - // TODO: do something with this test. - val exp = parseNumericExpressionSuccessfullyWithAllErrors("100000*2") - assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("100,000 times 2") + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo(a11yStr) } @Test - fun test34() { - // TODO: do something with this test. - val exp = parseNumericExpressionSuccessfullyWithAllErrors("sqrt(2)") - assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("square root of 2") - } + @RunParameterized( + Iteration("√2", "expression=√2", "a11yStr=square root of 2"), + Iteration("√x", "expression=√x", "a11yStr=square root of x"), + Iteration("√z", "expression=√z", "a11yStr=square root of zed"), + Iteration("√1234", "expression=√1234", "a11yStr=square root of 1,234"), + Iteration("√3.14", "expression=√3.14", "a11yStr=square root of 3.14"), + Iteration("√(2)", "expression=√(2)", "a11yStr=square root of 2"), + Iteration("√(x)", "expression=√(x)", "a11yStr=square root of x"), + Iteration("√(z)", "expression=√(z)", "a11yStr=square root of zed"), + Iteration("√(1234)", "expression=√(1234)", "a11yStr=square root of 1,234"), + Iteration("√(3.14)", "expression=√(3.14)", "a11yStr=square root of 3.14") + ) + fun testConvertToString_eng_inlineSqrt_returnsSquareRootOfArgumentString() { + // Allow for single-term parentheses for testing (even though these cases would normally result + // in errors). + val exp = parseAlgebraicExpression(expression, errorCheckingMode = REQUIRED_ONLY) - @Test - fun test35() { - // TODO: do something with this test. - val exp = parseNumericExpressionSuccessfullyWithAllErrors("√2") - assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("square root of 2") + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo(a11yStr) } @Test - fun test36() { - // TODO: do something with this test. - val exp = parseNumericExpressionSuccessfullyWithAllErrors("sqrt(1+2)") - assertThat(exp) - .forHumanReadable(ENGLISH) - .convertsToStringThat() - .isEqualTo("start square root 1 plus 2 end square root") - } - - @Test - fun test37() { - // TODO: do something with this test. - val singularOrdinalNames = mapOf( - 1 to "oneth", - 2 to "half", - 3 to "third", - 4 to "fourth", - 5 to "fifth", - 6 to "sixth", - 7 to "seventh", - 8 to "eighth", - 9 to "ninth", - 10 to "tenth", + @RunParameterized( + Iteration("sqrt(2)", "expression=sqrt(2)", "a11yStr=square root of 2"), + Iteration("sqrt(x)", "expression=sqrt(x)", "a11yStr=square root of x"), + Iteration("sqrt(z)", "expression=sqrt(z)", "a11yStr=square root of zed"), + Iteration("sqrt(1234)", "expression=sqrt(1234)", "a11yStr=square root of 1,234"), + Iteration("sqrt(3.14)", "expression=sqrt(3.14)", "a11yStr=square root of 3.14") + ) + fun testConvertToString_eng_sqrt_returnsSquareRootOfArgumentString() { + val exp = parseAlgebraicExpression(expression) + + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo(a11yStr) + } + + @Test + @RunParameterized( + Iteration("(2)", "expression=(2)", "a11yStr=2"), + Iteration("(x)", "expression=(x)", "a11yStr=x"), + Iteration("(z)", "expression=(z)", "a11yStr=zed"), + Iteration("(1234)", "expression=(1234)", "a11yStr=1,234"), + Iteration("(3.14)", "expression=(3.14)", "a11yStr=3.14"), + Iteration("((2))", "expression=((2))", "a11yStr=2"), + Iteration("((x))", "expression=((x))", "a11yStr=x"), + Iteration("((z))", "expression=((z))", "a11yStr=zed"), + Iteration("((1234))", "expression=((1234))", "a11yStr=1,234"), + Iteration("((3.14))", "expression=((3.14))", "a11yStr=3.14"), + Iteration("(√2)", "expression=(√2)", "a11yStr=square root of 2"), + Iteration("(√x)", "expression=(√x)", "a11yStr=square root of x"), + Iteration("(sqrt(2))", "expression=(sqrt(2))", "a11yStr=square root of 2"), + Iteration("(sqrt(x))", "expression=(sqrt(x))", "a11yStr=square root of x") + ) + fun testConvertToString_eng_group_singleTermOrNestedSingleTerm_returnsDirectString() { + // Allow for single-term parentheses for testing (even though these cases would normally result + // in errors). + val exp = parseAlgebraicExpression(expression, errorCheckingMode = REQUIRED_ONLY) + + // Verify that groups are not included in the final string when they only encapsulate single + // terms. + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo(a11yStr) + } + + @Test + @RunParameterized( + Iteration("(1+2)", "expression=(1+2)", "a11yStr=open parenthesis 1 plus 2 close parenthesis"), + Iteration("(1+x)", "expression=(1+x)", "a11yStr=open parenthesis 1 plus x close parenthesis"), + Iteration("(1+z)", "expression=(1+z)", "a11yStr=open parenthesis 1 plus zed close parenthesis"), + Iteration( + "(1+1234)", "expression=(1+1234)", "a11yStr=open parenthesis 1 plus 1,234 close parenthesis" + ), + Iteration( + "(1+3.14)", "expression=(1+3.14)", "a11yStr=open parenthesis 1 plus 3.14 close parenthesis" + ), + Iteration("(1-2)", "expression=(1-2)", "a11yStr=open parenthesis 1 minus 2 close parenthesis"), + Iteration("(x-2)", "expression=(x-2)", "a11yStr=open parenthesis x minus 2 close parenthesis"), + Iteration("(1*2)", "expression=(1*2)", "a11yStr=open parenthesis 1 times 2 close parenthesis"), + Iteration("(x*2)", "expression=(x*2)", "a11yStr=open parenthesis x times 2 close parenthesis"), + Iteration( + "(1/2)", "expression=(1/2)", "a11yStr=open parenthesis 1 divided by 2 close parenthesis" + ), + Iteration( + "(x/2)", "expression=(x/2)", "a11yStr=open parenthesis x divided by 2 close parenthesis" + ), + Iteration( + "(1^2)", + "expression=(1^2)", + "a11yStr=open parenthesis 1 raised to the power of 2 close parenthesis" + ), + Iteration( + "(x^2)", + "expression=(x^2)", + "a11yStr=open parenthesis x raised to the power of 2 close parenthesis" + ), + Iteration("(-2)", "expression=(-2)", "a11yStr=open parenthesis negative 2 close parenthesis"), + Iteration("(-x)", "expression=(-x)", "a11yStr=open parenthesis negative x close parenthesis"), + Iteration("(+2)", "expression=(+2)", "a11yStr=open parenthesis positive 2 close parenthesis"), + Iteration("(+x)", "expression=(+x)", "a11yStr=open parenthesis positive x close parenthesis") + ) + fun testConvertToString_eng_group_nestedOps_returnOpenParensOpCloseParensString() { + // Allow for the outer expression to have redundant parentheses to test cases when groups are + // announced (even though these exact cases would normally result in an error). + val exp = parseAlgebraicExpression(expression, errorCheckingMode = REQUIRED_ONLY) + + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo(a11yStr) + } + + @Test + @RunParameterized( + Iteration("√-2", "expression=√-2", "a11yStr=start square root negative 2 end square root"), + Iteration("√-x", "expression=√-x", "a11yStr=start square root negative x end square root"), + Iteration("√+2", "expression=√+2", "a11yStr=start square root positive 2 end square root"), + Iteration("√+x", "expression=√+x", "a11yStr=start square root positive x end square root"), + // Note that these cases compose with the group cases since √ only "attached" to the immediate + // next terms rather than being able to encapsulate a whole operation (like sqrt()). + Iteration( + "√(1+2)", + "expression=√(1+2)", + "a11yStr=start square root open parenthesis 1 plus 2 close parenthesis end square root" + ), + Iteration( + "√(1+x)", + "expression=√(1+x)", + "a11yStr=start square root open parenthesis 1 plus x close parenthesis end square root" + ), + Iteration( + "√(1-2)", + "expression=√(1-2)", + "a11yStr=start square root open parenthesis 1 minus 2 close parenthesis end square root" + ), + Iteration( + "√(1-x)", + "expression=√(1-x)", + "a11yStr=start square root open parenthesis 1 minus x close parenthesis end square root" + ), + Iteration( + "√(1*2)", + "expression=√(1*2)", + "a11yStr=start square root open parenthesis 1 times 2 close parenthesis end square root" + ), + Iteration( + "√(1*x)", + "expression=√(1*x)", + "a11yStr=start square root open parenthesis 1 times x close parenthesis end square root" + ), + Iteration( + "√(1/2)", + "expression=√(1/2)", + "a11yStr=start square root open parenthesis 1 divided by 2 close parenthesis end square root" + ), + Iteration( + "√(1/x)", + "expression=√(1/x)", + "a11yStr=start square root open parenthesis 1 divided by x close parenthesis end square root" + ), + Iteration( + "√(1^2)", + "expression=√(1^2)", + "a11yStr=start square root open parenthesis 1 raised to the power of 2 close parenthesis" + + " end square root" + ), + Iteration( + "√(1^x)", + "expression=√(1^x)", + "a11yStr=start square root open parenthesis 1 raised to the power of x close parenthesis" + + " end square root" + ), + Iteration( + "√(-2)", + "expression=√(-2)", + "a11yStr=start square root open parenthesis negative 2 close parenthesis end square root" + ), + Iteration( + "√(-x)", + "expression=√(-x)", + "a11yStr=start square root open parenthesis negative x close parenthesis end square root" + ), + Iteration( + "√(+2)", + "expression=√(+2)", + "a11yStr=start square root open parenthesis positive 2 close parenthesis end square root" + ), + Iteration( + "√(+x)", + "expression=√(+x)", + "a11yStr=start square root open parenthesis positive x close parenthesis end square root" ) - val pluralOrdinalNames = mapOf( - 1 to "oneths", - 2 to "halves", - 3 to "thirds", - 4 to "fourths", - 5 to "fifths", - 6 to "sixths", - 7 to "sevenths", - 8 to "eighths", - 9 to "ninths", - 10 to "tenths", + ) + fun testConvertToString_eng_inlineSqrt_nestedOp_returnsStartSquareRootConstructString() { + // Allow for positive unary expressions. + val exp = parseAlgebraicExpression(expression, errorCheckingMode = REQUIRED_ONLY) + + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo(a11yStr) + } + + @Test + @RunParameterized( + Iteration( + "sqrt(1+2)", "expression=sqrt(1+2)", "a11yStr=start square root 1 plus 2 end square root" + ), + Iteration( + "sqrt(1+x)", "expression=sqrt(1+x)", "a11yStr=start square root 1 plus x end square root" + ), + Iteration( + "sqrt(1-2)", "expression=sqrt(1-2)", "a11yStr=start square root 1 minus 2 end square root" + ), + Iteration( + "sqrt(1-x)", "expression=sqrt(1-x)", "a11yStr=start square root 1 minus x end square root" + ), + Iteration( + "sqrt(1*2)", "expression=sqrt(1*2)", "a11yStr=start square root 1 times 2 end square root" + ), + Iteration( + "sqrt(1*x)", "expression=sqrt(1*x)", "a11yStr=start square root 1 times x end square root" + ), + Iteration( + "sqrt(1/2)", + "expression=sqrt(1/2)", + "a11yStr=start square root 1 divided by 2 end square root" + ), + Iteration( + "sqrt(1/x)", + "expression=sqrt(1/x)", + "a11yStr=start square root 1 divided by x end square root" + ), + Iteration( + "sqrt(1^2)", + "expression=sqrt(1^2)", + "a11yStr=start square root 1 raised to the power of 2 end square root" + ), + Iteration( + "sqrt(1^x)", + "expression=sqrt(1^x)", + "a11yStr=start square root 1 raised to the power of x end square root" + ), + Iteration( + "sqrt(-2)", "expression=sqrt(-2)", "a11yStr=start square root negative 2 end square root" + ), + Iteration( + "sqrt(-x)", "expression=sqrt(-x)", "a11yStr=start square root negative x end square root" + ), + Iteration( + "sqrt(+2)", "expression=sqrt(+2)", "a11yStr=start square root positive 2 end square root" + ), + Iteration( + "sqrt(+x)", "expression=sqrt(+x)", "a11yStr=start square root positive x end square root" ) - for (denominatorToCheck in 1..10) { - for (numeratorToCheck in 0..denominatorToCheck) { - val exp = - parseNumericExpressionSuccessfullyWithAllErrors("$numeratorToCheck/$denominatorToCheck") - - val ordinalName = - if (numeratorToCheck == 1) { - singularOrdinalNames.getValue(denominatorToCheck) - } else pluralOrdinalNames.getValue(denominatorToCheck) - assertThat(exp) - .forHumanReadable(ENGLISH) - .convertsWithFractionsToStringThat() - .isEqualTo("$numeratorToCheck $ordinalName") - } - } - } - - @Test - fun test38() { - // TODO: do something with this test. - val exp = parseNumericExpressionSuccessfullyWithAllErrors("-1/3") - assertThat(exp) - .forHumanReadable(ENGLISH) - .convertsWithFractionsToStringThat() - .isEqualTo("negative 1 third") - } - - @Test - fun test39() { - // TODO: do something with this test. - val exp = parseNumericExpressionSuccessfullyWithAllErrors("-2/3") - assertThat(exp) - .forHumanReadable(ENGLISH) - .convertsWithFractionsToStringThat() - .isEqualTo("negative 2 thirds") - } - - @Test - fun test40() { - // TODO: do something with this test. - val exp = parseNumericExpressionSuccessfullyWithAllErrors("10/11") - assertThat(exp) - .forHumanReadable(ENGLISH) - .convertsWithFractionsToStringThat() - .isEqualTo("10 over 11") - } - - @Test - fun test41() { - // TODO: do something with this test. - val exp = parseNumericExpressionSuccessfullyWithAllErrors("121/7986") - assertThat(exp) - .forHumanReadable(ENGLISH) - .convertsWithFractionsToStringThat() - .isEqualTo("121 over 7,986") - } - - @Test - fun test42() { - // TODO: do something with this test. - val exp = parseNumericExpressionSuccessfullyWithAllErrors("8/7") - assertThat(exp) - .forHumanReadable(ENGLISH) - .convertsToStringThat() - .isEqualTo("8 over 7") - } - - @Test - fun test43() { - // TODO: do something with this test. - val exp = parseNumericExpressionSuccessfullyWithAllErrors("-10/-30") - assertThat(exp) - .forHumanReadable(ENGLISH) - .convertsWithFractionsToStringThat() - .isEqualTo("negative the fraction with numerator 10 and denominator negative 30") - } - - @Test - fun test44() { - // TODO: do something with this test. - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("1") - assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1") - } - - @Test - fun test45() { - // TODO: do something with this test. - val exp = parseAlgebraicExpressionSuccessfullyWithoutOptionalErrors("((1))") - assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1") - } - - @Test - fun test46() { - // TODO: do something with this test. - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("x") - assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("x") - } - - @Test - fun test47() { - // TODO: do something with this test. - val exp = parseAlgebraicExpressionSuccessfullyWithoutOptionalErrors("((x))") - assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("x") - } - - @Test - fun test48() { - // TODO: do something with this test. - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("-x") - assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("negative x") - } - - @Test - fun test49() { - // TODO: do something with this test. - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("+x") - assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("positive x") - } + ) + fun testConvertToString_eng_sqrt_nestedOp_returnsStartSquareRootConstructString() { + // Allow for positive unary expressions. + val exp = parseAlgebraicExpression(expression, errorCheckingMode = REQUIRED_ONLY) - @Test - fun test50() { - // TODO: do something with this test. - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+x") - assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 plus x") + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo(a11yStr) } @Test - fun test51() { - // TODO: do something with this test. - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("1-x") - assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 minus x") - } + @RunParameterized( + // Note that numeric exponentiations must be explicitly multiplied next to a constant. They + // otherwise result in a grammatical error that cannot be resolved. + Iteration("2x", "expression=2x", "a11yStr=2 x"), + Iteration("2z", "expression=2z", "a11yStr=2 zed"), + Iteration("2x^3", "expression=2x^3", "a11yStr=2 x raised to the power of 3"), + Iteration("2z^3", "expression=2z^3", "a11yStr=2 zed raised to the power of 3"), + Iteration("1234x^3.14", "expression=1234x^3.14", "a11yStr=1,234 x raised to the power of 3.14") + ) + fun testConvertToString_eng_implicitMult_leftConst_rightVarOrExp_returnsLeftRightString() { + val exp = parseAlgebraicExpression(expression) + + // Verify that the format [^ ] results in an implicit multiplication with + // no 'times' announced. + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo(a11yStr) + } + + @Test + @RunParameterized( + Iteration("xz", "expression=xz", "a11yStr=x times zed"), + Iteration("2xy", "expression=2yx", "a11yStr=2 y times x"), + Iteration("2√x", "expression=2√x", "a11yStr=2 times square root of x"), + Iteration("2sqrt(x)", "expression=2sqrt(x)", "a11yStr=2 times square root of x"), + Iteration("2(3)", "expression=2(3)", "a11yStr=2 times 3"), + Iteration("2(x)", "expression=2(x)", "a11yStr=2 times x"), + Iteration( + "2(x^3)", + "expression=2(x^3)", + "a11yStr=2 times open parenthesis x raised to the power of 3 close parenthesis" + ) + ) + fun testConvertToString_eng_impMult_nonLeftConst_orRightIsNotVarOrExp_returnsLeftTimesRightStr() { + // Allow for redundant single-term parentheses. + val exp = parseAlgebraicExpression(expression, errorCheckingMode = REQUIRED_ONLY) - @Test - fun test52() { - // TODO: do something with this test. - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("1*x") - assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 times x") + // If anything breaks up the format tested in the previous test (even if it's a group), then the + // multiplication is explicitly read out. + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo(a11yStr) } @Test - fun test53() { - // TODO: do something with this test. - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("1/x") - assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1 divided by x") - } + fun testConvertToString_eng_divisionAsFractions_oneDivTwo_returnsOneHalfString() { + val exp = parseAlgebraicExpression("1/2") - @Test - fun test54() { - // TODO: do something with this test. - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("1/x") + // 1/2 is a special case. assertThat(exp) .forHumanReadable(ENGLISH) - .convertsWithFractionsToStringThat() - .isEqualTo("the fraction with numerator 1 and denominator x") - } + .convertsWithFractionsToStringThat().isEqualTo("one half") + } + + @Test + @RunParameterized( + Iteration("0/1", "expression=0/1", "a11yStr=0 over 1"), + Iteration("1/1", "expression=1/1", "a11yStr=1 over 1"), + Iteration("0/2", "expression=0/2", "a11yStr=0 over 2"), + Iteration("2/2", "expression=2/2", "a11yStr=2 over 2"), + Iteration("0/3", "expression=0/3", "a11yStr=0 over 3"), + Iteration("1/3", "expression=1/3", "a11yStr=1 over 3"), + Iteration("2/3", "expression=2/3", "a11yStr=2 over 3"), + Iteration("3/3", "expression=3/3", "a11yStr=3 over 3"), + Iteration("4/3", "expression=4/3", "a11yStr=4 over 3"), + Iteration("5/3", "expression=5/3", "a11yStr=5 over 3"), + Iteration("6/3", "expression=6/3", "a11yStr=6 over 3"), + Iteration("5/9", "expression=5/9", "a11yStr=5 over 9"), + Iteration("19/3", "expression=19/3", "a11yStr=19 over 3"), + Iteration("2/17", "expression=2/17", "a11yStr=2 over 17") + ) + fun testConvertToString_eng_divisionAsFractions_smallIntegerFracs_returnsNumOverDenomString() { + val exp = parseAlgebraicExpression(expression) - @Test - fun test55() { - // TODO: do something with this test. - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("1+(1-x)") - assertThat(exp) - .forHumanReadable(ENGLISH) - .convertsToStringThat() - .isEqualTo("1 plus open parenthesis 1 minus x close parenthesis") + assertThat(exp).forHumanReadable(ENGLISH).convertsWithFractionsToStringThat().isEqualTo(a11yStr) } @Test - fun test56() { - // TODO: do something with this test. - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("2x") - assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("2 x") - } + @RunParameterized( + Iteration("1/1234", "expression=1/1234", "a11yStr=1 over 1,234"), + Iteration("1234/1", "expression=1234/1", "a11yStr=1,234 over 1"), + Iteration("1234/987654", "expression=1234/987654", "a11yStr=1,234 over 987,654") + ) + fun testConvertToString_eng_divisionAsFractions_largeIntegerFracs_returnsNumOverDenomString() { + val exp = parseAlgebraicExpression(expression) - @Test - fun test57() { - // TODO: do something with this test. - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("xy") - assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("x times y") + // Large numbers are read as part of the fraction. + assertThat(exp).forHumanReadable(ENGLISH).convertsWithFractionsToStringThat().isEqualTo(a11yStr) } @Test - fun test58() { - // TODO: do something with this test. - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("z") - assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("zed") - } + @RunParameterized( + Iteration("1/x", "expression=1/x", "a11yStr=1 over x"), + Iteration("1/z", "expression=1/z", "a11yStr=1 over zed"), + Iteration("x/2", "expression=x/2", "a11yStr=x over 2"), + Iteration("z/3", "expression=z/3", "a11yStr=zed over 3"), + Iteration("x/z", "expression=x/z", "a11yStr=x over zed") + ) + fun testConvertToString_eng_divisionAsFractions_fracsWithVariables_returnsNumOverDenomString() { + val exp = parseAlgebraicExpression(expression) + + // Variables are read as part of the fraction. + assertThat(exp).forHumanReadable(ENGLISH).convertsWithFractionsToStringThat().isEqualTo(a11yStr) + } + + @Test + @RunParameterized( + Iteration( + "x/√2", + "expression=x/√2", + "a11yStr=the fraction with numerator x and denominator square root of 2" + ), + Iteration( + "x/-2", + "expression=x/-2", + "a11yStr=the fraction with numerator x and denominator negative 2" + ), + Iteration( + "2/(1+2)", + "expression=2/(1+2)", + "a11yStr=the fraction with numerator 2 and denominator open parenthesis 1 plus 2 close" + + " parenthesis" + ), + // Nested fractions still cause the outer fraction to be read out the long way. + Iteration( + "2/(1/2)", + "expression=2/(1/2)", + "a11yStr=the fraction with numerator 2 and denominator open parenthesis one half close" + + " parenthesis" + ), + Iteration( + "2/(1/3)", + "expression=2/(1/3)", + "a11yStr=the fraction with numerator 2 and denominator open parenthesis 1 over 3 close" + + " parenthesis" + ), + Iteration( + "x/sqrt(y/3)", + "expression=x/sqrt(y/3)", + "a11yStr=the fraction with numerator x and denominator start square root y over 3 end" + + " square root" + ), + Iteration( + "3.14/x", "expression=3.14/x", "a11yStr=the fraction with numerator 3.14 and denominator x" + ), + Iteration( + "x/3.14", "expression=x/3.14", "a11yStr=the fraction with numerator x and denominator 3.14" + ) + ) + fun testConvertToString_eng_divisionAsFractions_fracWithComplexParts_returnsFracConstructStr() { + val exp = parseAlgebraicExpression(expression) - @Test - fun test59() { - // TODO: do something with this test. - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("2xz") - assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("2 x times zed") + // Verify that complex fractions are read out with more specificity. + assertThat(exp).forHumanReadable(ENGLISH).convertsWithFractionsToStringThat().isEqualTo(a11yStr) } @Test - fun test60() { - // TODO: do something with this test. - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("x^2") - assertThat(exp) - .forHumanReadable(ENGLISH) - .convertsToStringThat() - .isEqualTo("x raised to the power of 2") - } + @RunParameterized( + Iteration("1=2", "expression=1=2", "a11yStr=1 equals 2"), + Iteration("x=1", "expression=x=1", "a11yStr=x equals 1"), + Iteration("z=1", "expression=z=1", "a11yStr=zed equals 1"), + Iteration("2=x", "expression=2=x", "a11yStr=2 equals x"), + Iteration("2=z", "expression=2=z", "a11yStr=2 equals zed"), + Iteration("x=z", "expression=x=z", "a11yStr=x equals zed") + ) + fun testConvertToString_eng_simpleEquation_returnsLeftEqualsRightString() { + val eq = parseAlgebraicEquation(expression) - @Test - fun test61() { - // TODO: do something with this test. - val exp = parseAlgebraicExpressionSuccessfullyWithoutOptionalErrors("x^(1+x)") - assertThat(exp) - .forHumanReadable(ENGLISH) - .convertsToStringThat() - .isEqualTo("x raised to the power of open parenthesis 1 plus x close parenthesis") + assertThat(eq).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo(a11yStr) } @Test - fun test62() { - // TODO: do something with this test. - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("100000*2") - assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("100,000 times 2") - } + @RunParameterized( + Iteration("xyz", "expression=xyz", "a11yStr=x times y times zed"), + Iteration("1+x+x^2", "expression=1+x+x^2", "a11yStr=1 plus x plus x raised to the power of 2"), + Iteration( + "-3x^2+23x-14", + "expression=-3x^2+23x-14", + "a11yStr=negative 3 times x raised to the power of 2 plus 23 x minus 14" + ), + Iteration( + "y^2+xy+x^2", + "expression=y^2+xy+x^2", + "a11yStr=y raised to the power of 2 plus x times y plus x raised to the power of 2" + ) + ) + fun testConvertToString_eng_polynomialExpressions_returnsCorrectlyBuiltString() { + val exp = parseAlgebraicExpression(expression) + + // Polynomials should be read out correctly. + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo(a11yStr) + } + + @Test + @RunParameterized( + Iteration("z=xyz", "expression=z=xyz", "a11yStr=zed equals x times y times zed"), + Iteration( + "y=1+x+x^2", + "expression=y=1+x+x^2", + "a11yStr=y equals 1 plus x plus x raised to the power of 2" + ), + Iteration( + "-3x^2+23x-14=7y^3", + "expression=-3x^2+23x-14=7y^3", + "a11yStr=negative 3 times x raised to the power of 2 plus 23 x minus 14 equals 7 y raised" + + " to the power of 3" + ), + Iteration( + "sqrt(z)=y^2+xy+x^2", + "expression=sqrt(z)=y^2+xy+x^2", + "a11yStr=square root of zed equals y raised to the power of 2 plus x times y plus x raised" + + " to the power of 2" + ) + ) + fun testConvertToString_eng_polynomialEquations_returnsCorrectlyBuiltString() { + val eq = parseAlgebraicEquation(expression) + + // Polynomial equations should be read out correctly. + assertThat(eq).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo(a11yStr) + } + + @Test + @RunParameterized( + Iteration( + "(x^2+2x+1)/(x+1)", + "expression= ( x^2 + 2x + 1 ) /( x + 1)", + "a11yStr=open parenthesis x raised to the power of 2 plus 2 x plus 1 close parenthesis" + + " divided by open parenthesis x plus 1 close parenthesis" + ), + Iteration( + "(1/2)x", + "expression=(1/2) x", + "a11yStr=open parenthesis 1 divided by 2 close parenthesis times x" + ), + Iteration( + "(-27x^3)^(1/3)", + "expression=(\t-27x\n^3\r)^(1 / 3) ", + "a11yStr=open parenthesis negative 27 times x raised to the power of 3 close parenthesis" + + " raised to the power of open parenthesis 1 divided by 3 close parenthesis" + ), + Iteration( + "(4x^2)^(-1/2)", + "expression=( 4x ^ 2) ^ ( - 1 / 2 ) ", + "a11yStr=open parenthesis 4 x raised to the power of 2 close parenthesis raised to the" + + " power of open parenthesis negative 1 divided by 2 close parenthesis" + ), + Iteration( + "sqrt(sqrt(sqrt(x)+1))", + "expression=sqrt( sqrt( sqrt( x ) + 1 ) )", + "a11yStr=square root of start square root square root of x plus 1 end square root" + ), + Iteration( + "x-(1+(y-(2+z)))", + "expression= x - ( 1 + ( y - ( 2 + z )))", + "a11yStr=x minus open parenthesis 1 plus open parenthesis y minus open parenthesis 2 plus" + + " zed close parenthesis close parenthesis close parenthesis" + ), + Iteration( + "1/(2/(y+3/z))", + "expression=1 / ( 2 / ( y + 3/z ) )", + "a11yStr=1 divided by open parenthesis 2 divided by open parenthesis y plus 3 divided by" + + " zed close parenthesis close parenthesis" + ), + Iteration( + "x/y/z/2", "expression= x/ y/ z/ 2", "a11yStr=x divided by y divided by zed divided by 2" + ) + ) + fun testConvertToString_eng_complexNestedExpression_returnsCorrectlyBuiltString() { + val exp = parseAlgebraicExpression(expression) + + // The expression should correctly convert to a readable string, and all original whitespace + // should be ignored in the final rendered string. + assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo(a11yStr) + } + + @Test + @RunParameterized( + Iteration( + "(x^2+2x+1)/(x+1)", + "expression= ( x^2 + 2x + 1 ) /( x + 1)", + "a11yStr=the fraction with numerator open parenthesis x raised to the power of 2 plus 2 x" + + " plus 1 close parenthesis and denominator open parenthesis x plus 1 close parenthesis" + ), + Iteration( + "(1/2)x", "expression=(1/2) x", "a11yStr=open parenthesis one half close parenthesis times x" + ), + Iteration( + "(-27x^3)^(1/3)", + "expression=(\t-27x\n^3\r)^(1 / 3) ", + "a11yStr=open parenthesis negative 27 times x raised to the power of 3 close parenthesis" + + " raised to the power of open parenthesis 1 over 3 close parenthesis" + ), + Iteration( + "(4x^2)^(-1/2)", + "expression=( 4x ^ 2) ^ ( - 1 / 2 ) ", + "a11yStr=open parenthesis 4 x raised to the power of 2 close parenthesis raised to the" + + " power of open parenthesis the fraction with numerator negative 1 and denominator 2" + + " close parenthesis" + ), + Iteration( + "sqrt(sqrt(sqrt(x)+1))", + "expression=sqrt( sqrt( sqrt( x ) + 1 ) )", + "a11yStr=square root of start square root square root of x plus 1 end square root" + ), + Iteration( + "x-(1+(y-(2+z)))", + "expression= x - ( 1 + ( y - ( 2 + z )))", + "a11yStr=x minus open parenthesis 1 plus open parenthesis y minus open parenthesis 2 plus" + + " zed close parenthesis close parenthesis close parenthesis" + ), + Iteration( + "1/(2/(y+3/z))", + "expression=1 / ( 2 / ( y + 3/z ) )", + "a11yStr=the fraction with numerator 1 and denominator open parenthesis the fraction with" + + " numerator 2 and denominator open parenthesis y plus 3 over zed close parenthesis" + + " close parenthesis" + ), + Iteration( + "x/y/z/2", + "expression= x/ y/ z/ 2", + "a11yStr=the fraction with numerator the fraction with numerator x over y and denominator" + + " zed and denominator 2" + ) + ) + fun testConvertToString_eng_complexNestedExpression_divAsFracs_returnsCorrectlyBuiltString() { + val exp = parseAlgebraicExpression(expression) + + // The expression should correctly convert to a readable string, and all original whitespace + // should be ignored in the final rendered string. + assertThat(exp).forHumanReadable(ENGLISH).convertsWithFractionsToStringThat().isEqualTo(a11yStr) + } + + @Test + @RunParameterized( + Iteration( + "y=(x^2+2x+1)/(x+1)", + "expression= y = ( x^2 + 2x + 1 ) /( x + 1)", + "a11yStr=y equals open parenthesis x raised to the power of 2 plus 2 x plus 1 close" + + " parenthesis divided by open parenthesis x plus 1 close parenthesis" + ), + Iteration( + "(1/2)x=sqrt(x)", + "expression=(1/2) x =sqrt (x)", + "a11yStr=open parenthesis 1 divided by 2 close parenthesis times x equals square root of x" + ), + Iteration( + "-3x=(-27x^3)^(1/3)", + "expression=\n-\n3\nx\n=\n(\t-27x\n^3\r)^(1 / 3) ", + "a11yStr=negative 3 times x equals open parenthesis negative 27 times x raised to the power" + + " of 3 close parenthesis raised to the power of open parenthesis 1 divided by 3 close" + + " parenthesis" + ), + Iteration( + "(4x^2)^(-1/2)=1+x", + "expression=( 4x ^ 2) ^ ( - 1 / 2 ) =1 + x ", + "a11yStr=open parenthesis 4 x raised to the power of 2 close parenthesis raised to the" + + " power of open parenthesis negative 1 divided by 2 close parenthesis equals 1 plus x" + ), + Iteration( + "sqrt(sqrt(sqrt(x)+1))=1/2", + "expression=sqrt( sqrt( sqrt( x ) + 1 ) ) = 1 / 2", + "a11yStr=square root of start square root square root of x plus 1 end square root equals 1" + + " divided by 2" + ), + Iteration( + "xy+x+y=x-(1+(y-(2+z)))", + "expression=xy+x+y=x - ( 1 + ( y - ( 2 + z )))", + "a11yStr=x times y plus x plus y equals x minus open parenthesis 1 plus open parenthesis y" + + " minus open parenthesis 2 plus zed close parenthesis close parenthesis close parenthesis" + ), + Iteration( + "x=1/(2/(y+3/z))", + "expression= x = 1 / ( 2 / ( y + 3/z ) )", + "a11yStr=x equals 1 divided by open parenthesis 2 divided by open parenthesis y plus 3" + + " divided by zed close parenthesis close parenthesis" + ), + Iteration( + "x/y/z/2=z", + "expression= x/ y/ z/ 2=z", + "a11yStr=x divided by y divided by zed divided by 2 equals zed" + ) + ) + fun testConvertToString_eng_complexNestedEquations_returnsCorrectlyBuiltString() { + val eq = parseAlgebraicEquation(expression) + + // The equation should correctly convert to a readable string, and all original whitespace + // should be ignored in the final rendered string. + assertThat(eq).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo(a11yStr) + } + + @Test + @RunParameterized( + Iteration( + "y=(x^2+2x+1)/(x+1)", + "expression= y = ( x^2 + 2x + 1 ) /( x + 1)", + "a11yStr=y equals the fraction with numerator open parenthesis x raised to the power of 2" + + " plus 2 x plus 1 close parenthesis and denominator open parenthesis x plus 1 close" + + " parenthesis" + ), + Iteration( + "(1/2)x=sqrt(x)", + "expression=(1/2) x =sqrt (x)", + "a11yStr=open parenthesis one half close parenthesis times x equals square root of x" + ), + Iteration( + "-3x=(-27x^3)^(1/3)", + "expression=\n-\n3\nx\n=\n(\t-27x\n^3\r)^(1 / 3) ", + "a11yStr=negative 3 times x equals open parenthesis negative 27 times x raised to the power" + + " of 3 close parenthesis raised to the power of open parenthesis 1 over 3 close parenthesis" + ), + Iteration( + "(4x^2)^(-1/2)=1+x", + "expression=( 4x ^ 2) ^ ( - 1 / 2 ) =1 + x ", + "a11yStr=open parenthesis 4 x raised to the power of 2 close parenthesis raised to the" + + " power of open parenthesis the fraction with numerator negative 1 and denominator 2" + + " close parenthesis equals 1 plus x" + ), + Iteration( + "sqrt(sqrt(sqrt(x)+1))=1/2", + "expression=sqrt( sqrt( sqrt( x ) + 1 ) ) = 1 / 2", + "a11yStr=square root of start square root square root of x plus 1 end square root equals" + + " one half" + ), + Iteration( + "xy+x+y=x-(1+(y-(2+z)))", + "expression=xy+x+y=x - ( 1 + ( y - ( 2 + z )))", + "a11yStr=x times y plus x plus y equals x minus open parenthesis 1 plus open parenthesis y" + + " minus open parenthesis 2 plus zed close parenthesis close parenthesis close parenthesis" + ), + Iteration( + "x=1/(2/(y+3/z))", + "expression= x = 1 / ( 2 / ( y + 3/z ) )", + "a11yStr=x equals the fraction with numerator 1 and denominator open parenthesis the" + + " fraction with numerator 2 and denominator open parenthesis y plus 3 over zed close" + + " parenthesis close parenthesis" + ), + Iteration( + "x/y/z/2=z", + "expression= x/ y/ z/ 2=z", + "a11yStr=the fraction with numerator the fraction with numerator x over y and denominator" + + " zed and denominator 2 equals zed" + ) + ) + fun testConvertToString_eng_complexNestedEquations_divAsFracs_returnsCorrectlyBuiltString() { + val eq = parseAlgebraicEquation(expression) + + // The equation should correctly convert to a readable string, and all original whitespace + // should be ignored in the final rendered string. + assertThat(eq).forHumanReadable(ENGLISH).convertsWithFractionsToStringThat().isEqualTo(a11yStr) + } + + // This & the next test are implementing cases defined in the doc: + // https://docs.google.com/document/d/1P-dldXQ08O-02ZRG978paiWOSz0dsvcKpDgiV_rKH_Y/edit#. + @Test + @RunParameterized( + Iteration( + "(x + 6)/(x - 4)", + "expression=(x + 6)/(x - 4)", + "a11yStr=the fraction with numerator open parenthesis x plus 6 close parenthesis and" + + " denominator open parenthesis x minus 4 close parenthesis" + ), + Iteration( + "4*(x)^(2)+20x", + "expression=4*(x)^(2)+20x", + "a11yStr=4 times x raised to the power of 2 plus 20 x" + ), + Iteration("3+x-5", "expression=3+x-5", "a11yStr=3 plus x minus 5"), + Iteration("Z+A-Z", "expression=Z+A-Z", "a11yStr=Zed plus A minus Zed"), + Iteration("6C - 5A -1", "expression=6C - 5A -1", "a11yStr=6 C minus 5 A minus 1"), + Iteration("5*Z-w", "expression=5*Z-w", "a11yStr=5 times Zed minus w"), + Iteration("L*S-3S+L", "expression=L*S-3S+L", "a11yStr=L times S minus 3 S plus L"), + Iteration( + "2*(2+6+3+4)", + "expression=2*(2+6+3+4)", + "a11yStr=2 times open parenthesis 2 plus 6 plus 3 plus 4 close parenthesis" + ), + Iteration("sqrt(64)", "expression=sqrt(64)", "a11yStr=square root of 64"), + Iteration( + "√(a+b)", + "expression=√(a+b)", + "a11yStr=start square root open parenthesis a plus b close parenthesis end square root" + ), + Iteration( + "3 * 10^-5", "expression=3 * 10^-5", "a11yStr=3 times 10 raised to the power of negative 5" + ), + Iteration( + "((x+2y) + 5*(a - 2b) + z)", + "expression=((x+2y) + 5*(a - 2b) + z)", + "a11yStr=open parenthesis open parenthesis x plus 2 y close parenthesis plus 5 times open" + + " parenthesis a minus 2 b close parenthesis plus zed close parenthesis" + ) + ) + fun testConvertToString_eng_assortedExpressionsFromPrd_returnsCorrectlyComputedString() { + // Some of the expressions include cases that would normally result in errors. + val exp = parseAlgebraicExpression(expression, errorCheckingMode = REQUIRED_ONLY) - @Test - fun test63() { - // TODO: do something with this test. - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("sqrt(2)") - assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("square root of 2") + assertThat(exp).forHumanReadable(ENGLISH).convertsWithFractionsToStringThat().isEqualTo(a11yStr) } @Test - fun test64() { - // TODO: do something with this test. - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("sqrt(x)") - assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("square root of x") - } + @RunParameterized( + Iteration( + "3x^2 + 4y = 62", + "expression=3x^2 + 4y = 62", + "a11yStr=3 x raised to the power of 2 plus 4 y equals 62" + ) + ) + fun testConvertToString_eng_assortedEquationsFromPrd_returnsCorrectlyComputedString() { + val eq = parseAlgebraicEquation(expression) - @Test - fun test65() { - // TODO: do something with this test. - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("√2") - assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("square root of 2") + assertThat(eq).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo(a11yStr) } @Test - fun test66() { - // TODO: do something with this test. - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("√x") - assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("square root of x") - } + fun testConvertToString_eng_rationalConstant_returnsNull() { + val exp = MathExpression.newBuilder().apply { + constant = ONE_HALF + }.build() - @Test - fun test67() { - // TODO: do something with this test. - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("sqrt(1+2)") - assertThat(exp) - .forHumanReadable(ENGLISH) - .convertsToStringThat() - .isEqualTo("start square root 1 plus 2 end square root") + // The conversion should fail since the expression includes a rational real (which aren't yet + // supported). + assertThat(exp).forHumanReadable(ENGLISH).doesNotConvertToString() } @Test - fun test68() { - // TODO: do something with this test. - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("sqrt(1+x)") - assertThat(exp) - .forHumanReadable(ENGLISH) - .convertsToStringThat() - .isEqualTo("start square root 1 plus x end square root") - } + fun testConvertToString_eng_invalidConstant_returnsNull() { + val exp = MathExpression.newBuilder().apply { + constant = Real.getDefaultInstance() + }.build() - @Test - fun test69() { - // TODO: do something with this test. - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("√(1+x)") - assertThat(exp) - .forHumanReadable(ENGLISH) - .convertsToStringThat() - .isEqualTo("start square root open parenthesis 1 plus x close parenthesis end square root") - } - - @Test - fun test70() { - // TODO: do something with this test. - val singularOrdinalNames = mapOf( - 1 to "oneth", - 2 to "half", - 3 to "third", - 4 to "fourth", - 5 to "fifth", - 6 to "sixth", - 7 to "seventh", - 8 to "eighth", - 9 to "ninth", - 10 to "tenth", - ) - val pluralOrdinalNames = mapOf( - 1 to "oneths", - 2 to "halves", - 3 to "thirds", - 4 to "fourths", - 5 to "fifths", - 6 to "sixths", - 7 to "sevenths", - 8 to "eighths", - 9 to "ninths", - 10 to "tenths", - ) - for (denominatorToCheck in 1..10) { - for (numeratorToCheck in 0..denominatorToCheck) { - val exp = - parseAlgebraicExpressionSuccessfullyWithAllErrors("$numeratorToCheck/$denominatorToCheck") - - val ordinalName = - if (numeratorToCheck == 1) { - singularOrdinalNames.getValue(denominatorToCheck) - } else pluralOrdinalNames.getValue(denominatorToCheck) - assertThat(exp) - .forHumanReadable(ENGLISH) - .convertsWithFractionsToStringThat() - .isEqualTo("$numeratorToCheck $ordinalName") - } - } + // The conversion should fail since the expression includes an invalid real constant. + assertThat(exp).forHumanReadable(ENGLISH).doesNotConvertToString() } @Test - fun test71() { - // TODO: do something with this test. - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("1") - assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("1") - } + fun testConvertToString_eng_invalidBinaryOp_returnsNull() { + val exp = MathExpression.newBuilder().apply { + binaryOperation = MathBinaryOperation.getDefaultInstance() + }.build() - @Test - fun test72() { - // TODO: do something with this test. - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("x(5-y)") - assertThat(exp) - .forHumanReadable(ENGLISH) - .convertsToStringThat() - .isEqualTo("x times open parenthesis 5 minus y close parenthesis") + // The conversion should fail since the expression includes an invalid binary operation. + assertThat(exp).forHumanReadable(ENGLISH).doesNotConvertToString() } @Test - fun test73() { - // TODO: do something with this test. - val eq = parseAlgebraicEquationSuccessfullyWithAllErrors("x=1/y") - assertThat(eq) - .forHumanReadable(ENGLISH) - .convertsToStringThat() - .isEqualTo("x equals 1 divided by y") - } + fun testConvertToString_eng_invalidUnaryOp_returnsNull() { + val exp = MathExpression.newBuilder().apply { + unaryOperation = MathUnaryOperation.getDefaultInstance() + }.build() - @Test - fun test74() { - // TODO: do something with this test. - val eq = parseAlgebraicEquationSuccessfullyWithAllErrors("x=1/2") - assertThat(eq) - .forHumanReadable(ENGLISH) - .convertsToStringThat() - .isEqualTo("x equals 1 divided by 2") + // The conversion should fail since the expression includes an invalid unary operation. + assertThat(exp).forHumanReadable(ENGLISH).doesNotConvertToString() } @Test - fun test75() { - // TODO: do something with this test. - val eq = parseAlgebraicEquationSuccessfullyWithAllErrors("x=1/y") - assertThat(eq) - .forHumanReadable(ENGLISH) - .convertsWithFractionsToStringThat() - .isEqualTo("x equals the fraction with numerator 1 and denominator y") - } + fun testConvertToString_eng_invalidFunctionType_returnsNull() { + val exp = MathExpression.newBuilder().apply { + functionCall = MathFunctionCall.getDefaultInstance() + }.build() - @Test - fun test76() { - // TODO: do something with this test. - val eq = parseAlgebraicEquationSuccessfullyWithAllErrors("x=1/2") - assertThat(eq) - .forHumanReadable(ENGLISH) - .convertsWithFractionsToStringThat() - .isEqualTo("x equals 1 half") + // The conversion should fail since the expression includes an invalid function call. + assertThat(exp).forHumanReadable(ENGLISH).doesNotConvertToString() } @Test - fun test77() { - // TODO: do something with this test. - // Tests from examples in the PRD - val eq = parseAlgebraicEquationSuccessfullyWithAllErrors("3x^2+4y=62") - assertThat(eq) - .forHumanReadable(ENGLISH) - .convertsToStringThat() - .isEqualTo("3 x raised to the power of 2 plus 4 y equals 62") - } + fun testConvertToString_eng_nestedDefaultExp_returnsNull() { + val exp = MathExpression.newBuilder().apply { + group = MathExpression.getDefaultInstance() + }.build() - @Test - fun test78() { - // TODO: do something with this test. - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("(x+6)/(x-4)") - assertThat(exp) - .forHumanReadable(ENGLISH) - .convertsWithFractionsToStringThat() - .isEqualTo( - "the fraction with numerator open parenthesis x plus 6 close parenthesis and denominator" + - " open parenthesis x minus 4 close parenthesis" - ) + // The conversion should fail since the expression includes an invalid nested expression. + assertThat(exp).forHumanReadable(ENGLISH).doesNotConvertToString() } @Test - fun test79() { - // TODO: do something with this test. - val exp = parseAlgebraicExpressionSuccessfullyWithoutOptionalErrors("4*(x)^(2)+20x") - assertThat(exp) - .forHumanReadable(ENGLISH) - .convertsToStringThat() - .isEqualTo("4 times x raised to the power of 2 plus 20 x") - } + fun testConvertToString_eng_nestedInvalidBinaryOp_returnsNull() { + val exp = MathExpression.newBuilder().apply { + group = MathExpression.newBuilder().apply { + binaryOperation = MathBinaryOperation.getDefaultInstance() + }.build() + }.build() - @Test - fun test80() { - // TODO: do something with this test. - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("3+x-5") - assertThat(exp).forHumanReadable(ENGLISH).convertsToStringThat().isEqualTo("3 plus x minus 5") + // The conversion should fail since the expression includes an invalid nested expression. + assertThat(exp).forHumanReadable(ENGLISH).doesNotConvertToString() } @Test - fun test81() { - // TODO: do something with this test. - val exp = - parseAlgebraicExpressionSuccessfullyWithAllErrors( - "Z+A-Z", allowedVariables = listOf("A", "Z") - ) - assertThat(exp).forHumanReadable(ENGLISH) - .convertsToStringThat() - .isEqualTo("Zed plus A minus Zed") - } + fun testConvertToString_eng_nestedInvalidUnaryOp_returnsNull() { + val exp = MathExpression.newBuilder().apply { + group = MathExpression.newBuilder().apply { + unaryOperation = MathUnaryOperation.getDefaultInstance() + }.build() + }.build() - @Test - fun test82() { - // TODO: do something with this test. - val exp = - parseAlgebraicExpressionSuccessfullyWithAllErrors( - "6C-5A-1", allowedVariables = listOf("A", "C") - ) - assertThat(exp) - .forHumanReadable(ENGLISH) - .convertsToStringThat() - .isEqualTo("6 C minus 5 A minus 1") + // The conversion should fail since the expression includes an invalid nested expression. + assertThat(exp).forHumanReadable(ENGLISH).doesNotConvertToString() } @Test - fun test83() { - // TODO: do something with this test. - val exp = - parseAlgebraicExpressionSuccessfullyWithAllErrors( - "5*Z-w", allowedVariables = listOf("Z", "w") - ) - assertThat(exp) - .forHumanReadable(ENGLISH) - .convertsToStringThat() - .isEqualTo("5 times Zed minus w") - } + fun testConvertToString_eng_nestedInvalidFunctionType_returnsNull() { + val exp = MathExpression.newBuilder().apply { + group = MathExpression.newBuilder().apply { + functionCall = MathFunctionCall.getDefaultInstance() + }.build() + }.build() - @Test - fun test84() { - // TODO: do something with this test. - val exp = - parseAlgebraicExpressionSuccessfullyWithAllErrors( - "L*S-3S+L", allowedVariables = listOf("L", "S") - ) - assertThat(exp) - .forHumanReadable(ENGLISH) - .convertsToStringThat() - .isEqualTo("L times S minus 3 S plus L") + // The conversion should fail since the expression includes an invalid nested expression. + assertThat(exp).forHumanReadable(ENGLISH).doesNotConvertToString() } @Test - fun test85() { - // TODO: do something with this test. - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("2*(2+6+3+4)") - assertThat(exp) - .forHumanReadable(ENGLISH) - .convertsToStringThat() - .isEqualTo("2 times open parenthesis 2 plus 6 plus 3 plus 4 close parenthesis") - } + fun testConvertToString_eq_withLeftInvalidExp_returnsNull() { + val validExp = MathExpression.newBuilder().apply { + constant = ONE_HALF + }.build() + val invalidExp = MathExpression.getDefaultInstance() + val eq = MathEquation.newBuilder().apply { + leftSide = invalidExp + rightSide = validExp + }.build() - @Test - fun test86() { - // TODO: do something with this test. - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("sqrt(64)") - assertThat(exp) - .forHumanReadable(ENGLISH) - .convertsToStringThat() - .isEqualTo("square root of 64") + // Both sides of the equation must be valid. + assertThat(eq).forHumanReadable(ENGLISH).doesNotConvertToString() } @Test - fun test87() { - // TODO: do something with this test. - val exp = - parseAlgebraicExpressionSuccessfullyWithAllErrors( - "√(a+b)", allowedVariables = listOf("a", "b") - ) - assertThat(exp) - .forHumanReadable(ENGLISH) - .convertsToStringThat() - .isEqualTo("start square root open parenthesis a plus b close parenthesis end square root") - } + fun testConvertToString_eq_withRightInvalidExp_returnsNull() { + val validExp = MathExpression.newBuilder().apply { + constant = ONE_HALF + }.build() + val invalidExp = MathExpression.getDefaultInstance() + val eq = MathEquation.newBuilder().apply { + leftSide = validExp + rightSide = invalidExp + }.build() - @Test - fun test88() { - // TODO: do something with this test. - val exp = parseAlgebraicExpressionSuccessfullyWithAllErrors("3*10^-5") - assertThat(exp) - .forHumanReadable(ENGLISH) - .convertsToStringThat() - .isEqualTo("3 times 10 raised to the power of negative 5") + // Both sides of the equation must be valid. + assertThat(eq).forHumanReadable(ENGLISH).doesNotConvertToString() } - @Test - fun test89() { - // TODO: do something with this test. - val exp = - parseAlgebraicExpressionSuccessfullyWithoutOptionalErrors( - "((x+2y)+5*(a-2b)+z)", allowedVariables = listOf("x", "y", "a", "b", "z") - ) - assertThat(exp) - .forHumanReadable(ENGLISH) - .convertsToStringThat() - .isEqualTo( - "open parenthesis open parenthesis x plus 2 y close parenthesis plus 5 times open" + - " parenthesis a minus 2 b close parenthesis plus zed close parenthesis" - ) + @Before + fun setUp() { + setUpTestApplicationComponent() } private fun MathExpressionSubject.forHumanReadable( @@ -922,10 +1230,7 @@ class MathExpressionAccessibilityUtilTest { // TODO(#89): Move this to a common test application component. @Singleton - @Component( - modules = [ - ] - ) + @Component interface TestApplicationComponent { @Component.Builder interface Builder { @@ -951,72 +1256,29 @@ class MathExpressionAccessibilityUtilTest { } private companion object { - // TODO: finalize this API. - - private fun parseNumericExpressionSuccessfullyWithAllErrors( - expression: String - ): MathExpression { - val result = parseNumericExpressionInternal(expression, ErrorCheckingMode.ALL_ERRORS) - return (result as MathParsingResult.Success).result - } - - private fun parseNumericExpressionSuccessfullyWithoutOptionalErrors( - expression: String - ): MathExpression { - val result = - parseNumericExpressionInternal( - expression, ErrorCheckingMode.REQUIRED_ONLY - ) - return (result as MathParsingResult.Success).result - } - - private fun parseNumericExpressionInternal( - expression: String, - errorCheckingMode: ErrorCheckingMode - ): MathParsingResult { - return MathExpressionParser.parseNumericExpression(expression, errorCheckingMode) - } - - private fun parseAlgebraicExpressionSuccessfullyWithAllErrors( - expression: String, - allowedVariables: List = listOf("x", "y", "z") - ): MathExpression { - val result = - parseAlgebraicExpressionInternal(expression, ErrorCheckingMode.ALL_ERRORS, allowedVariables) - return (result as MathParsingResult.Success).result - } - - private fun parseAlgebraicExpressionSuccessfullyWithoutOptionalErrors( + private fun parseAlgebraicExpression( expression: String, - allowedVariables: List = listOf("x", "y", "z") + allowedVariables: List = listOf("x", "y", "z"), + errorCheckingMode: ErrorCheckingMode = ALL_ERRORS ): MathExpression { - val result = - parseAlgebraicExpressionInternal( - expression, ErrorCheckingMode.REQUIRED_ONLY, allowedVariables - ) - return (result as MathParsingResult.Success).result - } - - private fun parseAlgebraicExpressionInternal( - expression: String, - errorCheckingMode: ErrorCheckingMode, - allowedVariables: List = listOf("x", "y", "z") - ): MathParsingResult { return MathExpressionParser.parseAlgebraicExpression( expression, allowedVariables, errorCheckingMode - ) + ).getExpectedSuccess() } - private fun parseAlgebraicEquationSuccessfullyWithAllErrors( + private fun parseAlgebraicEquation( expression: String, - allowedVariables: List = listOf("x", "y", "z") ): MathEquation { - val result = - MathExpressionParser.parseAlgebraicEquation( - expression, allowedVariables, - ErrorCheckingMode.ALL_ERRORS - ) - return (result as MathParsingResult.Success).result + return MathExpressionParser.parseAlgebraicEquation( + expression, + allowedVariables = listOf("x", "y", "z"), + errorCheckingMode = ALL_ERRORS + ).getExpectedSuccess() + } + + private inline fun MathParsingResult.getExpectedSuccess(): T { + assertThat(this).isInstanceOf(MathParsingResult.Success::class.java) + return (this as MathParsingResult.Success).result } } } From 4c163bf95605c2ac42c72934d5e15b52e0ce9f2b Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Mon, 7 Feb 2022 22:36:01 -0800 Subject: [PATCH 123/162] Clean up + KDocs + exemption. --- .../android/app/testing/activity/BUILD.bazel | 1 + .../app/testing/activity/TestActivity.kt | 4 + .../translation/AppLanguageResourceHandler.kt | 6 + .../android/app/utility/math/BUILD.bazel | 15 +- .../math/MathExpressionAccessibilityUtil.kt | 225 ++++++++++++------ .../main/res/values/untranslated_strings.xml | 17 ++ .../AppLanguageResourceHandlerTest.kt | 33 +++ .../android/app/utility/math/BUILD.bazel | 16 ++ .../MathExpressionAccessibilityUtilTest.kt | 119 ++++++++- .../domain/locale/DisplayLocaleImpl.kt | 6 + .../domain/locale/DisplayLocaleImplTest.kt | 30 +++ .../file_content_validation_checks.textproto | 1 + .../oppia/android/util/locale/OppiaLocale.kt | 19 ++ 13 files changed, 410 insertions(+), 82 deletions(-) diff --git a/app/src/main/java/org/oppia/android/app/testing/activity/BUILD.bazel b/app/src/main/java/org/oppia/android/app/testing/activity/BUILD.bazel index 137513d16c5..eb5ef2049c6 100644 --- a/app/src/main/java/org/oppia/android/app/testing/activity/BUILD.bazel +++ b/app/src/main/java/org/oppia/android/app/testing/activity/BUILD.bazel @@ -18,5 +18,6 @@ kt_android_library( deps = [ "//app/src/main/java/org/oppia/android/app/activity:activity_intent_factories_shim", "//app/src/main/java/org/oppia/android/app/activity:injectable_app_compat_activity", + "//app/src/main/java/org/oppia/android/app/utility/math:math_expression_accessibility_util", ], ) diff --git a/app/src/main/java/org/oppia/android/app/testing/activity/TestActivity.kt b/app/src/main/java/org/oppia/android/app/testing/activity/TestActivity.kt index 05307f02df9..72be82faf23 100644 --- a/app/src/main/java/org/oppia/android/app/testing/activity/TestActivity.kt +++ b/app/src/main/java/org/oppia/android/app/testing/activity/TestActivity.kt @@ -8,6 +8,7 @@ import org.oppia.android.app.translation.AppLanguageResourceHandler import org.oppia.android.app.translation.AppLanguageWatcherMixin import org.oppia.android.app.utility.datetime.DateTimeUtil import javax.inject.Inject +import org.oppia.android.app.utility.math.MathExpressionAccessibilityUtil // TODO(#3830): Migrate all test activities over to using this test activity & make this closed. /** @@ -36,6 +37,9 @@ open class TestActivity : InjectableAppCompatActivity() { @Inject lateinit var appLanguageWatcherMixin: AppLanguageWatcherMixin + @Inject + lateinit var mathExpressionAccessibilityUtil: MathExpressionAccessibilityUtil + override fun attachBaseContext(newBase: Context?) { super.attachBaseContext(newBase) (activityComponent as Injector).inject(this) diff --git a/app/src/main/java/org/oppia/android/app/translation/AppLanguageResourceHandler.kt b/app/src/main/java/org/oppia/android/app/translation/AppLanguageResourceHandler.kt index 8dd7787dc15..4887f41175a 100644 --- a/app/src/main/java/org/oppia/android/app/translation/AppLanguageResourceHandler.kt +++ b/app/src/main/java/org/oppia/android/app/translation/AppLanguageResourceHandler.kt @@ -115,6 +115,12 @@ class AppLanguageResourceHandler @Inject constructor( } } + /** See [OppiaLocale.DisplayLocale.formatLong] for specific behavior. */ + fun formatLong(value: Long): String = getDisplayLocale().formatLong(value) + + /** See [OppiaLocale.DisplayLocale.formatDouble] for specific behavior. */ + fun formatDouble(value: Double): String = getDisplayLocale().formatDouble(value) + /** See [OppiaLocale.DisplayLocale.computeDateString]. */ fun computeDateString(timestampMillis: Long): String = getDisplayLocale().computeDateString(timestampMillis) diff --git a/app/src/main/java/org/oppia/android/app/utility/math/BUILD.bazel b/app/src/main/java/org/oppia/android/app/utility/math/BUILD.bazel index 5b5ee7cb433..12d6ae08213 100644 --- a/app/src/main/java/org/oppia/android/app/utility/math/BUILD.bazel +++ b/app/src/main/java/org/oppia/android/app/utility/math/BUILD.bazel @@ -5,14 +5,27 @@ General purposes utilities corresponding to displaying math expressions & constr load("@dagger//:workspace_defs.bzl", "dagger_rules") load("@io_bazel_rules_kotlin//kotlin:kotlin.bzl", "kt_android_library") +# Resource shim needed so that MathExpressionAccessibilityUtil can build in both Gradle & Bazel. +genrule( + name = "update_MathExpressionAccessibilityUtil", + srcs = ["MathExpressionAccessibilityUtil.kt"], + outs = ["MathExpressionAccessibilityUtil_updated.kt"], + cmd = """ + cat $(SRCS) | + sed 's/import org.oppia.android.R/import org.oppia.android.app.R/g' > $(OUTS) + """, +) + kt_android_library( name = "math_expression_accessibility_util", srcs = [ - "MathExpressionAccessibilityUtil.kt", + "MathExpressionAccessibilityUtil_updated.kt", ], visibility = ["//app:app_visibility"], deps = [ ":dagger", + "//app:resources", + "//app/src/main/java/org/oppia/android/app/translation:app_language_resource_handler", "//model/src/main/proto:languages_java_proto_lite", "//model/src/main/proto:math_java_proto_lite", "//utility/src/main/java/org/oppia/android/util/math:extensions", diff --git a/app/src/main/java/org/oppia/android/app/utility/math/MathExpressionAccessibilityUtil.kt b/app/src/main/java/org/oppia/android/app/utility/math/MathExpressionAccessibilityUtil.kt index efd658d12b1..5001922b9ab 100644 --- a/app/src/main/java/org/oppia/android/app/utility/math/MathExpressionAccessibilityUtil.kt +++ b/app/src/main/java/org/oppia/android/app/utility/math/MathExpressionAccessibilityUtil.kt @@ -1,6 +1,9 @@ package org.oppia.android.app.utility.math +import org.oppia.android.R +import javax.inject.Inject import org.oppia.android.app.model.MathBinaryOperation +import org.oppia.android.app.model.MathBinaryOperation.Operator as BinaryOperator import org.oppia.android.app.model.MathBinaryOperation.Operator.ADD import org.oppia.android.app.model.MathBinaryOperation.Operator.DIVIDE import org.oppia.android.app.model.MathBinaryOperation.Operator.EXPONENTIATE @@ -18,6 +21,7 @@ import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.VARIABLE import org.oppia.android.app.model.MathFunctionCall.FunctionType import org.oppia.android.app.model.MathFunctionCall.FunctionType.FUNCTION_UNSPECIFIED import org.oppia.android.app.model.MathFunctionCall.FunctionType.SQUARE_ROOT +import org.oppia.android.app.model.MathUnaryOperation.Operator as UnaryOperator import org.oppia.android.app.model.MathUnaryOperation.Operator.NEGATE import org.oppia.android.app.model.MathUnaryOperation.Operator.POSITIVE import org.oppia.android.app.model.OppiaLanguage @@ -30,18 +34,36 @@ import org.oppia.android.app.model.OppiaLanguage.LANGUAGE_UNSPECIFIED import org.oppia.android.app.model.OppiaLanguage.PORTUGUESE import org.oppia.android.app.model.OppiaLanguage.UNRECOGNIZED import org.oppia.android.app.model.Real.RealTypeCase.INTEGER -import java.text.NumberFormat -import java.util.Locale -import javax.inject.Inject -import org.oppia.android.app.model.MathBinaryOperation.Operator as BinaryOperator -import org.oppia.android.app.model.MathUnaryOperation.Operator as UnaryOperator import org.oppia.android.app.model.Real.RealTypeCase.IRRATIONAL import org.oppia.android.app.model.Real.RealTypeCase.RATIONAL import org.oppia.android.app.model.Real.RealTypeCase.REALTYPE_NOT_SET +import org.oppia.android.app.translation.AppLanguageResourceHandler -class MathExpressionAccessibilityUtil @Inject constructor() { - // TODO: document that rationals aren't supported, and that irrationals are rounded during formatting (and maybe that ints are also formatted?). - +/** + * Utility for computing an accessibility string for screenreaders to be able to read out parsed + * [MathExpression]s and [MathEquation]s. + * + * See [convertToHumanReadableString] for the specific function. + */ +class MathExpressionAccessibilityUtil @Inject constructor( + private val resourceHandler: AppLanguageResourceHandler +) { + /** + * Returns the human-readable string (for screenreaders) representation of the specified + * [expression]. + * + * Note that rational ``Real``s are specifically not supported and will result in a null value + * being returned (for custom expression constructs should use a division operation and set + * [divAsFraction] to true. Further, irrational reals may be rounded during formatting if they are + * very large or have long decimals (for an easier time reading). Numbers will be formatted + * according to the user's locale. + * + * @param expression the expression to convert + * @param language the target language for which the expression should be generated + * @param divAsFraction whether divisions should be read out as fractions rather than divisions + * @return the human-readable string, or null if the expression is malformed or the target + * language is unsupported + */ fun convertToHumanReadableString( expression: MathExpression, language: OppiaLanguage, @@ -54,6 +76,13 @@ class MathExpressionAccessibilityUtil @Inject constructor() { } } + /** + * Returns the human-readable string (for screenreaders) representation of the specified + * [equation]. + * + * This function behaves in the same way as the [MathExpression] version of + * [convertToHumanReadableString]--see that method's documentation for more details. + */ fun convertToHumanReadableString( equation: MathEquation, language: OppiaLanguage, @@ -66,84 +95,138 @@ class MathExpressionAccessibilityUtil @Inject constructor() { } } - private companion object { - // TODO: move these to the UI layer & have them utilize non-translatable strings. - private val numberFormat by lazy { NumberFormat.getNumberInstance(Locale.US) } + private fun MathEquation.toHumanReadableEnglishString(divAsFraction: Boolean): String? { + val lhsStr = leftSide.toHumanReadableEnglishString(divAsFraction) + val rhsStr = rightSide.toHumanReadableEnglishString(divAsFraction) + return if (lhsStr != null && rhsStr != null) { + resourceHandler.getStringInLocaleWithWrapping( + R.string.math_accessibility_a_equals_b, lhsStr, rhsStr + ) + } else null + } - private fun MathEquation.toHumanReadableEnglishString(divAsFraction: Boolean): String? { - val lhsStr = leftSide.toHumanReadableEnglishString(divAsFraction) - val rhsStr = rightSide.toHumanReadableEnglishString(divAsFraction) - return if (lhsStr != null && rhsStr != null) "$lhsStr equals $rhsStr" else null - } + private fun MathExpression.toHumanReadableEnglishString(divAsFraction: Boolean): String? { + // Reference: + // https://docs.google.com/document/d/1P-dldXQ08O-02ZRG978paiWOSz0dsvcKpDgiV_rKH_Y/view. - private fun MathExpression.toHumanReadableEnglishString(divAsFraction: Boolean): String? { - // Reference: - // https://docs.google.com/document/d/1P-dldXQ08O-02ZRG978paiWOSz0dsvcKpDgiV_rKH_Y/view. - return when (expressionTypeCase) { - CONSTANT -> when (constant.realTypeCase) { - IRRATIONAL -> numberFormat.format(constant.irrational) - INTEGER -> numberFormat.format(constant.integer.toLong()) - // Note that rational types should not actually be encountered in raw expressions, so - // there's no explicit support for reading them out. - RATIONAL, REALTYPE_NOT_SET, null -> null - } - VARIABLE -> when (variable) { - "z" -> "zed" - "Z" -> "Zed" - else -> variable + // Note that extra bidi wrapping is occurring here since there's not an obvious way to wrap "at + // the end" for non-equations. + return when (expressionTypeCase) { + CONSTANT -> when (constant.realTypeCase) { + IRRATIONAL -> resourceHandler.formatDouble(constant.irrational) + INTEGER -> resourceHandler.formatLong(constant.integer.toLong()) + // Note that rational types should not actually be encountered in raw expressions, so + // there's no explicit support for reading them out. + RATIONAL, REALTYPE_NOT_SET, null -> null + } + VARIABLE -> when (variable) { + "z", "Z" -> { + val zed = + resourceHandler.getStringInLocale(R.string.math_accessibility_part_zed) + if (variable == "Z") { + resourceHandler.capitalizeForHumans(zed) + } else zed } - BINARY_OPERATION -> { - val lhs = binaryOperation.leftOperand - val rhs = binaryOperation.rightOperand - val lhsStr = lhs.toHumanReadableEnglishString(divAsFraction) - val rhsStr = rhs.toHumanReadableEnglishString(divAsFraction) - if (lhsStr == null || rhsStr == null) return null - when (binaryOperation.operator) { - ADD -> "$lhsStr plus $rhsStr" - SUBTRACT -> "$lhsStr minus $rhsStr" - MULTIPLY -> { - if (binaryOperation.canBeReadAsImplicitMultiplication()) { - "$lhsStr $rhsStr" - } else "$lhsStr times $rhsStr" - } - DIVIDE -> when { - divAsFraction -> when { - binaryOperation.isOneHalf() -> "one half" - binaryOperation.isSimpleFraction() -> "$lhsStr over $rhsStr" - else -> "the fraction with numerator $lhsStr and denominator $rhsStr" + else -> variable + } + BINARY_OPERATION -> { + val lhs = binaryOperation.leftOperand + val rhs = binaryOperation.rightOperand + val lhsStr = lhs.toHumanReadableEnglishString(divAsFraction) + val rhsStr = rhs.toHumanReadableEnglishString(divAsFraction) + if (lhsStr == null || rhsStr == null) return null + when (binaryOperation.operator) { + ADD -> { + resourceHandler.getStringInLocaleWithWrapping( + R.string.math_accessibility_a_plus_b, lhsStr, rhsStr + ) + } + SUBTRACT -> { + resourceHandler.getStringInLocaleWithWrapping( + R.string.math_accessibility_a_minus_b, lhsStr, rhsStr + ) + } + MULTIPLY -> { + val strResId = if (binaryOperation.canBeReadAsImplicitMultiplication()) { + R.string.math_accessibility_implicit_multiplication + } else R.string.math_accessibility_a_times_b + resourceHandler.getStringInLocaleWithWrapping(strResId, lhsStr, rhsStr) + } + DIVIDE -> when { + divAsFraction -> when { + binaryOperation.isOneHalf() -> { + resourceHandler.getStringInLocaleWithWrapping( + R.string.math_accessibility_part_one_half + ) + } + binaryOperation.isSimpleFraction() -> { + resourceHandler.getStringInLocaleWithWrapping( + R.string.math_accessibility_simple_fraction, lhsStr, rhsStr + ) } - else -> "$lhsStr divided by $rhsStr" + else -> { + resourceHandler.getStringInLocaleWithWrapping( + R.string.math_accessibility_complex_fraction, lhsStr, rhsStr + ) + } + } + else -> { + resourceHandler.getStringInLocaleWithWrapping( + R.string.math_accessibility_a_divides_b, lhsStr, rhsStr + ) } - EXPONENTIATE -> "$lhsStr raised to the power of $rhsStr" - BinaryOperator.OPERATOR_UNSPECIFIED, BinaryOperator.UNRECOGNIZED, null -> null } + EXPONENTIATE -> { + resourceHandler.getStringInLocaleWithWrapping( + R.string.math_accessibility_a_exp_b, lhsStr, rhsStr + ) + } + BinaryOperator.OPERATOR_UNSPECIFIED, BinaryOperator.UNRECOGNIZED, null -> null } - UNARY_OPERATION -> { - val operandStr = unaryOperation.operand.toHumanReadableEnglishString(divAsFraction) - when (unaryOperation.operator) { - NEGATE -> operandStr?.let { "negative $it" } - POSITIVE -> operandStr?.let { "positive $it" } - UnaryOperator.OPERATOR_UNSPECIFIED, UnaryOperator.UNRECOGNIZED, null -> null + } + UNARY_OPERATION -> { + val operandStr = unaryOperation.operand.toHumanReadableEnglishString(divAsFraction) + when (unaryOperation.operator) { + NEGATE -> operandStr?.let { + resourceHandler.getStringInLocaleWithWrapping( + R.string.math_accessibility_negative_a, it + ) + } + POSITIVE -> operandStr?.let { + resourceHandler.getStringInLocaleWithWrapping( + R.string.math_accessibility_positive_a, it + ) } + UnaryOperator.OPERATOR_UNSPECIFIED, UnaryOperator.UNRECOGNIZED, null -> null } - FUNCTION_CALL -> { - val argStr = functionCall.argument.toHumanReadableEnglishString(divAsFraction) - when (functionCall.functionType) { - SQUARE_ROOT -> argStr?.let { - if (functionCall.argument.isSingleTerm()) { - "square root of $it" - } else "start square root $it end square root" + } + FUNCTION_CALL -> { + val argStr = functionCall.argument.toHumanReadableEnglishString(divAsFraction) + when (functionCall.functionType) { + SQUARE_ROOT -> argStr?.let { + if (functionCall.argument.isSingleTerm()) { + resourceHandler.getStringInLocaleWithWrapping( + R.string.math_accessibility_simple_square_root, it + ) + } else { + resourceHandler.getStringInLocaleWithWrapping( + R.string.math_accessibility_complex_square_root, it + ) } - FUNCTION_UNSPECIFIED, FunctionType.UNRECOGNIZED, null -> null } + FUNCTION_UNSPECIFIED, FunctionType.UNRECOGNIZED, null -> null } - GROUP -> group.toHumanReadableEnglishString(divAsFraction)?.let { - if (isSingleTerm()) it else "open parenthesis $it close parenthesis" - } - EXPRESSIONTYPE_NOT_SET, null -> null } + GROUP -> group.toHumanReadableEnglishString(divAsFraction)?.let { + if (!isSingleTerm()) { + resourceHandler.getStringInLocaleWithWrapping(R.string.math_accessibility_group, it) + } else it + } + EXPRESSIONTYPE_NOT_SET, null -> null } + } + private companion object { private fun MathBinaryOperation.canBeReadAsImplicitMultiplication(): Boolean { // Note that exponentiation is specialized since it's higher precedence than multiplication // which means the graph won't look like "constant * variable" for polynomial terms like 2x^4 diff --git a/app/src/main/res/values/untranslated_strings.xml b/app/src/main/res/values/untranslated_strings.xml index 5b1ceec25ab..5494cfc9daf 100644 --- a/app/src/main/res/values/untranslated_strings.xml +++ b/app/src/main/res/values/untranslated_strings.xml @@ -41,4 +41,21 @@ Profile data is currently uploading… All profile data has been uploaded. Please connect to a WiFi or Cellular network in order to upload profile data. + + zed + one half + %s equals %s + %s plus %s + %s minus %s + %s times %s + %s divided by %s + %s raised to the power of %s + negative %s + positive %s + square root of %s + start square root %s end square root + open parenthesis %s close parenthesis + %s %s + %s over %s + the fraction with numerator %s and denominator %s diff --git a/app/src/test/java/org/oppia/android/app/translation/AppLanguageResourceHandlerTest.kt b/app/src/test/java/org/oppia/android/app/translation/AppLanguageResourceHandlerTest.kt index 835f8d78d19..c3ac67aa09b 100644 --- a/app/src/test/java/org/oppia/android/app/translation/AppLanguageResourceHandlerTest.kt +++ b/app/src/test/java/org/oppia/android/app/translation/AppLanguageResourceHandlerTest.kt @@ -424,6 +424,39 @@ class AppLanguageResourceHandlerTest { } } + @Test + fun testFormatLong_forLargeLong_returnsStringWithExactDigits() { + updateAppLanguageTo(OppiaLanguage.ENGLISH) + val handler = retrieveAppLanguageResourceHandler() + + val formattedString = handler.formatLong(123456789) + + assertThat(formattedString.filter { it.isDigit() }).isEqualTo("123456789") + } + + @Test + fun testFormatLong_forDouble_returnsStringWithExactDigits() { + updateAppLanguageTo(OppiaLanguage.ENGLISH) + val handler = retrieveAppLanguageResourceHandler() + + val formattedString = handler.formatDouble(454545456.123) + + val digitsOnly = formattedString.filter { it.isDigit() } + assertThat(digitsOnly).contains("454545456") + assertThat(digitsOnly).contains("123") + } + + @Test + fun testFormatLong_forDouble_returnsStringWithPeriodsOrCommas() { + updateAppLanguageTo(OppiaLanguage.ENGLISH) + val handler = retrieveAppLanguageResourceHandler() + + val formattedString = handler.formatDouble(123456789.123) + + // Depending on formatting, commas and/or periods are used for large doubles. + assertThat(formattedString).containsMatch("[,.]") + } + @Test fun testComputeDateString_forFixedTime_returnMonthDayYearParts() { updateAppLanguageTo(OppiaLanguage.ENGLISH) diff --git a/app/src/test/java/org/oppia/android/app/utility/math/BUILD.bazel b/app/src/test/java/org/oppia/android/app/utility/math/BUILD.bazel index 48908b50a8c..3f5ff80c424 100644 --- a/app/src/test/java/org/oppia/android/app/utility/math/BUILD.bazel +++ b/app/src/test/java/org/oppia/android/app/utility/math/BUILD.bazel @@ -13,19 +13,35 @@ oppia_android_test( test_manifest = "//app:test_manifest", deps = [ ":dagger", + "//app", + "//app/src/main/java/org/oppia/android/app/translation/testing:test_module", "//app/src/main/java/org/oppia/android/app/utility/math:math_expression_accessibility_util", + "//domain/src/main/java/org/oppia/android/domain/onboarding/testing:retriever_test_module", "//model/src/main/proto:languages_java_proto_lite", "//model/src/main/proto:math_java_proto_lite", + "//testing", + "//testing/src/main/java/org/oppia/android/testing/data:data_provider_test_monitor", + "//testing/src/main/java/org/oppia/android/testing/junit:initialize_default_locale_rule", "//testing/src/main/java/org/oppia/android/testing/junit:oppia_parameterized_test_runner", "//testing/src/main/java/org/oppia/android/testing/junit:parameterized_robolectric_test_runner", "//testing/src/main/java/org/oppia/android/testing/math:math_equation_subject", "//testing/src/main/java/org/oppia/android/testing/math:math_expression_subject", + "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module", + "//testing/src/main/java/org/oppia/android/testing/threading:test_module", + "//testing/src/main/java/org/oppia/android/testing/time:test_module", "//third_party:androidx_test_core", "//third_party:com_google_truth_truth", "//third_party:junit_junit", "//third_party:org_robolectric_robolectric", "//third_party:robolectric_android-all", + "//utility/src/main/java/org/oppia/android/util/accessibility:test_module", + "//utility/src/main/java/org/oppia/android/util/caching:asset_prod_module", + "//utility/src/main/java/org/oppia/android/util/caching/testing:caching_test_module", + "//utility/src/main/java/org/oppia/android/util/locale/testing:test_module", + "//utility/src/main/java/org/oppia/android/util/logging:prod_module", "//utility/src/main/java/org/oppia/android/util/math:math_expression_parser", + "//utility/src/main/java/org/oppia/android/util/networking:debug_module", + "//utility/src/main/java/org/oppia/android/util/networking:debug_util_module", ], ) diff --git a/app/src/test/java/org/oppia/android/app/utility/math/MathExpressionAccessibilityUtilTest.kt b/app/src/test/java/org/oppia/android/app/utility/math/MathExpressionAccessibilityUtilTest.kt index baa7ea46a6e..23c929115e8 100644 --- a/app/src/test/java/org/oppia/android/app/utility/math/MathExpressionAccessibilityUtilTest.kt +++ b/app/src/test/java/org/oppia/android/app/utility/math/MathExpressionAccessibilityUtilTest.kt @@ -1,17 +1,29 @@ package org.oppia.android.app.utility.math import android.app.Application +import androidx.appcompat.app.AppCompatActivity import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.rules.ActivityScenarioRule import com.google.common.truth.StringSubject import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage import dagger.BindsInstance import dagger.Component -import javax.inject.Inject import javax.inject.Singleton import org.junit.Before +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith +import org.oppia.android.app.activity.ActivityComponent +import org.oppia.android.app.activity.ActivityComponentFactory +import org.oppia.android.app.activity.ActivityIntentFactoriesModule +import org.oppia.android.app.application.ApplicationComponent +import org.oppia.android.app.application.ApplicationInjector +import org.oppia.android.app.application.ApplicationInjectorProvider +import org.oppia.android.app.application.ApplicationModule +import org.oppia.android.app.application.ApplicationStartupListenerModule +import org.oppia.android.app.devoptions.DeveloperOptionsModule +import org.oppia.android.app.devoptions.DeveloperOptionsStarterModule import org.oppia.android.app.model.MathBinaryOperation import org.oppia.android.app.model.MathEquation import org.oppia.android.app.model.MathExpression @@ -27,6 +39,39 @@ import org.oppia.android.app.model.OppiaLanguage.LANGUAGE_UNSPECIFIED import org.oppia.android.app.model.OppiaLanguage.PORTUGUESE import org.oppia.android.app.model.OppiaLanguage.UNRECOGNIZED import org.oppia.android.app.model.Real +import org.oppia.android.app.shim.ViewBindingShimModule +import org.oppia.android.app.testing.activity.TestActivity +import org.oppia.android.app.topic.PracticeTabModule +import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule +import org.oppia.android.data.backends.gae.NetworkConfigProdModule +import org.oppia.android.data.backends.gae.NetworkModule +import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule +import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule +import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule +import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule +import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule +import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule +import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule +import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule +import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule +import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule +import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule +import org.oppia.android.domain.exploration.lightweightcheckpointing.ExplorationStorageModule +import org.oppia.android.domain.hintsandsolution.HintsAndSolutionConfigModule +import org.oppia.android.domain.hintsandsolution.HintsAndSolutionProdModule +import org.oppia.android.domain.onboarding.testing.ExpirationMetaDataRetrieverTestModule +import org.oppia.android.domain.oppialogger.LogStorageModule +import org.oppia.android.domain.oppialogger.loguploader.LogUploadWorkerModule +import org.oppia.android.domain.platformparameter.PlatformParameterModule +import org.oppia.android.domain.platformparameter.PlatformParameterSingletonModule +import org.oppia.android.domain.question.QuestionModule +import org.oppia.android.domain.topic.PrimeTopicAssetsControllerModule +import org.oppia.android.domain.workmanager.WorkManagerConfigurationModule +import org.oppia.android.testing.TestLogReportingModule +import org.oppia.android.testing.junit.InitializeDefaultLocaleRule import org.oppia.android.testing.junit.OppiaParameterizedTestRunner import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Iteration import org.oppia.android.testing.junit.OppiaParameterizedTestRunner.Parameter @@ -37,12 +82,27 @@ import org.oppia.android.testing.math.MathEquationSubject import org.oppia.android.testing.math.MathEquationSubject.Companion.assertThat import org.oppia.android.testing.math.MathExpressionSubject import org.oppia.android.testing.math.MathExpressionSubject.Companion.assertThat +import org.oppia.android.testing.robolectric.RobolectricModule +import org.oppia.android.testing.threading.TestDispatcherModule +import org.oppia.android.testing.time.FakeOppiaClockModule +import org.oppia.android.util.accessibility.AccessibilityTestModule +import org.oppia.android.util.caching.AssetModule +import org.oppia.android.util.caching.testing.CachingTestModule +import org.oppia.android.util.gcsresource.GcsResourceModule +import org.oppia.android.util.locale.testing.LocaleTestModule +import org.oppia.android.util.logging.LoggerModule +import org.oppia.android.util.logging.firebase.FirebaseLogUploaderModule import org.oppia.android.util.math.MathExpressionParser import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode.ALL_ERRORS import org.oppia.android.util.math.MathExpressionParser.Companion.ErrorCheckingMode.REQUIRED_ONLY import org.oppia.android.util.math.MathExpressionParser.Companion.MathParsingResult import org.oppia.android.util.math.ONE_HALF +import org.oppia.android.util.networking.NetworkConnectionDebugUtilModule +import org.oppia.android.util.networking.NetworkConnectionUtilDebugModule +import org.oppia.android.util.parser.html.HtmlParserEntityTypeModule +import org.oppia.android.util.parser.image.GlideImageLoaderModule +import org.oppia.android.util.parser.image.ImageParsingModule import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode @@ -61,13 +121,28 @@ import org.robolectric.annotation.LooperMode @LooperMode(LooperMode.Mode.PAUSED) @Config(application = MathExpressionAccessibilityUtilTest.TestApplication::class) class MathExpressionAccessibilityUtilTest { - @Inject lateinit var util: MathExpressionAccessibilityUtil + @get:Rule + val initializeDefaultLocaleRule = InitializeDefaultLocaleRule() + + @get:Rule + var activityRule = + ActivityScenarioRule( + TestActivity.createIntent(ApplicationProvider.getApplicationContext()) + ) @Parameter lateinit var language: String @Parameter lateinit var expression: String @Parameter lateinit var equation: String @Parameter lateinit var a11yStr: String + lateinit var util: MathExpressionAccessibilityUtil + + @Before + fun setUp() { + setUpTestApplicationComponent() + activityRule.scenario.onActivity { util = it.mathExpressionAccessibilityUtil } + } + @Test fun testConvertToString_defaultExp_english_returnsNull() { val exp = MathExpression.getDefaultInstance() @@ -1177,11 +1252,6 @@ class MathExpressionAccessibilityUtilTest { assertThat(eq).forHumanReadable(ENGLISH).doesNotConvertToString() } - @Before - fun setUp() { - setUpTestApplicationComponent() - } - private fun MathExpressionSubject.forHumanReadable( language: OppiaLanguage ): HumanReadableStringChecker { @@ -1230,8 +1300,31 @@ class MathExpressionAccessibilityUtilTest { // TODO(#89): Move this to a common test application component. @Singleton - @Component - interface TestApplicationComponent { + @Component( + modules = [ + RobolectricModule::class, TestDispatcherModule::class, ApplicationModule::class, + PlatformParameterModule::class, LoggerModule::class, ContinueModule::class, + FractionInputModule::class, ItemSelectionInputModule::class, MultipleChoiceInputModule::class, + NumberWithUnitsRuleModule::class, NumericInputRuleModule::class, TextInputRuleModule::class, + DragDropSortInputModule::class, ImageClickInputModule::class, InteractionsModule::class, + GcsResourceModule::class, GlideImageLoaderModule::class, ImageParsingModule::class, + HtmlParserEntityTypeModule::class, QuestionModule::class, TestLogReportingModule::class, + AccessibilityTestModule::class, LogStorageModule::class, CachingTestModule::class, + PrimeTopicAssetsControllerModule::class, ExpirationMetaDataRetrieverTestModule::class, + ViewBindingShimModule::class, RatioInputModule::class, NetworkConfigProdModule::class, + ApplicationStartupListenerModule::class, HintsAndSolutionConfigModule::class, + LogUploadWorkerModule::class, WorkManagerConfigurationModule::class, + FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class, + DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, + ExplorationStorageModule::class, NetworkModule::class, HintsAndSolutionProdModule::class, + NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, + AssetModule::class, LocaleTestModule::class, ActivityRecreatorTestModule::class, + ActivityIntentFactoriesModule::class, PlatformParameterSingletonModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class + ] + ) + interface TestApplicationComponent : ApplicationComponent { @Component.Builder interface Builder { @BindsInstance @@ -1243,7 +1336,7 @@ class MathExpressionAccessibilityUtilTest { fun inject(test: MathExpressionAccessibilityUtilTest) } - class TestApplication : Application() { + class TestApplication : Application(), ActivityComponentFactory, ApplicationInjectorProvider { private val component: TestApplicationComponent by lazy { DaggerMathExpressionAccessibilityUtilTest_TestApplicationComponent.builder() .setApplication(this) @@ -1253,6 +1346,12 @@ class MathExpressionAccessibilityUtilTest { fun inject(test: MathExpressionAccessibilityUtilTest) { component.inject(test) } + + override fun createActivityComponent(activity: AppCompatActivity): ActivityComponent { + return component.getActivityComponentBuilderProvider().get().setActivity(activity).build() + } + + override fun getApplicationInjector(): ApplicationInjector = component } private companion object { diff --git a/domain/src/main/java/org/oppia/android/domain/locale/DisplayLocaleImpl.kt b/domain/src/main/java/org/oppia/android/domain/locale/DisplayLocaleImpl.kt index 39b231cf4ac..8d3a291b961 100644 --- a/domain/src/main/java/org/oppia/android/domain/locale/DisplayLocaleImpl.kt +++ b/domain/src/main/java/org/oppia/android/domain/locale/DisplayLocaleImpl.kt @@ -9,6 +9,7 @@ import org.oppia.android.app.model.OppiaLocaleContext import org.oppia.android.util.locale.OppiaBidiFormatter import org.oppia.android.util.locale.OppiaLocale import java.text.DateFormat +import java.text.NumberFormat import java.util.Date import java.util.Locale import java.util.Objects @@ -33,6 +34,7 @@ class DisplayLocaleImpl( private val dateTimeFormat by lazy { DateFormat.getDateTimeInstance(DATE_FORMAT_LENGTH, TIME_FORMAT_LENGTH, formattingLocale) } + private val numberFormat by lazy { NumberFormat.getNumberInstance(formattingLocale) } private val bidiFormatter by lazy { formatterFactory.createFormatter(formattingLocale) } // TODO(#3766): Restrict to be 'internal'. @@ -46,6 +48,10 @@ class DisplayLocaleImpl( configuration.setLocale(formattingLocale) } + override fun formatLong(value: Long): String = numberFormat.format(value) + + override fun formatDouble(value: Double): String = numberFormat.format(value) + override fun computeDateString(timestampMillis: Long): String = dateFormat.format(Date(timestampMillis)) diff --git a/domain/src/test/java/org/oppia/android/domain/locale/DisplayLocaleImplTest.kt b/domain/src/test/java/org/oppia/android/domain/locale/DisplayLocaleImplTest.kt index 0ff73fdb1b8..4a55dffc08d 100644 --- a/domain/src/test/java/org/oppia/android/domain/locale/DisplayLocaleImplTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/locale/DisplayLocaleImplTest.kt @@ -133,6 +133,36 @@ class DisplayLocaleImplTest { assertThat(impl2).isEqualTo(impl1) } + @Test + fun testFormatLong_forLargeLong_returnsStringWithExactDigits() { + val impl = createDisplayLocaleImpl(US_ENGLISH_CONTEXT) + + val formattedString = impl.formatLong(123456789) + + assertThat(formattedString.filter { it.isDigit() }).isEqualTo("123456789") + } + + @Test + fun testFormatLong_forDouble_returnsStringWithExactDigits() { + val impl = createDisplayLocaleImpl(US_ENGLISH_CONTEXT) + + val formattedString = impl.formatDouble(454545456.123) + + val digitsOnly = formattedString.filter { it.isDigit() } + assertThat(digitsOnly).contains("454545456") + assertThat(digitsOnly).contains("123") + } + + @Test + fun testFormatLong_forDouble_returnsStringWithPeriodsOrCommas() { + val impl = createDisplayLocaleImpl(US_ENGLISH_CONTEXT) + + val formattedString = impl.formatDouble(123456789.123) + + // Depending on formatting, commas and/or periods are used for large doubles. + assertThat(formattedString).containsMatch("[,.]") + } + @Test fun testComputeDateString_forFixedTime_returnMonthDayYearParts() { val impl = createDisplayLocaleImpl(US_ENGLISH_CONTEXT) diff --git a/scripts/assets/file_content_validation_checks.textproto b/scripts/assets/file_content_validation_checks.textproto index 70a2d35df7a..f0d04f694f8 100644 --- a/scripts/assets/file_content_validation_checks.textproto +++ b/scripts/assets/file_content_validation_checks.textproto @@ -286,6 +286,7 @@ file_content_checks { file_path_regex: ".+?\\.kt" prohibited_content_regex: "OppiaParameterizedTestRunner" failure_message: "To use OppiaParameterizedTestRunner, please add an exemption to file_content_validation_checks.textproto and add an explanation for your use case in your PR description. Note that parameterized tests should only be used in special circumstances where a single behavior can be tested across multiple inputs, or for especially large test suites that can be trivially reduced." + exempted_file_name: "app/src/test/java/org/oppia/android/app/utility/math/MathExpressionAccessibilityUtilTest.kt" exempted_file_name: "domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputIsEquivalentToRuleClassifierProviderTest.kt" exempted_file_name: "domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProviderTest.kt" exempted_file_name: "domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProviderTest.kt" diff --git a/utility/src/main/java/org/oppia/android/util/locale/OppiaLocale.kt b/utility/src/main/java/org/oppia/android/util/locale/OppiaLocale.kt index b4aa0fa3439..0011ca6872f 100644 --- a/utility/src/main/java/org/oppia/android/util/locale/OppiaLocale.kt +++ b/utility/src/main/java/org/oppia/android/util/locale/OppiaLocale.kt @@ -207,6 +207,25 @@ sealed class OppiaLocale { * [org.oppia.android.domain.locale.LocaleController.setAsDefault]). */ abstract class DisplayLocale(override val localeContext: OppiaLocaleContext) : OppiaLocale() { + /** + * Returns a locally formatted representation of the long integer [value]. + * + * No assumptions can be made regarding the formatting of the returned string except that: + * 1. The exact value will be represented (no rounding or truncation will occur). + * 2. The resulting value should be generally readable by screenreaders if they support the the + * current locale. + */ + abstract fun formatLong(value: Long): String + + /** + * Returns a locally formatted representation of the double [value]. + * + * No assumptions can be made regarding the formatting of the returned string except that it + * should generally be readable by screenreaders if they support the current locale. This + * function may round and/or truncate the double for formatting simplicity. + */ + abstract fun formatDouble(value: Double): String + /** * Returns a locally formatted date string representing the specified Unix timestamp. * From 01b18321b9663577d8e632ab13dfa5595e572535 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Mon, 7 Feb 2022 22:36:51 -0800 Subject: [PATCH 124/162] Lint fixes. --- .../org/oppia/android/app/testing/activity/TestActivity.kt | 2 +- .../app/utility/math/MathExpressionAccessibilityUtil.kt | 6 +++--- .../app/utility/math/MathExpressionAccessibilityUtilTest.kt | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/org/oppia/android/app/testing/activity/TestActivity.kt b/app/src/main/java/org/oppia/android/app/testing/activity/TestActivity.kt index 72be82faf23..605c5699f69 100644 --- a/app/src/main/java/org/oppia/android/app/testing/activity/TestActivity.kt +++ b/app/src/main/java/org/oppia/android/app/testing/activity/TestActivity.kt @@ -7,8 +7,8 @@ import org.oppia.android.app.activity.InjectableAppCompatActivity import org.oppia.android.app.translation.AppLanguageResourceHandler import org.oppia.android.app.translation.AppLanguageWatcherMixin import org.oppia.android.app.utility.datetime.DateTimeUtil -import javax.inject.Inject import org.oppia.android.app.utility.math.MathExpressionAccessibilityUtil +import javax.inject.Inject // TODO(#3830): Migrate all test activities over to using this test activity & make this closed. /** diff --git a/app/src/main/java/org/oppia/android/app/utility/math/MathExpressionAccessibilityUtil.kt b/app/src/main/java/org/oppia/android/app/utility/math/MathExpressionAccessibilityUtil.kt index 5001922b9ab..6995cf901b1 100644 --- a/app/src/main/java/org/oppia/android/app/utility/math/MathExpressionAccessibilityUtil.kt +++ b/app/src/main/java/org/oppia/android/app/utility/math/MathExpressionAccessibilityUtil.kt @@ -1,9 +1,7 @@ package org.oppia.android.app.utility.math import org.oppia.android.R -import javax.inject.Inject import org.oppia.android.app.model.MathBinaryOperation -import org.oppia.android.app.model.MathBinaryOperation.Operator as BinaryOperator import org.oppia.android.app.model.MathBinaryOperation.Operator.ADD import org.oppia.android.app.model.MathBinaryOperation.Operator.DIVIDE import org.oppia.android.app.model.MathBinaryOperation.Operator.EXPONENTIATE @@ -21,7 +19,6 @@ import org.oppia.android.app.model.MathExpression.ExpressionTypeCase.VARIABLE import org.oppia.android.app.model.MathFunctionCall.FunctionType import org.oppia.android.app.model.MathFunctionCall.FunctionType.FUNCTION_UNSPECIFIED import org.oppia.android.app.model.MathFunctionCall.FunctionType.SQUARE_ROOT -import org.oppia.android.app.model.MathUnaryOperation.Operator as UnaryOperator import org.oppia.android.app.model.MathUnaryOperation.Operator.NEGATE import org.oppia.android.app.model.MathUnaryOperation.Operator.POSITIVE import org.oppia.android.app.model.OppiaLanguage @@ -38,6 +35,9 @@ import org.oppia.android.app.model.Real.RealTypeCase.IRRATIONAL import org.oppia.android.app.model.Real.RealTypeCase.RATIONAL import org.oppia.android.app.model.Real.RealTypeCase.REALTYPE_NOT_SET import org.oppia.android.app.translation.AppLanguageResourceHandler +import javax.inject.Inject +import org.oppia.android.app.model.MathBinaryOperation.Operator as BinaryOperator +import org.oppia.android.app.model.MathUnaryOperation.Operator as UnaryOperator /** * Utility for computing an accessibility string for screenreaders to be able to read out parsed diff --git a/app/src/test/java/org/oppia/android/app/utility/math/MathExpressionAccessibilityUtilTest.kt b/app/src/test/java/org/oppia/android/app/utility/math/MathExpressionAccessibilityUtilTest.kt index 23c929115e8..b9c58dc7a09 100644 --- a/app/src/test/java/org/oppia/android/app/utility/math/MathExpressionAccessibilityUtilTest.kt +++ b/app/src/test/java/org/oppia/android/app/utility/math/MathExpressionAccessibilityUtilTest.kt @@ -9,7 +9,6 @@ import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage import dagger.BindsInstance import dagger.Component -import javax.inject.Singleton import org.junit.Before import org.junit.Rule import org.junit.Test @@ -105,6 +104,7 @@ import org.oppia.android.util.parser.image.GlideImageLoaderModule import org.oppia.android.util.parser.image.ImageParsingModule import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode +import javax.inject.Singleton /** * Tests for [MathExpressionAccessibilityUtil]. From 0059a8dc495bcbcf2985ec92db27228e56e6589e Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Mon, 7 Feb 2022 23:31:37 -0800 Subject: [PATCH 125/162] Post-merge fix. --- utility/BUILD.bazel | 1 - .../src/main/java/org/oppia/android/util/parser/html/BUILD.bazel | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/utility/BUILD.bazel b/utility/BUILD.bazel index 5f1eda5cdec..ea0aac5c42a 100644 --- a/utility/BUILD.bazel +++ b/utility/BUILD.bazel @@ -65,7 +65,6 @@ kt_android_library( "//third_party:com_github_bumptech_glide_glide", "//third_party:com_google_guava_guava", "//third_party:glide_compiler", - "//third_party:io_github_karino2_kotlitex", "//third_party:org_jetbrains_kotlinx_kotlinx-coroutines-core", "//utility/src/main/java/org/oppia/android/util/caching:annotations", "//utility/src/main/java/org/oppia/android/util/caching:asset_repository", diff --git a/utility/src/main/java/org/oppia/android/util/parser/html/BUILD.bazel b/utility/src/main/java/org/oppia/android/util/parser/html/BUILD.bazel index 946f880582e..0f488770ddf 100644 --- a/utility/src/main/java/org/oppia/android/util/parser/html/BUILD.bazel +++ b/utility/src/main/java/org/oppia/android/util/parser/html/BUILD.bazel @@ -33,6 +33,7 @@ kt_android_library( visibility = ["//utility:__subpackages__"], deps = [ ":custom_html_content_handler", + "//third_party:io_github_karino2_kotlitex", "//utility/src/main/java/org/oppia/android/util/logging:console_logger", ], ) From 43fbc3d156867e44a2acf4b5d94821204037faba Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 9 Feb 2022 18:16:50 -0800 Subject: [PATCH 126/162] Cache KotliTeX renders. Directly rendering LaTeX through KotliTeX is way too slow, so this introduces a custom flow through Glide that computes a PNG for the LaTeX on a background thread and then caches it as part of Glide's cache to speed up re-renders of the LaTeX. We may need to manage/prune the cache over time, but for now we'll just rely on Glide's internal behaviors. This also documents some new tests that should be added, but it's not comprehensive. --- WORKSPACE | 2 +- .../android/app/parser/HtmlParserTest.kt | 4 + domain/src/main/assets/GJ2rLXRKD5hw_1.json | 2 +- .../src/main/assets/GJ2rLXRKD5hw_1.textproto | 2 +- .../PlatformParameterModule.kt | 12 ++ utility/build.gradle | 2 +- .../parser/html/CustomHtmlContentHandler.kt | 3 + .../android/util/parser/html/HtmlParser.kt | 14 ++- .../util/parser/html/MathTagHandler.kt | 34 ++++-- .../android/util/parser/image/BUILD.bazel | 2 + .../util/parser/image/GlideImageLoader.kt | 13 +++ .../android/util/parser/image/ImageLoader.kt | 6 + .../parser/image/RepositoryGlideModule.kt | 9 ++ .../util/parser/image/UrlImageParser.kt | 88 ++++++++++---- .../android/util/parser/math/BUILD.bazel | 29 +++++ .../util/parser/math/MathBitmapModelLoader.kt | 108 ++++++++++++++++++ .../android/util/parser/math/MathModel.kt | 36 ++++++ .../PlatformParameterConstants.kt | 12 ++ .../util/parser/html/MathTagHandlerTest.kt | 41 +++++-- 19 files changed, 367 insertions(+), 52 deletions(-) create mode 100644 utility/src/main/java/org/oppia/android/util/parser/math/BUILD.bazel create mode 100644 utility/src/main/java/org/oppia/android/util/parser/math/MathBitmapModelLoader.kt create mode 100644 utility/src/main/java/org/oppia/android/util/parser/math/MathModel.kt diff --git a/WORKSPACE b/WORKSPACE index 7cab69bb9ec..1aad21315d9 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -133,7 +133,7 @@ git_repository( # min target SDK version to be compatible with Oppia. git_repository( name = "kotlitex", - commit = "d9466ac308ed9d52da8135f4e8bee036fe000973", + commit = "95db69aaf1ef979fd20ef9a85bd4ad266515aa20", remote = "https://github.com/oppia/kotlitex", ) diff --git a/app/src/sharedTest/java/org/oppia/android/app/parser/HtmlParserTest.kt b/app/src/sharedTest/java/org/oppia/android/app/parser/HtmlParserTest.kt index 6362346e0fa..39e4c3a8241 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/parser/HtmlParserTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/parser/HtmlParserTest.kt @@ -548,6 +548,10 @@ class HtmlParserTest { onView(withId(R.id.test_html_content_text_view)).perform(click()) } + // TODO: finish tests. + // testHtmlContent_withMathTag_missingFileName_inlineMode_loadsNonMathModeKotlitexMathSpan + // testHtmlContent_withMathTag_missingFileName_nonInlineMode_loadsMathModeKotlitexMathSpan + @Test fun testHtmlContent_withMathTag_loadsTextSvg() { val htmlParser = htmlParserFactory.create( diff --git a/domain/src/main/assets/GJ2rLXRKD5hw_1.json b/domain/src/main/assets/GJ2rLXRKD5hw_1.json index 5e98aa89a01..3b818d497fd 100644 --- a/domain/src/main/assets/GJ2rLXRKD5hw_1.json +++ b/domain/src/main/assets/GJ2rLXRKD5hw_1.json @@ -4,7 +4,7 @@ "page_contents": { "subtitled_html": { "content_id": "content", - "html": "

Description of subtopic is here.

.

This is sample subtopic with dummy content related to Fractions:

Fractions represent equal parts of a whole or a collection. Fraction of a whole: When we divide a whole into equal parts, each part is a fraction of the whole. A fraction has two parts. The number on the top of the line is called the numerator. It tells how many equal parts of the whole or collection are taken. The number below the line is called the denominator. It shows the total divisible number of equal parts the whole into or the total number of equal parts which are there in a collection." + "html": "

Description of subtopic is here.

.

This is sample subtopic with dummy content related to Fractions: A lot more text that hopefully wraps to the next line in order to see whether the fraction correctly breaks subsequent text lines since we definitely don't want it to overlap since then it would most certainly not be readable in the least.

Fractions represent equal parts of a whole or a collection. Fraction of a whole: When we divide a whole into equal parts, each part is a fraction of the whole. A fraction has two parts. The number on the top of the line is called the numerator. It tells how many equal parts of the whole or collection are taken. The number below the line is called the denominator. It shows the total divisible number of equal parts the whole into or the total number of equal parts which are there in a collection.

" }, "recorded_voiceovers": { "voiceovers_mapping": { diff --git a/domain/src/main/assets/GJ2rLXRKD5hw_1.textproto b/domain/src/main/assets/GJ2rLXRKD5hw_1.textproto index 85a8298d90d..92a7e98d761 100644 --- a/domain/src/main/assets/GJ2rLXRKD5hw_1.textproto +++ b/domain/src/main/assets/GJ2rLXRKD5hw_1.textproto @@ -1,6 +1,6 @@ subtopic_title: "What is a Fraction?" page_contents { - html: "

Description of subtopic is here.

.

This is sample subtopic with dummy content related to Fractions:

Fractions represent equal parts of a whole or a collection. Fraction of a whole: When we divide a whole into equal parts, each part is a fraction of the whole. A fraction has two parts. The number on the top of the line is called the numerator. It tells how many equal parts of the whole or collection are taken. The number below the line is called the denominator. It shows the total divisible number of equal parts the whole into or the total number of equal parts which are there in a collection." + html: "

Description of subtopic is here.

.

This is sample subtopic with dummy content related to Fractions: A lot more text that hopefully wraps to the next line in order to see whether the fraction correctly breaks subsequent text lines since we definitely don't want it to overlap since then it would most certainly not be readable in the least.

Fractions represent equal parts of a whole or a collection. Fraction of a whole: When we divide a whole into equal parts, each part is a fraction of the whole. A fraction has two parts. The number on the top of the line is called the numerator. It tells how many equal parts of the whole or collection are taken. The number below the line is called the denominator. It shows the total divisible number of equal parts the whole into or the total number of equal parts which are there in a collection.

" content_id: "content" } recorded_voiceover { diff --git a/domain/src/main/java/org/oppia/android/domain/platformparameter/PlatformParameterModule.kt b/domain/src/main/java/org/oppia/android/domain/platformparameter/PlatformParameterModule.kt index 43916e0b513..c228216d7f0 100644 --- a/domain/src/main/java/org/oppia/android/domain/platformparameter/PlatformParameterModule.kt +++ b/domain/src/main/java/org/oppia/android/domain/platformparameter/PlatformParameterModule.kt @@ -2,6 +2,9 @@ package org.oppia.android.domain.platformparameter import dagger.Module import dagger.Provides +import org.oppia.android.util.platformparameter.CACHE_LATEX_RENDERING +import org.oppia.android.util.platformparameter.CACHE_LATEX_RENDERING_DEFAULT_VALUE +import org.oppia.android.util.platformparameter.CacheLatexRendering import org.oppia.android.util.platformparameter.ENABLE_LANGUAGE_SELECTION_UI_DEFAULT_VALUE import org.oppia.android.util.platformparameter.EnableLanguageSelectionUi import org.oppia.android.util.platformparameter.LEARNER_STUDY_ANALYTICS @@ -56,4 +59,13 @@ class PlatformParameterModule { return platformParameterSingleton.getBooleanPlatformParameter(LEARNER_STUDY_ANALYTICS) ?: PlatformParameterValue.createDefaultParameter(LEARNER_STUDY_ANALYTICS_DEFAULT_VALUE) } + + @Provides + @CacheLatexRendering + fun provideCacheLatexRendering( + platformParameterSingleton: PlatformParameterSingleton + ): PlatformParameterValue { + return platformParameterSingleton.getBooleanPlatformParameter(CACHE_LATEX_RENDERING) + ?: PlatformParameterValue.createDefaultParameter(CACHE_LATEX_RENDERING_DEFAULT_VALUE) + } } diff --git a/utility/build.gradle b/utility/build.gradle index 834288badb4..14fa7ca50fd 100644 --- a/utility/build.gradle +++ b/utility/build.gradle @@ -65,7 +65,7 @@ dependencies { 'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0-alpha03', 'androidx.work:work-runtime-ktx:2.4.0', 'com.github.oppia:androidsvg:6bd15f69caee3e6857fcfcd123023716b4adec1d', - 'com.github.oppia:kotlitex:d9466ac308ed9d52da8135f4e8bee036fe000973', + 'com.github.oppia:kotlitex:95db69aaf1ef979fd20ef9a85bd4ad266515aa20', 'com.github.bumptech.glide:glide:4.11.0', 'com.google.dagger:dagger:2.24', 'com.google.firebase:firebase-analytics-ktx:17.5.0', diff --git a/utility/src/main/java/org/oppia/android/util/parser/html/CustomHtmlContentHandler.kt b/utility/src/main/java/org/oppia/android/util/parser/html/CustomHtmlContentHandler.kt index c19f35f61f3..d8952fc0c9b 100644 --- a/utility/src/main/java/org/oppia/android/util/parser/html/CustomHtmlContentHandler.kt +++ b/utility/src/main/java/org/oppia/android/util/parser/html/CustomHtmlContentHandler.kt @@ -173,6 +173,9 @@ class CustomHtmlContentHandler private constructor( /** Returns a new [Drawable] corresponding to the specified image filename and [Type]. */ fun loadDrawable(filename: String, type: Type): Drawable + // TODO: add docs & tests. + fun loadMathDrawable(rawLatex: String, lineHeight: Float, type: Type): Drawable + /** Corresponds to the types of images that can be retrieved. */ enum class Type { /** diff --git a/utility/src/main/java/org/oppia/android/util/parser/html/HtmlParser.kt b/utility/src/main/java/org/oppia/android/util/parser/html/HtmlParser.kt index 75601fc8a4b..168064f396a 100755 --- a/utility/src/main/java/org/oppia/android/util/parser/html/HtmlParser.kt +++ b/utility/src/main/java/org/oppia/android/util/parser/html/HtmlParser.kt @@ -10,6 +10,8 @@ import androidx.core.view.ViewCompat import org.oppia.android.util.logging.ConsoleLogger import org.oppia.android.util.parser.image.UrlImageParser import javax.inject.Inject +import org.oppia.android.util.platformparameter.CacheLatexRendering +import org.oppia.android.util.platformparameter.PlatformParameterValue /** Html Parser to parse custom Oppia tags with Android-compatible versions. */ class HtmlParser private constructor( @@ -19,8 +21,8 @@ class HtmlParser private constructor( private val entityType: String, private val entityId: String, private val imageCenterAlign: Boolean, - private val useInlineMathRendering: Boolean, private val consoleLogger: ConsoleLogger, + private val cacheLatexRendering: Boolean, customOppiaTagActionListener: CustomOppiaTagActionListener? ) { private val conceptCardTagHandler by lazy { @@ -112,7 +114,7 @@ class HtmlParser private constructor( consoleLogger, context.assets, htmlContentTextView.lineHeight.toFloat(), - useInlineMathRendering + cacheLatexRendering ) if (supportsConceptCards) { handlersMap[CUSTOM_CONCEPT_CARD_TAG] = conceptCardTagHandler @@ -153,7 +155,8 @@ class HtmlParser private constructor( class Factory @Inject constructor( private val urlImageParserFactory: UrlImageParser.Factory, private val consoleLogger: ConsoleLogger, - private val context: Context + private val context: Context, + @CacheLatexRendering private val enableCacheLatexRendering: PlatformParameterValue ) { /** * Returns a new [HtmlParser] with the specified entity type and ID for loading images, and an @@ -164,8 +167,7 @@ class HtmlParser private constructor( entityType: String, entityId: String, imageCenterAlign: Boolean, - customOppiaTagActionListener: CustomOppiaTagActionListener? = null, - useInlineMathRendering: Boolean = true + customOppiaTagActionListener: CustomOppiaTagActionListener? = null ): HtmlParser { return HtmlParser( context, @@ -174,8 +176,8 @@ class HtmlParser private constructor( entityType, entityId, imageCenterAlign, - useInlineMathRendering, consoleLogger, + cacheLatexRendering = enableCacheLatexRendering.value, customOppiaTagActionListener ) } diff --git a/utility/src/main/java/org/oppia/android/util/parser/html/MathTagHandler.kt b/utility/src/main/java/org/oppia/android/util/parser/html/MathTagHandler.kt index 481bfbed042..7aed0047ac6 100644 --- a/utility/src/main/java/org/oppia/android/util/parser/html/MathTagHandler.kt +++ b/utility/src/main/java/org/oppia/android/util/parser/html/MathTagHandler.kt @@ -7,11 +7,14 @@ import android.text.style.ImageSpan import io.github.karino2.kotlitex.view.MathExpressionSpan import org.json.JSONObject import org.oppia.android.util.logging.ConsoleLogger +import org.oppia.android.util.parser.html.CustomHtmlContentHandler.ImageRetriever.Type.BLOCK_IMAGE +import org.oppia.android.util.parser.html.CustomHtmlContentHandler.ImageRetriever.Type.INLINE_TEXT_IMAGE import org.xml.sax.Attributes /** The custom tag corresponding to [MathTagHandler]. */ const val CUSTOM_MATH_TAG = "oppia-noninteractive-math" -private const val CUSTOM_MATH_SVG_PATH_ATTRIBUTE = "math_content-with-value" +private const val CUSTOM_MATH_MATH_CONTENT_ATTRIBUTE = "math_content-with-value" +private const val CUSTOM_MATH_RENDER_TYPE_ATTRIBUTE = "render-type" /** * A custom tag handler for properly formatting math items in HTML parsed with @@ -21,7 +24,7 @@ class MathTagHandler( private val consoleLogger: ConsoleLogger, private val assetManager: AssetManager, private val lineHeight: Float, - private val useInlineRendering: Boolean + private val cacheLatexRendering: Boolean ) : CustomHtmlContentHandler.CustomTagHandler { override fun handleTag( attributes: Attributes, @@ -32,22 +35,39 @@ class MathTagHandler( ) { // Only insert the image tag if it's parsed correctly. val content = MathContent.parseMathContent( - attributes.getJsonObjectValue(CUSTOM_MATH_SVG_PATH_ATTRIBUTE) + attributes.getJsonObjectValue(CUSTOM_MATH_MATH_CONTENT_ATTRIBUTE) ) + // TODO: add tests for these cases. + // TODO: file TODO for fixing vertical alignment. + val useInlineRendering = when (attributes.getValue(CUSTOM_MATH_RENDER_TYPE_ATTRIBUTE)) { + "inline" -> true + "block" -> false + else -> true + } val newSpan = when (content) { is MathContent.MathAsSvg -> { ImageSpan( imageRetriever.loadDrawable( content.svgFilename, - CustomHtmlContentHandler.ImageRetriever.Type.INLINE_TEXT_IMAGE + INLINE_TEXT_IMAGE ), content.svgFilename ) } is MathContent.MathAsLatex -> { - MathExpressionSpan( - content.rawLatex, lineHeight, assetManager, isMathMode = !useInlineRendering - ) + if (cacheLatexRendering) { + ImageSpan( + imageRetriever.loadMathDrawable( + content.rawLatex, + lineHeight, + type = if (useInlineRendering) INLINE_TEXT_IMAGE else BLOCK_IMAGE + ) + ) + } else { + MathExpressionSpan( + content.rawLatex, lineHeight, assetManager, isMathMode = !useInlineRendering + ) + } } null -> { consoleLogger.e("MathTagHandler", "Failed to parse math tag") diff --git a/utility/src/main/java/org/oppia/android/util/parser/image/BUILD.bazel b/utility/src/main/java/org/oppia/android/util/parser/image/BUILD.bazel index f12bd0fedb6..bdc56d21a08 100644 --- a/utility/src/main/java/org/oppia/android/util/parser/image/BUILD.bazel +++ b/utility/src/main/java/org/oppia/android/util/parser/image/BUILD.bazel @@ -42,6 +42,7 @@ kt_android_library( "//third_party:javax_inject_javax_inject", "//utility/src/main/java/org/oppia/android/util/caching:annotations", "//utility/src/main/java/org/oppia/android/util/caching:asset_repository", + "//utility/src/main/java/org/oppia/android/util/parser/math:math_latex_model", "//utility/src/main/java/org/oppia/android/util/parser/svg:block_picture_drawable", "//utility/src/main/java/org/oppia/android/util/parser/svg:scalable_vector_graphic", "//utility/src/main/java/org/oppia/android/util/parser/svg:svg_blur_transformation", @@ -151,6 +152,7 @@ kt_android_library( "//third_party:glide_compiler", "//utility/src/main/java/org/oppia/android/util/caching:annotations", "//utility/src/main/java/org/oppia/android/util/caching:asset_repository", + "//utility/src/main/java/org/oppia/android/util/parser/math:math_bitmap_model_loader", "//utility/src/main/java/org/oppia/android/util/parser/svg:block_picture_drawable", "//utility/src/main/java/org/oppia/android/util/parser/svg:block_svg_drawable_transcoder", "//utility/src/main/java/org/oppia/android/util/parser/svg:scalable_vector_graphic", diff --git a/utility/src/main/java/org/oppia/android/util/parser/image/GlideImageLoader.kt b/utility/src/main/java/org/oppia/android/util/parser/image/GlideImageLoader.kt index 10e91bad48f..dd4dadc6856 100644 --- a/utility/src/main/java/org/oppia/android/util/parser/image/GlideImageLoader.kt +++ b/utility/src/main/java/org/oppia/android/util/parser/image/GlideImageLoader.kt @@ -18,6 +18,7 @@ import org.oppia.android.util.parser.svg.SvgDecoder import org.oppia.android.util.parser.svg.SvgPictureDrawable import javax.inject.Inject import javax.inject.Singleton +import org.oppia.android.util.parser.math.MathModel /** An [ImageLoader] that uses Glide. */ @Singleton @@ -68,6 +69,18 @@ class GlideImageLoader @Inject constructor( .intoTarget(target) } + override fun loadMathDrawable( + rawLatex: String, + lineHeight: Float, + useInlineRendering: Boolean, + target: ImageTarget + ) { + glide + .asBitmap() + .load(MathModel(rawLatex, lineHeight, useInlineRendering)) + .intoTarget(target) + } + private inline fun loadSvgWithGlide( imageUrl: String, target: ImageTarget, diff --git a/utility/src/main/java/org/oppia/android/util/parser/image/ImageLoader.kt b/utility/src/main/java/org/oppia/android/util/parser/image/ImageLoader.kt index 30963c9f3d6..8013a793cc9 100644 --- a/utility/src/main/java/org/oppia/android/util/parser/image/ImageLoader.kt +++ b/utility/src/main/java/org/oppia/android/util/parser/image/ImageLoader.kt @@ -1,6 +1,7 @@ package org.oppia.android.util.parser.image import android.graphics.Bitmap +import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.Drawable import androidx.annotation.DrawableRes import org.oppia.android.util.parser.svg.BlockPictureDrawable @@ -48,4 +49,9 @@ interface ImageLoader { target: ImageTarget, transformations: List = listOf() ) + + // TODO: add docs & tests & verify tests. + fun loadMathDrawable( + rawLatex: String, lineHeight: Float, useInlineRendering: Boolean, target: ImageTarget + ) } diff --git a/utility/src/main/java/org/oppia/android/util/parser/image/RepositoryGlideModule.kt b/utility/src/main/java/org/oppia/android/util/parser/image/RepositoryGlideModule.kt index b11652e0207..d1a44a07254 100644 --- a/utility/src/main/java/org/oppia/android/util/parser/image/RepositoryGlideModule.kt +++ b/utility/src/main/java/org/oppia/android/util/parser/image/RepositoryGlideModule.kt @@ -11,6 +11,9 @@ import org.oppia.android.util.parser.svg.ScalableVectorGraphic import org.oppia.android.util.parser.svg.SvgDecoder import org.oppia.android.util.parser.svg.TextSvgDrawableTranscoder import java.io.InputStream +import java.nio.ByteBuffer +import org.oppia.android.util.parser.math.MathBitmapModelLoader +import org.oppia.android.util.parser.math.MathModel /** * Custom [AppGlideModule] to enable loading images from @@ -37,5 +40,11 @@ class RepositoryGlideModule : AppGlideModule() { InputStream::class.java, RepositoryModelLoader.Factory() ) + + registry.append( + MathModel::class.java, + ByteBuffer::class.java, + MathBitmapModelLoader.Factory(context.applicationContext) + ) } } diff --git a/utility/src/main/java/org/oppia/android/util/parser/image/UrlImageParser.kt b/utility/src/main/java/org/oppia/android/util/parser/image/UrlImageParser.kt index f5a2567e493..161cb4c66bf 100644 --- a/utility/src/main/java/org/oppia/android/util/parser/image/UrlImageParser.kt +++ b/utility/src/main/java/org/oppia/android/util/parser/image/UrlImageParser.kt @@ -18,11 +18,12 @@ import com.bumptech.glide.request.transition.Transition import org.oppia.android.util.R import org.oppia.android.util.locale.OppiaLocale import org.oppia.android.util.logging.ConsoleLogger -import org.oppia.android.util.parser.html.CustomHtmlContentHandler import org.oppia.android.util.parser.html.CustomHtmlContentHandler.ImageRetriever import org.oppia.android.util.parser.svg.BlockPictureDrawable import javax.inject.Inject import kotlin.math.max +import org.oppia.android.util.parser.html.CustomHtmlContentHandler.ImageRetriever.Type.BLOCK_IMAGE +import org.oppia.android.util.parser.html.CustomHtmlContentHandler.ImageRetriever.Type.INLINE_TEXT_IMAGE // TODO(#169): Replace this with exploration asset downloader. @@ -39,10 +40,10 @@ class UrlImageParser private constructor( private val imageLoader: ImageLoader, private val consoleLogger: ConsoleLogger, private val machineLocale: OppiaLocale.MachineLocale -) : Html.ImageGetter, CustomHtmlContentHandler.ImageRetriever { +) : Html.ImageGetter, ImageRetriever { override fun getDrawable(urlString: String): Drawable { // Only block images can be loaded through the standard ImageGetter. - return loadDrawable(urlString, ImageRetriever.Type.BLOCK_IMAGE) + return loadDrawable(urlString, BLOCK_IMAGE) } override fun loadDrawable(filename: String, type: ImageRetriever.Type): Drawable { @@ -53,21 +54,23 @@ class UrlImageParser private constructor( val proxyDrawable = ProxyDrawable() // TODO(#1039): Introduce custom type OppiaImage for rendering Bitmap and Svg. val isSvg = machineLocale.run { imageUrl.endsWithIgnoreCase("svg") } - val adjustedType = if (type == ImageRetriever.Type.INLINE_TEXT_IMAGE && !isSvg) { + val adjustedType = if (type == INLINE_TEXT_IMAGE && !isSvg) { // Treat non-svg in-line images as block, instead, since only SVG is supported. consoleLogger.w("UrlImageParser", "Forcing image $filename to block image") - ImageRetriever.Type.BLOCK_IMAGE + BLOCK_IMAGE } else type return when (adjustedType) { - ImageRetriever.Type.INLINE_TEXT_IMAGE -> { + INLINE_TEXT_IMAGE -> { imageLoader.loadTextSvg( imageUrl, - createCustomTarget(proxyDrawable, AutoAdjustingImageTarget.TextSvgTarget::create) + createCustomTarget(proxyDrawable) { + AutoAdjustingImageTarget.InlineTextImage.createForSvg(it) + } ) proxyDrawable } - ImageRetriever.Type.BLOCK_IMAGE -> { + BLOCK_IMAGE -> { if (isSvg) { imageLoader.loadBlockSvg( imageUrl, @@ -90,6 +93,24 @@ class UrlImageParser private constructor( } } + override fun loadMathDrawable( + rawLatex: String, lineHeight: Float, type: ImageRetriever.Type + ): Drawable { + return ProxyDrawable().also { drawable -> + imageLoader.loadMathDrawable( + rawLatex, + lineHeight, + useInlineRendering = type == INLINE_TEXT_IMAGE, + createCustomTarget(drawable) { + when (type) { + INLINE_TEXT_IMAGE -> AutoAdjustingImageTarget.InlineTextImage.createForMath(context, it) + BLOCK_IMAGE -> AutoAdjustingImageTarget.BlockImageTarget.BitmapTarget.create(it) + } + } + ) + } + } + private fun > createCustomTarget( proxyDrawable: ProxyDrawable, createTarget: (AutoAdjustingImageTarget.TargetConfiguration) -> C @@ -239,26 +260,47 @@ class UrlImageParser private constructor( } /** - * A [AutoAdjustingImageTarget] that should be used for in-line SVG images that will not be - * resized or aligned beyond what the SVG itself requires, and what the system performs - * automatically. + * A [AutoAdjustingImageTarget] that should be used for in-line SVG images and math expressions + * that will not be resized or aligned beyond what the target itself requires, and what the + * system performs automatically. */ - class TextSvgTarget( - targetConfiguration: TargetConfiguration - ) : AutoAdjustingImageTarget(targetConfiguration) { - override fun retrieveDrawable(resource: TextPictureDrawable): TextPictureDrawable = resource - - override fun computeBounds( - drawable: TextPictureDrawable, - viewWidth: Int - ): Rect { - drawable.computeTextPicture(htmlContentTextView.paint) + class InlineTextImage( + targetConfiguration: TargetConfiguration, + private val computeDrawable: (T) -> D, + private val computeDimensions: (D, TextView) -> Unit, + ) : AutoAdjustingImageTarget(targetConfiguration) { + override fun retrieveDrawable(resource: T): D = computeDrawable(resource) + + override fun computeBounds(drawable: D, viewWidth: Int): Rect { + computeDimensions(drawable, htmlContentTextView) return Rect(/* left= */ 0, /* top= */ 0, drawable.intrinsicWidth, drawable.intrinsicHeight) } companion object { - /** Returns a new [TextSvgTarget] for the specified configuration. */ - fun create(targetConfiguration: TargetConfiguration) = TextSvgTarget(targetConfiguration) + /** Returns a new [InlineTextImage] for the specified SVG configuration. */ + fun createForSvg( + targetConfiguration: TargetConfiguration + ): InlineTextImage { + return InlineTextImage( + targetConfiguration, + computeDrawable = { it }, + computeDimensions = { drawable, textView -> + drawable.computeTextPicture(textView.paint) + } + ) + } + + /** Returns a new [InlineTextImage] for the specified math configuration. */ + fun createForMath( + applicationContext: Context, + targetConfiguration: TargetConfiguration + ): InlineTextImage { + return InlineTextImage( + targetConfiguration, + computeDrawable = { BitmapDrawable(applicationContext.resources, it) }, + computeDimensions = { _, _ -> } + ) + } } } diff --git a/utility/src/main/java/org/oppia/android/util/parser/math/BUILD.bazel b/utility/src/main/java/org/oppia/android/util/parser/math/BUILD.bazel new file mode 100644 index 00000000000..7e021bddea2 --- /dev/null +++ b/utility/src/main/java/org/oppia/android/util/parser/math/BUILD.bazel @@ -0,0 +1,29 @@ +""" +Components required to render LaTeX math expressions through Glide. +""" + +load("@io_bazel_rules_kotlin//kotlin:kotlin.bzl", "kt_android_library") + +kt_android_library( + name = "math_latex_model", + srcs = [ + "MathModel.kt", + ], + visibility = ["//utility/src/main/java/org/oppia/android/util/parser/image:__pkg__"], + deps = [ + "//third_party:com_github_bumptech_glide_glide", + ], +) + +kt_android_library( + name = "math_bitmap_model_loader", + srcs = [ + "MathBitmapModelLoader.kt", + ], + visibility = ["//utility/src/main/java/org/oppia/android/util/parser/image:__pkg__"], + deps = [ + ":math_latex_model", + "//third_party:com_github_bumptech_glide_glide", + "//third_party:io_github_karino2_kotlitex", + ], +) diff --git a/utility/src/main/java/org/oppia/android/util/parser/math/MathBitmapModelLoader.kt b/utility/src/main/java/org/oppia/android/util/parser/math/MathBitmapModelLoader.kt new file mode 100644 index 00000000000..c4d53d9d754 --- /dev/null +++ b/utility/src/main/java/org/oppia/android/util/parser/math/MathBitmapModelLoader.kt @@ -0,0 +1,108 @@ +package org.oppia.android.util.parser.math + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Canvas +import android.text.Layout +import android.text.Spannable +import android.text.SpannableStringBuilder +import android.text.StaticLayout +import android.text.TextPaint +import com.bumptech.glide.Priority +import com.bumptech.glide.load.DataSource +import com.bumptech.glide.load.Options +import com.bumptech.glide.load.data.DataFetcher +import com.bumptech.glide.load.model.ModelLoader +import com.bumptech.glide.load.model.ModelLoaderFactory +import com.bumptech.glide.load.model.MultiModelLoaderFactory +import com.bumptech.glide.request.target.Target +import io.github.karino2.kotlitex.view.MathExpressionSpan +import java.io.ByteArrayOutputStream +import java.nio.ByteBuffer + +// Reference: https://bumptech.github.io/glide/tut/custom-modelloader.html#writing-the-modelloader. +class MathBitmapModelLoader private constructor( + private val applicationContext: Context +): ModelLoader { + override fun buildLoadData( + model: MathModel, width: Int, height: Int, options: Options + ): ModelLoader.LoadData { + return ModelLoader.LoadData( + model.toKeySignature(), LatexModelDataFetcher(applicationContext, model, width, height) + ) + } + + override fun handles(model: MathModel): Boolean = true + + private class LatexModelDataFetcher( + private val applicationContext: Context, + private val model: MathModel, + private val targetWidth: Int, + private val targetHeight: Int + ): DataFetcher { + override fun loadData(priority: Priority, callback: DataFetcher.DataCallback) { + val span = MathExpressionSpan( + model.rawLatex, model.lineHeight, applicationContext.assets, !model.useInlineRendering + ) + val renderableText = SpannableStringBuilder("\uFFFC").apply { + setSpan(span, /* start= */ 0, /* end= */ 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + } + + // Use Android's StaticLayout to ensure the text is rendered correctly. Note that the + // constants are derived from TextView's defaults (except width which is defaulted to 0 since + // the width isn't necessarily known ahead of time). + @Suppress("DEPRECATION") // This call is necessary for the supported min API version. + val staticTextLayout = + StaticLayout( + renderableText, + TextPaint(), // Any TextPaint can be used since the span will use its own. + /* width= */ 0, + Layout.Alignment.ALIGN_LEFT, + /* spacingmult= */ 1f, + /* spacingadd= */ 0f, + /* includepad= */ true + ) + + // Reference: https://stackoverflow.com/a/27631737/3689782. + val bounds = span.drawableBounds + val canvasBitmap = + Bitmap.createBitmap(bounds.width(), bounds.height(), Bitmap.Config.ARGB_8888) + val bitmapCanvas = Canvas(canvasBitmap) + staticTextLayout.draw(bitmapCanvas) + + val finalWidth = if (targetWidth == Target.SIZE_ORIGINAL) bounds.width() else targetWidth + val finalHeight = if (targetHeight == Target.SIZE_ORIGINAL) bounds.height() else targetHeight + + // Compute the final bitmap (which might need to be scaled depending on options). + val bitmap = if (canvasBitmap.width != finalWidth || canvasBitmap.height != finalHeight) { + // Glide is requesting the image in a different size, so adjust it. + Bitmap.createScaledBitmap(canvasBitmap, finalWidth, finalHeight, /* filter= */ true) + } else canvasBitmap // Otherwise, the original bitmap is the correct size. + + // Convert the bitmap to a PNG to store within Glide's cache for later retrieval. + val rawBitmap = ByteArrayOutputStream().also { outputStream -> + bitmap.compress(Bitmap.CompressFormat.PNG, /* quality= */ 100, outputStream) + }.toByteArray() + callback.onDataReady(ByteBuffer.wrap(rawBitmap)) + } + + override fun cleanup() {} + + override fun cancel() {} + + override fun getDataClass(): Class = ByteBuffer::class.java + + // 'Retrieval' is expensive in this case since a rendering operation is needed. + override fun getDataSource(): DataSource = DataSource.REMOTE + } + + class Factory( + private val applicationContext: Context + ): ModelLoaderFactory { + override fun build(factory: MultiModelLoaderFactory): ModelLoader { + return MathBitmapModelLoader(applicationContext) + } + + override fun teardown() {} + } +} diff --git a/utility/src/main/java/org/oppia/android/util/parser/math/MathModel.kt b/utility/src/main/java/org/oppia/android/util/parser/math/MathModel.kt new file mode 100644 index 00000000000..07a38d37bca --- /dev/null +++ b/utility/src/main/java/org/oppia/android/util/parser/math/MathModel.kt @@ -0,0 +1,36 @@ +package org.oppia.android.util.parser.math + +import com.bumptech.glide.load.Key +import java.nio.ByteBuffer +import java.security.MessageDigest + +data class MathModel( + val rawLatex: String, val lineHeight: Float, val useInlineRendering: Boolean +) { + fun toKeySignature(): MathModelSignature = + MathModelSignature.createSignature(rawLatex, lineHeight, useInlineRendering) + + // Reference: http://bumptech.github.io/glide/doc/caching.html#custom-cache-invalidation. + // TODO: document that lineHeight is only stored up to 2 decimal places, and to use factory method. + data class MathModelSignature( + val rawLatex: String, val lineHeightHundredX: Int, val useInlineRendering: Boolean + ) : Key { + override fun updateDiskCacheKey(messageDigest: MessageDigest) { + val rawLatexBytes = rawLatex.encodeToByteArray() + messageDigest.update(ByteBuffer.allocate(rawLatexBytes.size + Int.SIZE_BYTES + 1).apply { + put(rawLatexBytes) + putInt(lineHeightHundredX) + put(if (useInlineRendering) 1 else 0) + }.array()) + } + + companion object { + fun createSignature( + rawLatex: String, lineHeight: Float, useInlineRendering: Boolean + ): MathModelSignature { + val lineHeightHundredX = (lineHeight * 100f).toInt() + return MathModelSignature(rawLatex, lineHeightHundredX, useInlineRendering) + } + } + } +} diff --git a/utility/src/main/java/org/oppia/android/util/platformparameter/PlatformParameterConstants.kt b/utility/src/main/java/org/oppia/android/util/platformparameter/PlatformParameterConstants.kt index 1ecb1f717bd..38d66b30d27 100644 --- a/utility/src/main/java/org/oppia/android/util/platformparameter/PlatformParameterConstants.kt +++ b/utility/src/main/java/org/oppia/android/util/platformparameter/PlatformParameterConstants.kt @@ -80,3 +80,15 @@ const val LEARNER_STUDY_ANALYTICS = "learner_study_analytics" * and working of learner study related analytics logging. */ const val LEARNER_STUDY_ANALYTICS_DEFAULT_VALUE = false + +/** + * Qualifier for the platform parameter that controls whether to cache LaTeX rendering using Glide. + */ +@Qualifier +annotation class CacheLatexRendering + +/** Name of the platform that controls whether to cache LaTeX rendering using Glide. */ +const val CACHE_LATEX_RENDERING = "cache_latex_rendering" + +/** Default value for whether to cache LaTeX rendering using Glide. */ +const val CACHE_LATEX_RENDERING_DEFAULT_VALUE = true diff --git a/utility/src/test/java/org/oppia/android/util/parser/html/MathTagHandlerTest.kt b/utility/src/test/java/org/oppia/android/util/parser/html/MathTagHandlerTest.kt index 5e584f9449f..646179411f7 100644 --- a/utility/src/test/java/org/oppia/android/util/parser/html/MathTagHandlerTest.kt +++ b/utility/src/test/java/org/oppia/android/util/parser/html/MathTagHandlerTest.kt @@ -59,6 +59,8 @@ private const val MATH_WITHOUT_FILENAME_MARKUP = ":&quot;\\\\frac{2}{5}&quot;,}\">" /** Tests for [MathTagHandler]. */ +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) class MathTagHandlerTest { @@ -74,15 +76,24 @@ class MathTagHandlerTest { @Inject lateinit var consoleLogger: ConsoleLogger private lateinit var noTagHandlers: Map - private lateinit var tagHandlersWithMathSupport: Map + private lateinit var tagHandlersWithInlineMathSupport: Map + private lateinit var tagHandlersWithBlockMathSupport: Map @Before fun setUp() { setUpTestApplicationComponent() noTagHandlers = mapOf() - tagHandlersWithMathSupport = mapOf( - CUSTOM_MATH_TAG to MathTagHandler(consoleLogger) + tagHandlersWithInlineMathSupport = mapOf( + CUSTOM_MATH_TAG to createMathTagHandler(useInlineRendering = true) ) + tagHandlersWithBlockMathSupport = mapOf( + CUSTOM_MATH_TAG to createMathTagHandler(useInlineRendering = false) + ) + } + + private fun createMathTagHandler(useInlineRendering: Boolean): MathTagHandler { + // Pick an arbitrary line height since rendering doesn't actually happen in the test env. + return MathTagHandler(consoleLogger, context.assets, lineHeight = 10.0f, useInlineRendering) } // TODO(#3085): Introduce test for verifying that the error log scenario is logged correctly. @@ -93,7 +104,7 @@ class MathTagHandlerTest { CustomHtmlContentHandler.fromHtml( html = "", imageRetriever = mockImageRetriever, - customTagHandlers = tagHandlersWithMathSupport + customTagHandlers = tagHandlersWithInlineMathSupport ) val imageSpans = parsedHtml.getSpansFromWholeString(ImageSpan::class) @@ -106,7 +117,7 @@ class MathTagHandlerTest { CustomHtmlContentHandler.fromHtml( html = MATH_MARKUP_1, imageRetriever = mockImageRetriever, - customTagHandlers = tagHandlersWithMathSupport + customTagHandlers = tagHandlersWithInlineMathSupport ) val imageSpans = parsedHtml.getSpansFromWholeString(ImageSpan::class) @@ -119,7 +130,7 @@ class MathTagHandlerTest { CustomHtmlContentHandler.fromHtml( html = MATH_MARKUP_1, imageRetriever = mockImageRetriever, - customTagHandlers = tagHandlersWithMathSupport + customTagHandlers = tagHandlersWithInlineMathSupport ) // The image only adds a control character, so there aren't any human-readable characters. @@ -134,33 +145,39 @@ class MathTagHandlerTest { CustomHtmlContentHandler.fromHtml( html = MATH_WITHOUT_CONTENT_VALUE_MARKUP, imageRetriever = mockImageRetriever, - customTagHandlers = tagHandlersWithMathSupport + customTagHandlers = tagHandlersWithInlineMathSupport ) val imageSpans = parsedHtml.getSpansFromWholeString(ImageSpan::class) assertThat(imageSpans).isEmpty() } + // TODO: update this to say it DOES include the span. @Test fun testParseHtml_withMathMarkup_missingRawLatex_doesNotIncludeImageSpan() { val parsedHtml = CustomHtmlContentHandler.fromHtml( html = MATH_WITHOUT_RAW_LATEX_MARKUP, imageRetriever = mockImageRetriever, - customTagHandlers = tagHandlersWithMathSupport + customTagHandlers = tagHandlersWithInlineMathSupport ) val imageSpans = parsedHtml.getSpansFromWholeString(ImageSpan::class) assertThat(imageSpans).isEmpty() } + + // TODO: finish tests + // Replace below: testParseHtml_withMathMarkup_missingFilename_includesNonMathModeKotlitexMathSpan + // testParseHtml_withMathMarkup_missingFilename_nonInline_includesMathModeKotlitexMathSpan + @Test fun testParseHtml_withMathMarkup_missingFilename_doesNotIncludeImageSpan() { val parsedHtml = CustomHtmlContentHandler.fromHtml( html = MATH_WITHOUT_FILENAME_MARKUP, imageRetriever = mockImageRetriever, - customTagHandlers = tagHandlersWithMathSupport + customTagHandlers = tagHandlersWithInlineMathSupport ) val imageSpans = parsedHtml.getSpansFromWholeString(ImageSpan::class) @@ -186,7 +203,7 @@ class MathTagHandlerTest { CustomHtmlContentHandler.fromHtml( html = "$MATH_MARKUP_1 and $MATH_MARKUP_2", imageRetriever = mockImageRetriever, - customTagHandlers = tagHandlersWithMathSupport + customTagHandlers = tagHandlersWithInlineMathSupport ) val imageSpans = parsedHtml.getSpansFromWholeString(ImageSpan::class) @@ -198,7 +215,7 @@ class MathTagHandlerTest { CustomHtmlContentHandler.fromHtml( html = MATH_MARKUP_1, imageRetriever = mockImageRetriever, - customTagHandlers = tagHandlersWithMathSupport + customTagHandlers = tagHandlersWithInlineMathSupport ) verify(mockImageRetriever).loadDrawable(capture(stringCaptor), capture(retrieverTypeCaptor)) @@ -211,7 +228,7 @@ class MathTagHandlerTest { CustomHtmlContentHandler.fromHtml( html = "$MATH_MARKUP_2 and $MATH_MARKUP_1", imageRetriever = mockImageRetriever, - customTagHandlers = tagHandlersWithMathSupport + customTagHandlers = tagHandlersWithInlineMathSupport ) // Verify that both images are loaded in order. From 47b153e5da7d3f13906b293c09a59c707c868f2a Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 9 Feb 2022 21:48:51 -0800 Subject: [PATCH 127/162] Add tests, docs, and exemptions. --- .../android/app/parser/HtmlParserTest.kt | 58 +++++++- scripts/assets/test_file_exemptions.textproto | 1 + .../parser/html/CustomHtmlContentHandler.kt | 5 +- .../util/parser/html/MathTagHandler.kt | 3 +- .../android/util/parser/image/ImageLoader.kt | 13 +- .../util/parser/image/TestGlideImageLoader.kt | 15 ++ .../android/util/parser/math/BUILD.bazel | 5 +- .../util/parser/math/MathBitmapModelLoader.kt | 14 +- .../android/util/parser/math/MathModel.kt | 29 +++- .../util/parser/html/MathTagHandlerTest.kt | 129 +++++++++++++----- .../util/parser/image/UrlImageParserTest.kt | 55 +++++++- .../android/util/parser/math/BUILD.bazel | 19 +++ .../android/util/parser/math/MathModelTest.kt | 126 +++++++++++++++++ 13 files changed, 420 insertions(+), 52 deletions(-) create mode 100644 utility/src/test/java/org/oppia/android/util/parser/math/BUILD.bazel create mode 100644 utility/src/test/java/org/oppia/android/util/parser/math/MathModelTest.kt diff --git a/app/src/sharedTest/java/org/oppia/android/app/parser/HtmlParserTest.kt b/app/src/sharedTest/java/org/oppia/android/app/parser/HtmlParserTest.kt index 39e4c3a8241..7a92da4f1f3 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/parser/HtmlParserTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/parser/HtmlParserTest.kt @@ -548,9 +548,61 @@ class HtmlParserTest { onView(withId(R.id.test_html_content_text_view)).perform(click()) } - // TODO: finish tests. - // testHtmlContent_withMathTag_missingFileName_inlineMode_loadsNonMathModeKotlitexMathSpan - // testHtmlContent_withMathTag_missingFileName_nonInlineMode_loadsMathModeKotlitexMathSpan + @Test + fun testHtmlContent_withMathTag_missingFileName_inlineMode_loadsNonMathModeKotlitexMathSpan() { + val htmlParser = htmlParserFactory.create( + resourceBucketName, + entityType = "", + entityId = "", + imageCenterAlign = true, + ) + activityRule.scenario.runWithActivity { + val textView: TextView = it.findViewById(R.id.test_html_content_text_view) + val htmlResult: Spannable = htmlParser.parseOppiaHtml( + "" + + "", + textView, + supportsLinks = true, + supportsConceptCards = true + ) + textView.text = htmlResult + } + + // The rendering mode should be inline for this render type. + val loadedInlineImages = testGlideImageLoader.getLoadedMathDrawables() + assertThat(loadedInlineImages).hasSize(1) + assertThat(loadedInlineImages.first().rawLatex).isEqualTo("\\frac{2}{5}") + assertThat(loadedInlineImages.first().useInlineRendering).isTrue() + } + + @Test + fun testHtmlContent_withMathTag_missingFileName_blockMode_loadsMathModeKotlitexMathSpan() { + val htmlParser = htmlParserFactory.create( + resourceBucketName, + entityType = "", + entityId = "", + imageCenterAlign = true, + ) + activityRule.scenario.runWithActivity { + val textView: TextView = it.findViewById(R.id.test_html_content_text_view) + val htmlResult: Spannable = htmlParser.parseOppiaHtml( + "" + + "", + textView, + supportsLinks = true, + supportsConceptCards = true + ) + textView.text = htmlResult + } + + // The rendering mode should be non-inline for this render type. + val loadedInlineImages = testGlideImageLoader.getLoadedMathDrawables() + assertThat(loadedInlineImages).hasSize(1) + assertThat(loadedInlineImages.first().rawLatex).isEqualTo("\\frac{2}{5}") + assertThat(loadedInlineImages.first().useInlineRendering).isFalse() + } @Test fun testHtmlContent_withMathTag_loadsTextSvg() { diff --git a/scripts/assets/test_file_exemptions.textproto b/scripts/assets/test_file_exemptions.textproto index 6b58b289fd9..09ce31eb350 100644 --- a/scripts/assets/test_file_exemptions.textproto +++ b/scripts/assets/test_file_exemptions.textproto @@ -750,6 +750,7 @@ exempted_file_path: "utility/src/main/java/org/oppia/android/util/parser/image/R exempted_file_path: "utility/src/main/java/org/oppia/android/util/parser/image/RepositoryModelLoader.kt" exempted_file_path: "utility/src/main/java/org/oppia/android/util/parser/image/TestGlideImageLoader.kt" exempted_file_path: "utility/src/main/java/org/oppia/android/util/parser/image/TextPictureDrawable.kt" +exempted_file_path: "utility/src/main/java/org/oppia/android/util/parser/math/MathBitmapModelLoader.kt" exempted_file_path: "utility/src/main/java/org/oppia/android/util/parser/svg/BlockPictureDrawable.kt" exempted_file_path: "utility/src/main/java/org/oppia/android/util/parser/svg/BlockSvgDrawableTranscoder.kt" exempted_file_path: "utility/src/main/java/org/oppia/android/util/parser/svg/ScalableVectorGraphic.kt" diff --git a/utility/src/main/java/org/oppia/android/util/parser/html/CustomHtmlContentHandler.kt b/utility/src/main/java/org/oppia/android/util/parser/html/CustomHtmlContentHandler.kt index d8952fc0c9b..ea2011ba47b 100644 --- a/utility/src/main/java/org/oppia/android/util/parser/html/CustomHtmlContentHandler.kt +++ b/utility/src/main/java/org/oppia/android/util/parser/html/CustomHtmlContentHandler.kt @@ -173,7 +173,10 @@ class CustomHtmlContentHandler private constructor( /** Returns a new [Drawable] corresponding to the specified image filename and [Type]. */ fun loadDrawable(filename: String, type: Type): Drawable - // TODO: add docs & tests. + /** + * Returns a new [Drawable] representing a cached render of the specified [rawLatex] for the + * given [lineHeight] and for the rendering [type]. + */ fun loadMathDrawable(rawLatex: String, lineHeight: Float, type: Type): Drawable /** Corresponds to the types of images that can be retrieved. */ diff --git a/utility/src/main/java/org/oppia/android/util/parser/html/MathTagHandler.kt b/utility/src/main/java/org/oppia/android/util/parser/html/MathTagHandler.kt index 7aed0047ac6..cbcdba0c35c 100644 --- a/utility/src/main/java/org/oppia/android/util/parser/html/MathTagHandler.kt +++ b/utility/src/main/java/org/oppia/android/util/parser/html/MathTagHandler.kt @@ -37,8 +37,7 @@ class MathTagHandler( val content = MathContent.parseMathContent( attributes.getJsonObjectValue(CUSTOM_MATH_MATH_CONTENT_ATTRIBUTE) ) - // TODO: add tests for these cases. - // TODO: file TODO for fixing vertical alignment. + // TODO(#4170): Fix vertical alignment centering for inline cached LaTeX. val useInlineRendering = when (attributes.getValue(CUSTOM_MATH_RENDER_TYPE_ATTRIBUTE)) { "inline" -> true "block" -> false diff --git a/utility/src/main/java/org/oppia/android/util/parser/image/ImageLoader.kt b/utility/src/main/java/org/oppia/android/util/parser/image/ImageLoader.kt index 8013a793cc9..b9f91573584 100644 --- a/utility/src/main/java/org/oppia/android/util/parser/image/ImageLoader.kt +++ b/utility/src/main/java/org/oppia/android/util/parser/image/ImageLoader.kt @@ -31,8 +31,8 @@ interface ImageLoader { ) /** - * Same as [loadBlockSvg] except this specifically loads a [TextPictureDrawable] which can be rendered - * in-line with text. + * Same as [loadBlockSvg] except this specifically loads a [TextPictureDrawable] which can be + * rendered in-line with text. */ fun loadTextSvg( imageUrl: String, @@ -41,7 +41,8 @@ interface ImageLoader { ) /** - * Loads the specified [imageDrawable] resource into the specified [target]. + * Loads the specified [imageDrawableResId] resource into the specified [target]. + * * Optional [transformations] may be applied to the image. */ fun loadDrawable( @@ -50,7 +51,11 @@ interface ImageLoader { transformations: List = listOf() ) - // TODO: add docs & tests & verify tests. + /** + * Loads the specified cached math [rawLatex] into the specified [target] with the provided font + * [lineHeight] setting and details on how the image will be displayed indicated by + * [useInlineRendering]. + */ fun loadMathDrawable( rawLatex: String, lineHeight: Float, useInlineRendering: Boolean, target: ImageTarget ) diff --git a/utility/src/main/java/org/oppia/android/util/parser/image/TestGlideImageLoader.kt b/utility/src/main/java/org/oppia/android/util/parser/image/TestGlideImageLoader.kt index 800b7b5b566..c6f653aaba3 100644 --- a/utility/src/main/java/org/oppia/android/util/parser/image/TestGlideImageLoader.kt +++ b/utility/src/main/java/org/oppia/android/util/parser/image/TestGlideImageLoader.kt @@ -5,6 +5,7 @@ import android.graphics.drawable.Drawable import org.oppia.android.util.parser.svg.BlockPictureDrawable import javax.inject.Inject import javax.inject.Singleton +import org.oppia.android.util.parser.math.MathModel /** * [TestGlideImageLoader] is designed to be used in tests. It uses real [GlideImageLoader] @@ -18,6 +19,7 @@ class TestGlideImageLoader @Inject constructor( private val loadedBitmaps = mutableListOf() private val loadedBlockSvgs = mutableListOf() private val loadedTextSvgs = mutableListOf() + private val loadedMathDrawables = mutableListOf() override fun loadBitmap( imageUrl: String, @@ -62,6 +64,16 @@ class TestGlideImageLoader @Inject constructor( } } + override fun loadMathDrawable( + rawLatex: String, + lineHeight: Float, + useInlineRendering: Boolean, + target: ImageTarget + ) { + loadedMathDrawables += MathModel(rawLatex, lineHeight, useInlineRendering) + glideImageLoader.loadMathDrawable(rawLatex, lineHeight, useInlineRendering, target) + } + /** * Returns the list of image URLs that have been loaded as bitmaps since the start of the * application. @@ -79,4 +91,7 @@ class TestGlideImageLoader @Inject constructor( * start of the application. */ fun getLoadedTextSvgs(): List = loadedTextSvgs + + /** Returns the list of renderable math LaTeX [MathModel]s that have been loaded as drawables. */ + fun getLoadedMathDrawables(): List = loadedMathDrawables } diff --git a/utility/src/main/java/org/oppia/android/util/parser/math/BUILD.bazel b/utility/src/main/java/org/oppia/android/util/parser/math/BUILD.bazel index 7e021bddea2..0592c099da9 100644 --- a/utility/src/main/java/org/oppia/android/util/parser/math/BUILD.bazel +++ b/utility/src/main/java/org/oppia/android/util/parser/math/BUILD.bazel @@ -9,7 +9,10 @@ kt_android_library( srcs = [ "MathModel.kt", ], - visibility = ["//utility/src/main/java/org/oppia/android/util/parser/image:__pkg__"], + visibility = [ + "//:oppia_testing_visibility", + "//utility/src/main/java/org/oppia/android/util/parser/image:__pkg__", + ], deps = [ "//third_party:com_github_bumptech_glide_glide", ], diff --git a/utility/src/main/java/org/oppia/android/util/parser/math/MathBitmapModelLoader.kt b/utility/src/main/java/org/oppia/android/util/parser/math/MathBitmapModelLoader.kt index c4d53d9d754..33cbd32e9cc 100644 --- a/utility/src/main/java/org/oppia/android/util/parser/math/MathBitmapModelLoader.kt +++ b/utility/src/main/java/org/oppia/android/util/parser/math/MathBitmapModelLoader.kt @@ -20,10 +20,21 @@ import io.github.karino2.kotlitex.view.MathExpressionSpan import java.io.ByteArrayOutputStream import java.nio.ByteBuffer -// Reference: https://bumptech.github.io/glide/tut/custom-modelloader.html#writing-the-modelloader. +/** + * [ModelLoader] for rendering and caching bitmap representations of LaTeX represented by + * [MathModel]s. + * + * This loader provides support for loading a bitmap version of rendered LaTeX that's been + * pre-rendered into a Glide-cacheable bitmap. Note that this is computationally more expensive to + * use than direct rendering since it includes steps to encode the image on-disk, but it's far more + * performant for repeated rendering of the the LaTeX (real-time LaTeX rendering is very expensive + * and blocks the main thread). + */ class MathBitmapModelLoader private constructor( private val applicationContext: Context ): ModelLoader { + // Ref: https://bumptech.github.io/glide/tut/custom-modelloader.html#writing-the-modelloader. + override fun buildLoadData( model: MathModel, width: Int, height: Int, options: Options ): ModelLoader.LoadData { @@ -96,6 +107,7 @@ class MathBitmapModelLoader private constructor( override fun getDataSource(): DataSource = DataSource.REMOTE } + /** [ModelLoaderFactory] for creating new [MathBitmapModelLoader]s. */ class Factory( private val applicationContext: Context ): ModelLoaderFactory { diff --git a/utility/src/main/java/org/oppia/android/util/parser/math/MathModel.kt b/utility/src/main/java/org/oppia/android/util/parser/math/MathModel.kt index 07a38d37bca..00471f0bc0f 100644 --- a/utility/src/main/java/org/oppia/android/util/parser/math/MathModel.kt +++ b/utility/src/main/java/org/oppia/android/util/parser/math/MathModel.kt @@ -4,28 +4,47 @@ import com.bumptech.glide.load.Key import java.nio.ByteBuffer import java.security.MessageDigest +/** + * Represents a set of LaTeX that can be rendered as a single bitmap. + * + * @property rawLatex the LaTeX to render + * @property lineHeight the height (in pixels) of a text line (to help scale the LaTeX) + * @property useInlineRendering whether the LaTeX will be inlined with text + */ data class MathModel( val rawLatex: String, val lineHeight: Float, val useInlineRendering: Boolean ) { + /** Returns a Glide [Key] signature (see [MathModelSignature] for specifics). */ fun toKeySignature(): MathModelSignature = MathModelSignature.createSignature(rawLatex, lineHeight, useInlineRendering) - // Reference: http://bumptech.github.io/glide/doc/caching.html#custom-cache-invalidation. - // TODO: document that lineHeight is only stored up to 2 decimal places, and to use factory method. + /** + * Glide [Key] that provides caching support by allowing individual renderable math scenarios to + * be comparable based on select parameters. + * + * @property rawLatex the raw LaTeX string used to render a cached bitmap + * @property lineHeightHundredX an [Int] representation of the 100x scaled line height from + * [MathModel] (this is used to preserve up to 2 digits of the height, but any past that will + * be truncated to reduce cache size for highly reusable cached renders) + * @property useInlineRendering whether the render is formatted to be displayed in-line with text + */ data class MathModelSignature( val rawLatex: String, val lineHeightHundredX: Int, val useInlineRendering: Boolean ) : Key { + // Impl reference: http://bumptech.github.io/glide/doc/caching.html#custom-cache-invalidation. + override fun updateDiskCacheKey(messageDigest: MessageDigest) { val rawLatexBytes = rawLatex.encodeToByteArray() messageDigest.update(ByteBuffer.allocate(rawLatexBytes.size + Int.SIZE_BYTES + 1).apply { put(rawLatexBytes) - putInt(lineHeightHundredX) + putInt(lineHeightHundredX) put(if (useInlineRendering) 1 else 0) }.array()) } - companion object { - fun createSignature( + internal companion object { + /** Returns a new [MathModelSignature] for the specified [MathModel] properties. */ + internal fun createSignature( rawLatex: String, lineHeight: Float, useInlineRendering: Boolean ): MathModelSignature { val lineHeightHundredX = (lineHeight * 100f).toInt() diff --git a/utility/src/test/java/org/oppia/android/util/parser/html/MathTagHandlerTest.kt b/utility/src/test/java/org/oppia/android/util/parser/html/MathTagHandlerTest.kt index 646179411f7..96c35f4a238 100644 --- a/utility/src/test/java/org/oppia/android/util/parser/html/MathTagHandlerTest.kt +++ b/utility/src/test/java/org/oppia/android/util/parser/html/MathTagHandlerTest.kt @@ -12,6 +12,7 @@ import dagger.Binds import dagger.BindsInstance import dagger.Component import dagger.Module +import io.github.karino2.kotlitex.view.MathExpressionSpan import org.junit.Before import org.junit.Rule import org.junit.Test @@ -36,6 +37,7 @@ import org.robolectric.annotation.LooperMode import javax.inject.Inject import javax.inject.Singleton import kotlin.reflect.KClass +import org.mockito.Mockito.verifyNoMoreInteractions private const val MATH_MARKUP_1 = "" + ":&quot;\\\\frac{2}{5}&quot;}\">" + +private const val MATH_WITHOUT_FILENAME_RENDER_TYPE_INLINE_MARKUP = + "" + +private const val MATH_WITHOUT_FILENAME_RENDER_TYPE_BLOCK_MARKUP = + "" /** Tests for [MathTagHandler]. */ // FunctionName: test names are conventionally named with underscores. @@ -71,31 +83,27 @@ class MathTagHandlerTest { @Mock lateinit var mockImageRetriever: FakeImageRetriever @Captor lateinit var stringCaptor: ArgumentCaptor @Captor lateinit var retrieverTypeCaptor: ArgumentCaptor + @Captor lateinit var floatCaptor: ArgumentCaptor @Inject lateinit var context: Context @Inject lateinit var consoleLogger: ConsoleLogger private lateinit var noTagHandlers: Map - private lateinit var tagHandlersWithInlineMathSupport: Map - private lateinit var tagHandlersWithBlockMathSupport: Map + private lateinit var tagHandlersWithCachedMathSupport: Map + private lateinit var tagHandlersWithUncachedMathSupport: Map @Before fun setUp() { setUpTestApplicationComponent() noTagHandlers = mapOf() - tagHandlersWithInlineMathSupport = mapOf( - CUSTOM_MATH_TAG to createMathTagHandler(useInlineRendering = true) + tagHandlersWithCachedMathSupport = mapOf( + CUSTOM_MATH_TAG to createMathTagHandler(cacheLatexRendering = true) ) - tagHandlersWithBlockMathSupport = mapOf( - CUSTOM_MATH_TAG to createMathTagHandler(useInlineRendering = false) + tagHandlersWithUncachedMathSupport = mapOf( + CUSTOM_MATH_TAG to createMathTagHandler(cacheLatexRendering = false) ) } - private fun createMathTagHandler(useInlineRendering: Boolean): MathTagHandler { - // Pick an arbitrary line height since rendering doesn't actually happen in the test env. - return MathTagHandler(consoleLogger, context.assets, lineHeight = 10.0f, useInlineRendering) - } - // TODO(#3085): Introduce test for verifying that the error log scenario is logged correctly. @Test @@ -104,7 +112,7 @@ class MathTagHandlerTest { CustomHtmlContentHandler.fromHtml( html = "", imageRetriever = mockImageRetriever, - customTagHandlers = tagHandlersWithInlineMathSupport + customTagHandlers = tagHandlersWithCachedMathSupport ) val imageSpans = parsedHtml.getSpansFromWholeString(ImageSpan::class) @@ -117,9 +125,23 @@ class MathTagHandlerTest { CustomHtmlContentHandler.fromHtml( html = MATH_MARKUP_1, imageRetriever = mockImageRetriever, - customTagHandlers = tagHandlersWithInlineMathSupport + customTagHandlers = tagHandlersWithCachedMathSupport + ) + + val imageSpans = parsedHtml.getSpansFromWholeString(ImageSpan::class) + assertThat(imageSpans).hasLength(1) + } + + @Test + fun testParseHtml_withMathMarkup_missingRawLatex_includesImageSpan() { + val parsedHtml = + CustomHtmlContentHandler.fromHtml( + html = MATH_WITHOUT_RAW_LATEX_MARKUP, + imageRetriever = mockImageRetriever, + customTagHandlers = tagHandlersWithCachedMathSupport ) + // There is an image span since the filename is still present. val imageSpans = parsedHtml.getSpansFromWholeString(ImageSpan::class) assertThat(imageSpans).hasLength(1) } @@ -130,7 +152,7 @@ class MathTagHandlerTest { CustomHtmlContentHandler.fromHtml( html = MATH_MARKUP_1, imageRetriever = mockImageRetriever, - customTagHandlers = tagHandlersWithInlineMathSupport + customTagHandlers = tagHandlersWithCachedMathSupport ) // The image only adds a control character, so there aren't any human-readable characters. @@ -145,43 +167,83 @@ class MathTagHandlerTest { CustomHtmlContentHandler.fromHtml( html = MATH_WITHOUT_CONTENT_VALUE_MARKUP, imageRetriever = mockImageRetriever, - customTagHandlers = tagHandlersWithInlineMathSupport + customTagHandlers = tagHandlersWithCachedMathSupport ) val imageSpans = parsedHtml.getSpansFromWholeString(ImageSpan::class) assertThat(imageSpans).isEmpty() } - // TODO: update this to say it DOES include the span. @Test - fun testParseHtml_withMathMarkup_missingRawLatex_doesNotIncludeImageSpan() { + fun testParseHtml_withMathMarkup_missingFilename_includesCachedInlineLatexImageSpan() { val parsedHtml = CustomHtmlContentHandler.fromHtml( - html = MATH_WITHOUT_RAW_LATEX_MARKUP, + html = MATH_WITHOUT_FILENAME_MARKUP, imageRetriever = mockImageRetriever, - customTagHandlers = tagHandlersWithInlineMathSupport + customTagHandlers = tagHandlersWithCachedMathSupport ) + // The image span is a cached bitmap loaded from LaTeX. val imageSpans = parsedHtml.getSpansFromWholeString(ImageSpan::class) - assertThat(imageSpans).isEmpty() + assertThat(imageSpans).hasLength(1) + verify(mockImageRetriever).loadMathDrawable( + capture(stringCaptor), capture(floatCaptor), capture(retrieverTypeCaptor) + ) + assertThat(stringCaptor.value).isEqualTo("\\frac{2}{5}") + assertThat(retrieverTypeCaptor.value).isEqualTo(ImageRetriever.Type.INLINE_TEXT_IMAGE) } + @Test + fun testParseHtml_withMathMarkup_missingFilename_inlineMode_includesCachedInlineLatexImageSpan() { + val parsedHtml = + CustomHtmlContentHandler.fromHtml( + html = MATH_WITHOUT_FILENAME_RENDER_TYPE_INLINE_MARKUP, + imageRetriever = mockImageRetriever, + customTagHandlers = tagHandlersWithCachedMathSupport + ) - // TODO: finish tests - // Replace below: testParseHtml_withMathMarkup_missingFilename_includesNonMathModeKotlitexMathSpan - // testParseHtml_withMathMarkup_missingFilename_nonInline_includesMathModeKotlitexMathSpan + // The image span is a cached bitmap loaded from LaTeX. + val imageSpans = parsedHtml.getSpansFromWholeString(ImageSpan::class) + assertThat(imageSpans).hasLength(1) + verify(mockImageRetriever).loadMathDrawable( + capture(stringCaptor), capture(floatCaptor), capture(retrieverTypeCaptor) + ) + assertThat(stringCaptor.value).isEqualTo("\\frac{2}{5}") + assertThat(retrieverTypeCaptor.value).isEqualTo(ImageRetriever.Type.INLINE_TEXT_IMAGE) + } @Test - fun testParseHtml_withMathMarkup_missingFilename_doesNotIncludeImageSpan() { + fun testParseHtml_withMathMarkup_missingFilename_blockMode_includesCachedBlockLatexImageSpan() { val parsedHtml = CustomHtmlContentHandler.fromHtml( - html = MATH_WITHOUT_FILENAME_MARKUP, + html = MATH_WITHOUT_FILENAME_RENDER_TYPE_BLOCK_MARKUP, imageRetriever = mockImageRetriever, - customTagHandlers = tagHandlersWithInlineMathSupport + customTagHandlers = tagHandlersWithCachedMathSupport ) + // The image span is a cached bitmap loaded from LaTeX. val imageSpans = parsedHtml.getSpansFromWholeString(ImageSpan::class) - assertThat(imageSpans).isEmpty() + assertThat(imageSpans).hasLength(1) + verify(mockImageRetriever).loadMathDrawable( + capture(stringCaptor), capture(floatCaptor), capture(retrieverTypeCaptor) + ) + assertThat(stringCaptor.value).isEqualTo("\\frac{2}{5}") + assertThat(retrieverTypeCaptor.value).isEqualTo(ImageRetriever.Type.BLOCK_IMAGE) + } + + @Test + fun testParseHtml_withMathMarkup_cachingOff_includesMathSpan() { + val parsedHtml = + CustomHtmlContentHandler.fromHtml( + html = MATH_WITHOUT_FILENAME_MARKUP, + imageRetriever = mockImageRetriever, + customTagHandlers = tagHandlersWithUncachedMathSupport + ) + + // The image span is a direct math expression since caching is off. + val imageSpans = parsedHtml.getSpansFromWholeString(MathExpressionSpan::class) + assertThat(imageSpans).hasLength(1) + verifyNoMoreInteractions(mockImageRetriever) // No cached image loading. } @Test @@ -203,7 +265,7 @@ class MathTagHandlerTest { CustomHtmlContentHandler.fromHtml( html = "$MATH_MARKUP_1 and $MATH_MARKUP_2", imageRetriever = mockImageRetriever, - customTagHandlers = tagHandlersWithInlineMathSupport + customTagHandlers = tagHandlersWithCachedMathSupport ) val imageSpans = parsedHtml.getSpansFromWholeString(ImageSpan::class) @@ -215,7 +277,7 @@ class MathTagHandlerTest { CustomHtmlContentHandler.fromHtml( html = MATH_MARKUP_1, imageRetriever = mockImageRetriever, - customTagHandlers = tagHandlersWithInlineMathSupport + customTagHandlers = tagHandlersWithCachedMathSupport ) verify(mockImageRetriever).loadDrawable(capture(stringCaptor), capture(retrieverTypeCaptor)) @@ -228,7 +290,7 @@ class MathTagHandlerTest { CustomHtmlContentHandler.fromHtml( html = "$MATH_MARKUP_2 and $MATH_MARKUP_1", imageRetriever = mockImageRetriever, - customTagHandlers = tagHandlersWithInlineMathSupport + customTagHandlers = tagHandlersWithCachedMathSupport ) // Verify that both images are loaded in order. @@ -242,6 +304,11 @@ class MathTagHandlerTest { .inOrder() } + private fun createMathTagHandler(cacheLatexRendering: Boolean): MathTagHandler { + // Pick an arbitrary line height since rendering doesn't actually happen in tests. + return MathTagHandler(consoleLogger, context.assets, lineHeight = 10.0f, cacheLatexRendering) + } + private fun Spannable.getSpansFromWholeString(spanClass: KClass): Array = getSpans(/* start= */ 0, /* end= */ length, spanClass.javaObjectType) diff --git a/utility/src/test/java/org/oppia/android/util/parser/image/UrlImageParserTest.kt b/utility/src/test/java/org/oppia/android/util/parser/image/UrlImageParserTest.kt index 28b61d3a885..9ba03edd630 100644 --- a/utility/src/test/java/org/oppia/android/util/parser/image/UrlImageParserTest.kt +++ b/utility/src/test/java/org/oppia/android/util/parser/image/UrlImageParserTest.kt @@ -28,6 +28,8 @@ import org.oppia.android.util.parser.html.CustomHtmlContentHandler.ImageRetrieve import org.robolectric.annotation.LooperMode import javax.inject.Inject import javax.inject.Singleton +import org.oppia.android.util.parser.html.CustomHtmlContentHandler.ImageRetriever.Type.BLOCK_IMAGE +import org.oppia.android.util.parser.html.CustomHtmlContentHandler.ImageRetriever.Type.INLINE_TEXT_IMAGE /** Tests for [UrlImageParser]. */ @RunWith(AndroidJUnit4::class) @@ -80,7 +82,7 @@ class UrlImageParserTest { @Test fun testLoadDrawable_bitmap_blockType_loadsBitmapImage() { - urlImageParser.loadDrawable("test_image.png", ImageRetriever.Type.BLOCK_IMAGE) + urlImageParser.loadDrawable("test_image.png", BLOCK_IMAGE) val loadedBitmaps = testGlideImageLoader.getLoadedBitmaps() assertThat(loadedBitmaps).hasSize(1) @@ -89,7 +91,7 @@ class UrlImageParserTest { @Test fun testLoadDrawable_bitmap_inlineType_loadsBitmapImage() { - urlImageParser.loadDrawable("test_image.png", ImageRetriever.Type.INLINE_TEXT_IMAGE) + urlImageParser.loadDrawable("test_image.png", INLINE_TEXT_IMAGE) // The request to load the bitmap inline is ignored since inline bitmaps aren't supported. The // bitmap is instead loaded in block format. @@ -101,7 +103,7 @@ class UrlImageParserTest { @Test fun testLoadDrawable_svg_blockType_loadsSvgBlockImage() { - urlImageParser.loadDrawable("test_image.svg", ImageRetriever.Type.BLOCK_IMAGE) + urlImageParser.loadDrawable("test_image.svg", BLOCK_IMAGE) val loadedBitmaps = testGlideImageLoader.getLoadedBlockSvgs() assertThat(loadedBitmaps).hasSize(1) @@ -110,7 +112,7 @@ class UrlImageParserTest { @Test fun testLoadDrawable_svg_inlineType_loadsSvgTextImage() { - urlImageParser.loadDrawable("test_image.svg", ImageRetriever.Type.INLINE_TEXT_IMAGE) + urlImageParser.loadDrawable("test_image.svg", INLINE_TEXT_IMAGE) // The request to load the bitmap inline is ignored since inline bitmaps aren't supported. val loadedBitmaps = testGlideImageLoader.getLoadedTextSvgs() @@ -118,6 +120,51 @@ class UrlImageParserTest { assertThat(loadedBitmaps.first()).contains("test_image.svg") } + @Test + fun testLoadDrawable_latex_inlineType_loadsInlineLatexImage() { + urlImageParser.loadMathDrawable( + rawLatex = "\\frac{2}{6}", lineHeight = 20f, type = INLINE_TEXT_IMAGE + ) + + val mathDrawables = testGlideImageLoader.getLoadedMathDrawables() + assertThat(mathDrawables).hasSize(1) + assertThat(mathDrawables.first().rawLatex).isEqualTo("\\frac{2}{6}") + assertThat(mathDrawables.first().lineHeight).isWithin(1e-5f).of(20f) + assertThat(mathDrawables.first().useInlineRendering).isTrue() + } + + @Test + fun testLoadDrawable_latex_blockType_loadsBlockLatexImage() { + urlImageParser.loadMathDrawable( + rawLatex = "\\frac{2}{6}", lineHeight = 20f, type = BLOCK_IMAGE + ) + + val mathDrawables = testGlideImageLoader.getLoadedMathDrawables() + assertThat(mathDrawables).hasSize(1) + assertThat(mathDrawables.first().rawLatex).isEqualTo("\\frac{2}{6}") + assertThat(mathDrawables.first().lineHeight).isWithin(1e-5f).of(20f) + assertThat(mathDrawables.first().useInlineRendering).isFalse() + } + + @Test + fun testLoadDrawable_latex_multiple_loadsEachLatexImage() { + urlImageParser.loadMathDrawable( + rawLatex = "\\frac{1}{6}", lineHeight = 20f, type = INLINE_TEXT_IMAGE + ) + urlImageParser.loadMathDrawable( + rawLatex = "\\frac{2}{6}", lineHeight = 20f, type = INLINE_TEXT_IMAGE + ) + urlImageParser.loadMathDrawable( + rawLatex = "\\frac{2}{6}", lineHeight = 19f, type = INLINE_TEXT_IMAGE + ) + urlImageParser.loadMathDrawable( + rawLatex = "\\frac{2}{6}", lineHeight = 20f, type = BLOCK_IMAGE + ) + + val mathDrawables = testGlideImageLoader.getLoadedMathDrawables() + assertThat(mathDrawables).hasSize(4) + } + private fun setUpTestApplicationComponent() { DaggerUrlImageParserTest_TestApplicationComponent.builder() .setApplication(ApplicationProvider.getApplicationContext()) diff --git a/utility/src/test/java/org/oppia/android/util/parser/math/BUILD.bazel b/utility/src/test/java/org/oppia/android/util/parser/math/BUILD.bazel new file mode 100644 index 00000000000..3fb1fc751be --- /dev/null +++ b/utility/src/test/java/org/oppia/android/util/parser/math/BUILD.bazel @@ -0,0 +1,19 @@ +""" +Tests for the components used to render LaTeX math expressions. +""" + +load("//:oppia_android_test.bzl", "oppia_android_test") + +oppia_android_test( + name = "MathModelTest", + srcs = ["MathModelTest.kt"], + custom_package = "org.oppia.android.util.parser.math", + test_class = "org.oppia.android.util.parser.math.MathModelTest", + test_manifest = "//utility:test_manifest", + deps = [ + "//third_party:com_google_truth_truth", + "//third_party:junit_junit", + "//third_party:robolectric_android-all", + "//utility/src/main/java/org/oppia/android/util/parser/math:math_latex_model", + ], +) diff --git a/utility/src/test/java/org/oppia/android/util/parser/math/MathModelTest.kt b/utility/src/test/java/org/oppia/android/util/parser/math/MathModelTest.kt new file mode 100644 index 00000000000..0ebfd8a1cbb --- /dev/null +++ b/utility/src/test/java/org/oppia/android/util/parser/math/MathModelTest.kt @@ -0,0 +1,126 @@ +package org.oppia.android.util.parser.math + +import com.google.common.truth.Truth.assertThat +import java.security.MessageDigest +import org.junit.Test + +/** Tests for [MathModel]. */ +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") +class MathModelTest { + @Test + fun testToKeySignature_sameModelByValues_returnsSameKeyWithSameDigest() { + val model1 = MathModel(rawLatex = "\\frac{2}{6}", lineHeight = 21.5f, useInlineRendering = true) + val model2 = MathModel(rawLatex = "\\frac{2}{6}", lineHeight = 21.5f, useInlineRendering = true) + val digest1 = MessageDigest.getInstance("SHA-256") + val digest2 = MessageDigest.getInstance("SHA-256") + + val key1 = model1.toKeySignature() + val key2 = model2.toKeySignature() + key1.updateDiskCacheKey(digest1) + key2.updateDiskCacheKey(digest2) + + assertThat(key1).isEqualTo(key2) + assertThat(key1.hashCode()).isEqualTo(key2.hashCode()) + assertThat(digest1.digest()).isEqualTo(digest2.digest()) + assertThat(model1).isEqualTo(model2) + } + + @Test + fun testToKeySignature_differentModelByLatex_returnsDifferentKeyWithDifferentDigest() { + val model1 = MathModel(rawLatex = "\\frac{2}{6}", lineHeight = 21.5f, useInlineRendering = true) + val model2 = MathModel(rawLatex = "\\frac{3}{6}", lineHeight = 21.5f, useInlineRendering = true) + val digest1 = MessageDigest.getInstance("SHA-256") + val digest2 = MessageDigest.getInstance("SHA-256") + + val key1 = model1.toKeySignature() + val key2 = model2.toKeySignature() + key1.updateDiskCacheKey(digest1) + key2.updateDiskCacheKey(digest2) + + // Since the LaTeX differs, nothing should match. + assertThat(key1).isNotEqualTo(key2) + assertThat(key1.hashCode()).isNotEqualTo(key2.hashCode()) + assertThat(digest1.digest()).isNotEqualTo(digest2.digest()) + assertThat(model1).isNotEqualTo(model2) + } + + @Test + fun testToKeySignature_differentModelByLineHeight_returnsDifferentKeyWithDifferentDigest() { + val model1 = MathModel(rawLatex = "\\frac{2}{6}", lineHeight = 21.5f, useInlineRendering = true) + val model2 = MathModel(rawLatex = "\\frac{2}{6}", lineHeight = 20.5f, useInlineRendering = true) + val digest1 = MessageDigest.getInstance("SHA-256") + val digest2 = MessageDigest.getInstance("SHA-256") + + val key1 = model1.toKeySignature() + val key2 = model2.toKeySignature() + key1.updateDiskCacheKey(digest1) + key2.updateDiskCacheKey(digest2) + + // Since the line height differs, nothing should match. + assertThat(key1).isNotEqualTo(key2) + assertThat(key1.hashCode()).isNotEqualTo(key2.hashCode()) + assertThat(digest1.digest()).isNotEqualTo(digest2.digest()) + assertThat(model1).isNotEqualTo(model2) + } + + @Test + fun testToKeySignature_diffModelByLineHeight_withinTwoDecimals_returnsSameKeyWithSameDigest() { + val model1 = MathModel(rawLatex = "\\frac{2}{6}", lineHeight = 21.5f, useInlineRendering = true) + val model2 = + MathModel(rawLatex = "\\frac{2}{6}", lineHeight = 21.501f, useInlineRendering = true) + val digest1 = MessageDigest.getInstance("SHA-256") + val digest2 = MessageDigest.getInstance("SHA-256") + + val key1 = model1.toKeySignature() + val key2 = model2.toKeySignature() + key1.updateDiskCacheKey(digest1) + key2.updateDiskCacheKey(digest2) + + // The line heights are close enough that they're considered equal for key purposes (but are + // still different models). + assertThat(key1).isEqualTo(key2) + assertThat(key1.hashCode()).isEqualTo(key2.hashCode()) + assertThat(digest1.digest()).isEqualTo(digest2.digest()) + assertThat(model1).isNotEqualTo(model2) + } + + @Test + fun testToKeySignature_diffModelByLineHeight_outsideTwoDecimals_returnsDiffKeyWithDiffDigest() { + val model1 = MathModel(rawLatex = "\\frac{2}{6}", lineHeight = 21.5f, useInlineRendering = true) + val model2 = MathModel(rawLatex = "\\frac{2}{6}", lineHeight = 21.6f, useInlineRendering = true) + val digest1 = MessageDigest.getInstance("SHA-256") + val digest2 = MessageDigest.getInstance("SHA-256") + + val key1 = model1.toKeySignature() + val key2 = model2.toKeySignature() + key1.updateDiskCacheKey(digest1) + key2.updateDiskCacheKey(digest2) + + // Since the line height differs, nothing should match. + assertThat(key1).isNotEqualTo(key2) + assertThat(key1.hashCode()).isNotEqualTo(key2.hashCode()) + assertThat(digest1.digest()).isNotEqualTo(digest2.digest()) + assertThat(model1).isNotEqualTo(model2) + } + + @Test + fun testToKeySignature_differentModelByInlineRendering_returnsDifferentKeyWithDifferentDigest() { + val model1 = MathModel(rawLatex = "\\frac{2}{6}", lineHeight = 21.5f, useInlineRendering = true) + val model2 = + MathModel(rawLatex = "\\frac{2}{6}", lineHeight = 21.5f, useInlineRendering = false) + val digest1 = MessageDigest.getInstance("SHA-256") + val digest2 = MessageDigest.getInstance("SHA-256") + + val key1 = model1.toKeySignature() + val key2 = model2.toKeySignature() + key1.updateDiskCacheKey(digest1) + key2.updateDiskCacheKey(digest2) + + // Since the inline rendering setting differs, nothing should match. + assertThat(key1).isNotEqualTo(key2) + assertThat(key1.hashCode()).isNotEqualTo(key2.hashCode()) + assertThat(digest1.digest()).isNotEqualTo(digest2.digest()) + assertThat(model1).isNotEqualTo(model2) + } +} From 0918b865c70193593440d6efef8c99b911e68b1d Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 9 Feb 2022 23:49:41 -0800 Subject: [PATCH 128/162] Update to fixed version of KotliTeX. The newer version correctly computes the bounds for rendered LaTeX. --- WORKSPACE | 2 +- utility/build.gradle | 2 +- .../oppia/android/util/parser/math/MathBitmapModelLoader.kt | 5 ++++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/WORKSPACE b/WORKSPACE index 1aad21315d9..d8d636e6637 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -133,7 +133,7 @@ git_repository( # min target SDK version to be compatible with Oppia. git_repository( name = "kotlitex", - commit = "95db69aaf1ef979fd20ef9a85bd4ad266515aa20", + commit = "4ed63498717af239bc20c9a27ffdcd8eaa13c918", remote = "https://github.com/oppia/kotlitex", ) diff --git a/utility/build.gradle b/utility/build.gradle index 14fa7ca50fd..d34421ef402 100644 --- a/utility/build.gradle +++ b/utility/build.gradle @@ -65,7 +65,7 @@ dependencies { 'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0-alpha03', 'androidx.work:work-runtime-ktx:2.4.0', 'com.github.oppia:androidsvg:6bd15f69caee3e6857fcfcd123023716b4adec1d', - 'com.github.oppia:kotlitex:95db69aaf1ef979fd20ef9a85bd4ad266515aa20', + 'com.github.oppia:kotlitex:4ed63498717af239bc20c9a27ffdcd8eaa13c918', 'com.github.bumptech.glide:glide:4.11.0', 'com.google.dagger:dagger:2.24', 'com.google.firebase:firebase-analytics-ktx:17.5.0', diff --git a/utility/src/main/java/org/oppia/android/util/parser/math/MathBitmapModelLoader.kt b/utility/src/main/java/org/oppia/android/util/parser/math/MathBitmapModelLoader.kt index 33cbd32e9cc..84ce796e701 100644 --- a/utility/src/main/java/org/oppia/android/util/parser/math/MathBitmapModelLoader.kt +++ b/utility/src/main/java/org/oppia/android/util/parser/math/MathBitmapModelLoader.kt @@ -74,7 +74,10 @@ class MathBitmapModelLoader private constructor( /* includepad= */ true ) - // Reference: https://stackoverflow.com/a/27631737/3689782. + // Reference for how Android manages different parts of text during rendering: + // https://stackoverflow.com/a/27631737/3689782. Note that the specifics of how text + // properties are used to compute these bounds are in the modified KotliTeX implementation + // (see the getter implementation for the property below). val bounds = span.drawableBounds val canvasBitmap = Bitmap.createBitmap(bounds.width(), bounds.height(), Bitmap.Config.ARGB_8888) From 6f2723da43884df2269ce5ba398260a45f2b9250 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 9 Feb 2022 23:52:56 -0800 Subject: [PATCH 129/162] Lint fixes. --- .../android/util/parser/html/HtmlParser.kt | 2 +- .../util/parser/image/GlideImageLoader.kt | 2 +- .../android/util/parser/image/ImageLoader.kt | 6 +++-- .../parser/image/RepositoryGlideModule.kt | 4 ++-- .../util/parser/image/TestGlideImageLoader.kt | 2 +- .../util/parser/image/UrlImageParser.kt | 10 ++++---- .../util/parser/math/MathBitmapModelLoader.kt | 11 +++++---- .../android/util/parser/math/MathModel.kt | 24 ++++++++++++------- .../util/parser/html/MathTagHandlerTest.kt | 2 +- .../util/parser/image/UrlImageParserTest.kt | 5 ++-- .../android/util/parser/math/MathModelTest.kt | 2 +- 11 files changed, 42 insertions(+), 28 deletions(-) diff --git a/utility/src/main/java/org/oppia/android/util/parser/html/HtmlParser.kt b/utility/src/main/java/org/oppia/android/util/parser/html/HtmlParser.kt index 168064f396a..b21abbc8bca 100755 --- a/utility/src/main/java/org/oppia/android/util/parser/html/HtmlParser.kt +++ b/utility/src/main/java/org/oppia/android/util/parser/html/HtmlParser.kt @@ -9,9 +9,9 @@ import android.widget.TextView import androidx.core.view.ViewCompat import org.oppia.android.util.logging.ConsoleLogger import org.oppia.android.util.parser.image.UrlImageParser -import javax.inject.Inject import org.oppia.android.util.platformparameter.CacheLatexRendering import org.oppia.android.util.platformparameter.PlatformParameterValue +import javax.inject.Inject /** Html Parser to parse custom Oppia tags with Android-compatible versions. */ class HtmlParser private constructor( diff --git a/utility/src/main/java/org/oppia/android/util/parser/image/GlideImageLoader.kt b/utility/src/main/java/org/oppia/android/util/parser/image/GlideImageLoader.kt index dd4dadc6856..c70575d97dc 100644 --- a/utility/src/main/java/org/oppia/android/util/parser/image/GlideImageLoader.kt +++ b/utility/src/main/java/org/oppia/android/util/parser/image/GlideImageLoader.kt @@ -11,6 +11,7 @@ import com.bumptech.glide.request.RequestOptions import org.oppia.android.util.caching.AssetRepository import org.oppia.android.util.caching.CacheAssetsLocally import org.oppia.android.util.caching.LoadImagesFromAssets +import org.oppia.android.util.parser.math.MathModel import org.oppia.android.util.parser.svg.BlockPictureDrawable import org.oppia.android.util.parser.svg.ScalableVectorGraphic import org.oppia.android.util.parser.svg.SvgBlurTransformation @@ -18,7 +19,6 @@ import org.oppia.android.util.parser.svg.SvgDecoder import org.oppia.android.util.parser.svg.SvgPictureDrawable import javax.inject.Inject import javax.inject.Singleton -import org.oppia.android.util.parser.math.MathModel /** An [ImageLoader] that uses Glide. */ @Singleton diff --git a/utility/src/main/java/org/oppia/android/util/parser/image/ImageLoader.kt b/utility/src/main/java/org/oppia/android/util/parser/image/ImageLoader.kt index b9f91573584..8ae318b67fb 100644 --- a/utility/src/main/java/org/oppia/android/util/parser/image/ImageLoader.kt +++ b/utility/src/main/java/org/oppia/android/util/parser/image/ImageLoader.kt @@ -1,7 +1,6 @@ package org.oppia.android.util.parser.image import android.graphics.Bitmap -import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.Drawable import androidx.annotation.DrawableRes import org.oppia.android.util.parser.svg.BlockPictureDrawable @@ -57,6 +56,9 @@ interface ImageLoader { * [useInlineRendering]. */ fun loadMathDrawable( - rawLatex: String, lineHeight: Float, useInlineRendering: Boolean, target: ImageTarget + rawLatex: String, + lineHeight: Float, + useInlineRendering: Boolean, + target: ImageTarget ) } diff --git a/utility/src/main/java/org/oppia/android/util/parser/image/RepositoryGlideModule.kt b/utility/src/main/java/org/oppia/android/util/parser/image/RepositoryGlideModule.kt index d1a44a07254..d2349ab3eec 100644 --- a/utility/src/main/java/org/oppia/android/util/parser/image/RepositoryGlideModule.kt +++ b/utility/src/main/java/org/oppia/android/util/parser/image/RepositoryGlideModule.kt @@ -5,6 +5,8 @@ import com.bumptech.glide.Glide import com.bumptech.glide.Registry import com.bumptech.glide.annotation.GlideModule import com.bumptech.glide.module.AppGlideModule +import org.oppia.android.util.parser.math.MathBitmapModelLoader +import org.oppia.android.util.parser.math.MathModel import org.oppia.android.util.parser.svg.BlockPictureDrawable import org.oppia.android.util.parser.svg.BlockSvgDrawableTranscoder import org.oppia.android.util.parser.svg.ScalableVectorGraphic @@ -12,8 +14,6 @@ import org.oppia.android.util.parser.svg.SvgDecoder import org.oppia.android.util.parser.svg.TextSvgDrawableTranscoder import java.io.InputStream import java.nio.ByteBuffer -import org.oppia.android.util.parser.math.MathBitmapModelLoader -import org.oppia.android.util.parser.math.MathModel /** * Custom [AppGlideModule] to enable loading images from diff --git a/utility/src/main/java/org/oppia/android/util/parser/image/TestGlideImageLoader.kt b/utility/src/main/java/org/oppia/android/util/parser/image/TestGlideImageLoader.kt index c6f653aaba3..82acfc150b6 100644 --- a/utility/src/main/java/org/oppia/android/util/parser/image/TestGlideImageLoader.kt +++ b/utility/src/main/java/org/oppia/android/util/parser/image/TestGlideImageLoader.kt @@ -2,10 +2,10 @@ package org.oppia.android.util.parser.image import android.graphics.Bitmap import android.graphics.drawable.Drawable +import org.oppia.android.util.parser.math.MathModel import org.oppia.android.util.parser.svg.BlockPictureDrawable import javax.inject.Inject import javax.inject.Singleton -import org.oppia.android.util.parser.math.MathModel /** * [TestGlideImageLoader] is designed to be used in tests. It uses real [GlideImageLoader] diff --git a/utility/src/main/java/org/oppia/android/util/parser/image/UrlImageParser.kt b/utility/src/main/java/org/oppia/android/util/parser/image/UrlImageParser.kt index 161cb4c66bf..a72a533170d 100644 --- a/utility/src/main/java/org/oppia/android/util/parser/image/UrlImageParser.kt +++ b/utility/src/main/java/org/oppia/android/util/parser/image/UrlImageParser.kt @@ -19,11 +19,11 @@ import org.oppia.android.util.R import org.oppia.android.util.locale.OppiaLocale import org.oppia.android.util.logging.ConsoleLogger import org.oppia.android.util.parser.html.CustomHtmlContentHandler.ImageRetriever +import org.oppia.android.util.parser.html.CustomHtmlContentHandler.ImageRetriever.Type.BLOCK_IMAGE +import org.oppia.android.util.parser.html.CustomHtmlContentHandler.ImageRetriever.Type.INLINE_TEXT_IMAGE import org.oppia.android.util.parser.svg.BlockPictureDrawable import javax.inject.Inject import kotlin.math.max -import org.oppia.android.util.parser.html.CustomHtmlContentHandler.ImageRetriever.Type.BLOCK_IMAGE -import org.oppia.android.util.parser.html.CustomHtmlContentHandler.ImageRetriever.Type.INLINE_TEXT_IMAGE // TODO(#169): Replace this with exploration asset downloader. @@ -94,7 +94,9 @@ class UrlImageParser private constructor( } override fun loadMathDrawable( - rawLatex: String, lineHeight: Float, type: ImageRetriever.Type + rawLatex: String, + lineHeight: Float, + type: ImageRetriever.Type ): Drawable { return ProxyDrawable().also { drawable -> imageLoader.loadMathDrawable( @@ -264,7 +266,7 @@ class UrlImageParser private constructor( * that will not be resized or aligned beyond what the target itself requires, and what the * system performs automatically. */ - class InlineTextImage( + class InlineTextImage( targetConfiguration: TargetConfiguration, private val computeDrawable: (T) -> D, private val computeDimensions: (D, TextView) -> Unit, diff --git a/utility/src/main/java/org/oppia/android/util/parser/math/MathBitmapModelLoader.kt b/utility/src/main/java/org/oppia/android/util/parser/math/MathBitmapModelLoader.kt index 84ce796e701..c56cb2fe4b4 100644 --- a/utility/src/main/java/org/oppia/android/util/parser/math/MathBitmapModelLoader.kt +++ b/utility/src/main/java/org/oppia/android/util/parser/math/MathBitmapModelLoader.kt @@ -32,11 +32,14 @@ import java.nio.ByteBuffer */ class MathBitmapModelLoader private constructor( private val applicationContext: Context -): ModelLoader { +) : ModelLoader { // Ref: https://bumptech.github.io/glide/tut/custom-modelloader.html#writing-the-modelloader. override fun buildLoadData( - model: MathModel, width: Int, height: Int, options: Options + model: MathModel, + width: Int, + height: Int, + options: Options ): ModelLoader.LoadData { return ModelLoader.LoadData( model.toKeySignature(), LatexModelDataFetcher(applicationContext, model, width, height) @@ -50,7 +53,7 @@ class MathBitmapModelLoader private constructor( private val model: MathModel, private val targetWidth: Int, private val targetHeight: Int - ): DataFetcher { + ) : DataFetcher { override fun loadData(priority: Priority, callback: DataFetcher.DataCallback) { val span = MathExpressionSpan( model.rawLatex, model.lineHeight, applicationContext.assets, !model.useInlineRendering @@ -113,7 +116,7 @@ class MathBitmapModelLoader private constructor( /** [ModelLoaderFactory] for creating new [MathBitmapModelLoader]s. */ class Factory( private val applicationContext: Context - ): ModelLoaderFactory { + ) : ModelLoaderFactory { override fun build(factory: MultiModelLoaderFactory): ModelLoader { return MathBitmapModelLoader(applicationContext) } diff --git a/utility/src/main/java/org/oppia/android/util/parser/math/MathModel.kt b/utility/src/main/java/org/oppia/android/util/parser/math/MathModel.kt index 00471f0bc0f..77b0d03998b 100644 --- a/utility/src/main/java/org/oppia/android/util/parser/math/MathModel.kt +++ b/utility/src/main/java/org/oppia/android/util/parser/math/MathModel.kt @@ -12,7 +12,9 @@ import java.security.MessageDigest * @property useInlineRendering whether the LaTeX will be inlined with text */ data class MathModel( - val rawLatex: String, val lineHeight: Float, val useInlineRendering: Boolean + val rawLatex: String, + val lineHeight: Float, + val useInlineRendering: Boolean ) { /** Returns a Glide [Key] signature (see [MathModelSignature] for specifics). */ fun toKeySignature(): MathModelSignature = @@ -29,23 +31,29 @@ data class MathModel( * @property useInlineRendering whether the render is formatted to be displayed in-line with text */ data class MathModelSignature( - val rawLatex: String, val lineHeightHundredX: Int, val useInlineRendering: Boolean + val rawLatex: String, + val lineHeightHundredX: Int, + val useInlineRendering: Boolean ) : Key { // Impl reference: http://bumptech.github.io/glide/doc/caching.html#custom-cache-invalidation. override fun updateDiskCacheKey(messageDigest: MessageDigest) { val rawLatexBytes = rawLatex.encodeToByteArray() - messageDigest.update(ByteBuffer.allocate(rawLatexBytes.size + Int.SIZE_BYTES + 1).apply { - put(rawLatexBytes) - putInt(lineHeightHundredX) - put(if (useInlineRendering) 1 else 0) - }.array()) + messageDigest.update( + ByteBuffer.allocate(rawLatexBytes.size + Int.SIZE_BYTES + 1).apply { + put(rawLatexBytes) + putInt(lineHeightHundredX) + put(if (useInlineRendering) 1 else 0) + }.array() + ) } internal companion object { /** Returns a new [MathModelSignature] for the specified [MathModel] properties. */ internal fun createSignature( - rawLatex: String, lineHeight: Float, useInlineRendering: Boolean + rawLatex: String, + lineHeight: Float, + useInlineRendering: Boolean ): MathModelSignature { val lineHeightHundredX = (lineHeight * 100f).toInt() return MathModelSignature(rawLatex, lineHeightHundredX, useInlineRendering) diff --git a/utility/src/test/java/org/oppia/android/util/parser/html/MathTagHandlerTest.kt b/utility/src/test/java/org/oppia/android/util/parser/html/MathTagHandlerTest.kt index 96c35f4a238..cb112925660 100644 --- a/utility/src/test/java/org/oppia/android/util/parser/html/MathTagHandlerTest.kt +++ b/utility/src/test/java/org/oppia/android/util/parser/html/MathTagHandlerTest.kt @@ -22,6 +22,7 @@ import org.mockito.Captor import org.mockito.Mock import org.mockito.Mockito.times import org.mockito.Mockito.verify +import org.mockito.Mockito.verifyNoMoreInteractions import org.mockito.junit.MockitoJUnit import org.mockito.junit.MockitoRule import org.oppia.android.testing.mockito.capture @@ -37,7 +38,6 @@ import org.robolectric.annotation.LooperMode import javax.inject.Inject import javax.inject.Singleton import kotlin.reflect.KClass -import org.mockito.Mockito.verifyNoMoreInteractions private const val MATH_MARKUP_1 = " Date: Thu, 10 Feb 2022 00:38:55 -0800 Subject: [PATCH 130/162] Add new dependency licenses. This isn't done yet (some of the licenses still need to be fixed). --- scripts/assets/maven_dependencies.textproto | 141 ++++++++++++++++++++ 1 file changed, 141 insertions(+) diff --git a/scripts/assets/maven_dependencies.textproto b/scripts/assets/maven_dependencies.textproto index 88b52fa3628..145b2f82d3d 100644 --- a/scripts/assets/maven_dependencies.textproto +++ b/scripts/assets/maven_dependencies.textproto @@ -262,6 +262,83 @@ maven_dependency { } } } +maven_dependency { + artifact_name: "androidx.test.espresso:espresso-core:3.2.0" + artifact_version: "3.2.0" + license { + license_name: "The Apache Software License, Version 2.0" + original_link: "https://www.apache.org/licenses/LICENSE-2.0.txt" + scrapable_link { + url: "https://www.apache.org/licenses/LICENSE-2.0.txt" + } + } +} +maven_dependency { + artifact_name: "androidx.test.espresso:espresso-idling-resource:3.2.0" + artifact_version: "3.2.0" + license { + license_name: "The Apache Software License, Version 2.0" + original_link: "https://www.apache.org/licenses/LICENSE-2.0.txt" + scrapable_link { + url: "https://www.apache.org/licenses/LICENSE-2.0.txt" + } + } +} +maven_dependency { + artifact_name: "androidx.test.ext:junit:1.1.1" + artifact_version: "1.1.1" + license { + license_name: "The Apache Software License, Version 2.0" + original_link: "https://www.apache.org/licenses/LICENSE-2.0.txt" + scrapable_link { + url: "https://www.apache.org/licenses/LICENSE-2.0.txt" + } + } +} +maven_dependency { + artifact_name: "androidx.test:core:1.4.0" + artifact_version: "1.4.0" + license { + license_name: "The Apache Software License, Version 2.0" + original_link: "https://www.apache.org/licenses/LICENSE-2.0.txt" + scrapable_link { + url: "https://www.apache.org/licenses/LICENSE-2.0.txt" + } + } +} +maven_dependency { + artifact_name: "androidx.test:monitor:1.4.0" + artifact_version: "1.4.0" + license { + license_name: "The Apache Software License, Version 2.0" + original_link: "https://www.apache.org/licenses/LICENSE-2.0.txt" + scrapable_link { + url: "https://www.apache.org/licenses/LICENSE-2.0.txt" + } + } +} +maven_dependency { + artifact_name: "androidx.test:rules:1.1.0" + artifact_version: "1.1.0" + license { + license_name: "The Apache Software License, Version 2.0" + original_link: "https://www.apache.org/licenses/LICENSE-2.0.txt" + scrapable_link { + url: "https://www.apache.org/licenses/LICENSE-2.0.txt" + } + } +} +maven_dependency { + artifact_name: "androidx.test:runner:1.2.0" + artifact_version: "1.2.0" + license { + license_name: "The Apache Software License, Version 2.0" + original_link: "https://www.apache.org/licenses/LICENSE-2.0.txt" + scrapable_link { + url: "https://www.apache.org/licenses/LICENSE-2.0.txt" + } + } +} maven_dependency { artifact_name: "androidx.vectordrawable:vectordrawable-animated:1.1.0" artifact_version: "1.1.0" @@ -650,6 +727,17 @@ maven_dependency { } } } +maven_dependency { + artifact_name: "com.squareup:javawriter:2.1.1" + artifact_version: "2.1.1" + license { + license_name: "Apache 2.0" + original_link: "https://www.apache.org/licenses/LICENSE-2.0.txt" + scrapable_link { + url: "https://www.apache.org/licenses/LICENSE-2.0.txt" + } + } +} maven_dependency { artifact_name: "com.squareup:kotlinpoet:1.6.0" artifact_version: "1.6.0" @@ -713,6 +801,14 @@ maven_dependency { } } } +maven_dependency { + artifact_name: "junit:junit:4.13.2" + artifact_version: "4.13.2" + license { + license_name: "Eclipse Public License 1.0" + original_link: "https://www.eclipse.org/legal/epl-v10.html" + } +} maven_dependency { artifact_name: "net.ltgt.gradle.incap:incap:0.3" artifact_version: "0.3" @@ -724,6 +820,18 @@ maven_dependency { } } } +maven_dependency { + artifact_name: "net.sf.kxml:kxml2:2.3.0" + artifact_version: "2.3.0" + license { + license_name: "BSD style" + original_link: "https://kxml.cvs.sourceforge.net/viewvc/kxml/kxml2/license.txt?view=markup" + } + license { + license_name: "Public Domain" + original_link: "https://creativecommons.org/licenses/publicdomain" + } +} maven_dependency { artifact_name: "nl.dionsegijn:konfetti:1.2.5" artifact_version: "1.2.5" @@ -753,6 +861,39 @@ maven_dependency { } } } +maven_dependency { + artifact_name: "org.hamcrest:hamcrest-core:1.3" + artifact_version: "1.3" + license { + license_name: "BSD-3-Clause" + original_link: "https://opensource.org/licenses/BSD-3-Clause" + extracted_copy_link { + url: "https://raw.githubusercontent.com/oppia/oppia-android-licenses/develop/bsd-3-clause-license.txt" + } + } +} +maven_dependency { + artifact_name: "org.hamcrest:hamcrest-integration:1.3" + artifact_version: "1.3" + license { + license_name: "BSD-3-Clause" + original_link: "https://opensource.org/licenses/BSD-3-Clause" + extracted_copy_link { + url: "https://raw.githubusercontent.com/oppia/oppia-android-licenses/develop/bsd-3-clause-license.txt" + } + } +} +maven_dependency { + artifact_name: "org.hamcrest:hamcrest-library:1.3" + artifact_version: "1.3" + license { + license_name: "BSD-3-Clause" + original_link: "https://opensource.org/licenses/BSD-3-Clause" + extracted_copy_link { + url: "https://raw.githubusercontent.com/oppia/oppia-android-licenses/develop/bsd-3-clause-license.txt" + } + } +} maven_dependency { artifact_name: "org.jetbrains.kotlin:kotlin-reflect:1.5.0" artifact_version: "1.5.0" From 2727235a44890547c4abb20c2ea751052fb954ed Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 10 Feb 2022 12:58:53 -0800 Subject: [PATCH 131/162] Fix license links. Note that the kxml one was tricky since its Maven entry says it's licensed under BSD and CC0 1.0, and its SourceForge link says the same plus LGPL 2.0. However, the source code and GitHub version of the project license it under MIT, and this seems to predate the others so it seems like the most correct license to use in this case and the one that we're using to represent the dependency. --- scripts/assets/maven_dependencies.textproto | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/scripts/assets/maven_dependencies.textproto b/scripts/assets/maven_dependencies.textproto index 145b2f82d3d..ea321c8f560 100644 --- a/scripts/assets/maven_dependencies.textproto +++ b/scripts/assets/maven_dependencies.textproto @@ -807,6 +807,9 @@ maven_dependency { license { license_name: "Eclipse Public License 1.0" original_link: "https://www.eclipse.org/legal/epl-v10.html" + extracted_copy_link { + url: "https://raw.githubusercontent.com/oppia/oppia-android-licenses/develop/eclipse-public-license-1.0.txt" + } } } maven_dependency { @@ -824,12 +827,11 @@ maven_dependency { artifact_name: "net.sf.kxml:kxml2:2.3.0" artifact_version: "2.3.0" license { - license_name: "BSD style" - original_link: "https://kxml.cvs.sourceforge.net/viewvc/kxml/kxml2/license.txt?view=markup" - } - license { - license_name: "Public Domain" - original_link: "https://creativecommons.org/licenses/publicdomain" + license_name: "The MIT License" + original_link: "https://github.com/kobjects/kxml2/blob/master/license.txt" + scrapable_link { + url: "https://raw.githubusercontent.com/kobjects/kxml2/master/license.txt" + } } } maven_dependency { From 6fb2c6c7b412f1a09fba539db34f55ac78109019 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 10 Feb 2022 17:03:43 -0800 Subject: [PATCH 132/162] Fix Gradle build. This uses a version of KotliTeX that builds correctly on Jitpack for Gradle, and fixes the StaticLayout creation to use an alignment constant that builds on Gradle (I'm not sure why there's a difference here between Gradle & Bazel, but the previous constant isn't part of the enum per official Android docs). --- WORKSPACE | 2 +- utility/build.gradle | 2 +- .../org/oppia/android/util/parser/math/MathBitmapModelLoader.kt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/WORKSPACE b/WORKSPACE index d8d636e6637..e7c86e31247 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -133,7 +133,7 @@ git_repository( # min target SDK version to be compatible with Oppia. git_repository( name = "kotlitex", - commit = "4ed63498717af239bc20c9a27ffdcd8eaa13c918", + commit = "ba1d5febba7642589f5441bb4affbb619156dbad", remote = "https://github.com/oppia/kotlitex", ) diff --git a/utility/build.gradle b/utility/build.gradle index d34421ef402..e8b517da751 100644 --- a/utility/build.gradle +++ b/utility/build.gradle @@ -65,7 +65,7 @@ dependencies { 'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0-alpha03', 'androidx.work:work-runtime-ktx:2.4.0', 'com.github.oppia:androidsvg:6bd15f69caee3e6857fcfcd123023716b4adec1d', - 'com.github.oppia:kotlitex:4ed63498717af239bc20c9a27ffdcd8eaa13c918', + 'com.github.oppia:kotlitex:ba1d5febba7642589f5441bb4affbb619156dbad', 'com.github.bumptech.glide:glide:4.11.0', 'com.google.dagger:dagger:2.24', 'com.google.firebase:firebase-analytics-ktx:17.5.0', diff --git a/utility/src/main/java/org/oppia/android/util/parser/math/MathBitmapModelLoader.kt b/utility/src/main/java/org/oppia/android/util/parser/math/MathBitmapModelLoader.kt index c56cb2fe4b4..978b4794e3a 100644 --- a/utility/src/main/java/org/oppia/android/util/parser/math/MathBitmapModelLoader.kt +++ b/utility/src/main/java/org/oppia/android/util/parser/math/MathBitmapModelLoader.kt @@ -71,7 +71,7 @@ class MathBitmapModelLoader private constructor( renderableText, TextPaint(), // Any TextPaint can be used since the span will use its own. /* width= */ 0, - Layout.Alignment.ALIGN_LEFT, + Layout.Alignment.ALIGN_NORMAL, /* spacingmult= */ 1f, /* spacingadd= */ 0f, /* includepad= */ true From 9d72168da6ac175e4a69605a6329f00f6bebeaa8 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 10 Feb 2022 18:50:48 -0800 Subject: [PATCH 133/162] Create the math drawable synchronously. This requires exposing new injectors broadly in the app since the math model loader doesn't have access to the dependency injection graph directly. --- app/BUILD.bazel | 2 + .../app/application/ApplicationInjector.kt | 6 +- .../ApplicationInjectorProvider.kt | 12 +- .../app/options/OptionsFragmentTest.kt | 13 ++ .../testing/options/OptionsFragmentTest.kt | 13 ++ scripts/assets/test_file_exemptions.textproto | 4 + .../oppia/android/util/logging/BUILD.bazel | 26 ++++ .../util/logging/ConsoleLoggerInjector.kt | 7 + .../logging/ConsoleLoggerInjectorProvider.kt | 7 + .../android/util/parser/math/BUILD.bazel | 2 + .../util/parser/math/MathBitmapModelLoader.kt | 145 ++++++++++++------ .../oppia/android/util/threading/BUILD.bazel | 27 ++++ .../util/threading/DispatcherInjector.kt | 12 ++ .../threading/DispatcherInjectorProvider.kt | 7 + 14 files changed, 234 insertions(+), 49 deletions(-) create mode 100644 utility/src/main/java/org/oppia/android/util/logging/ConsoleLoggerInjector.kt create mode 100644 utility/src/main/java/org/oppia/android/util/logging/ConsoleLoggerInjectorProvider.kt create mode 100644 utility/src/main/java/org/oppia/android/util/threading/DispatcherInjector.kt create mode 100644 utility/src/main/java/org/oppia/android/util/threading/DispatcherInjectorProvider.kt diff --git a/app/BUILD.bazel b/app/BUILD.bazel index b454897fe44..4d074338b92 100644 --- a/app/BUILD.bazel +++ b/app/BUILD.bazel @@ -768,7 +768,9 @@ kt_android_library( "//utility/src/main/java/org/oppia/android/util/parser/image:image_parsing_module", # TODO(#2432): Replace debug_module with prod_module when building the app in prod mode. "//utility/src/main/java/org/oppia/android/util/networking:debug_module", + "//utility/src/main/java/org/oppia/android/util/logging:console_logger_injector_provider", "//utility/src/main/java/org/oppia/android/util/statusbar:status_bar_color", + "//utility/src/main/java/org/oppia/android/util/threading:dispatcher_injector_provider", ], ) diff --git a/app/src/main/java/org/oppia/android/app/application/ApplicationInjector.kt b/app/src/main/java/org/oppia/android/app/application/ApplicationInjector.kt index 39717b2ec30..0d27a11aced 100644 --- a/app/src/main/java/org/oppia/android/app/application/ApplicationInjector.kt +++ b/app/src/main/java/org/oppia/android/app/application/ApplicationInjector.kt @@ -3,11 +3,15 @@ package org.oppia.android.app.application import org.oppia.android.app.translation.AppLanguageApplicationInjector import org.oppia.android.domain.locale.LocaleApplicationInjector import org.oppia.android.util.data.DataProvidersInjector +import org.oppia.android.util.logging.ConsoleLoggerInjector import org.oppia.android.util.system.OppiaClockInjector +import org.oppia.android.util.threading.DispatcherInjector /** Injector for application-level dependencies that can't be directly injected where needed. */ interface ApplicationInjector : DataProvidersInjector, AppLanguageApplicationInjector, OppiaClockInjector, - LocaleApplicationInjector + LocaleApplicationInjector, + DispatcherInjector, + ConsoleLoggerInjector diff --git a/app/src/main/java/org/oppia/android/app/application/ApplicationInjectorProvider.kt b/app/src/main/java/org/oppia/android/app/application/ApplicationInjectorProvider.kt index 55c68268d00..4ef6fc55b3a 100644 --- a/app/src/main/java/org/oppia/android/app/application/ApplicationInjectorProvider.kt +++ b/app/src/main/java/org/oppia/android/app/application/ApplicationInjectorProvider.kt @@ -6,15 +6,21 @@ import org.oppia.android.domain.locale.LocaleApplicationInjector import org.oppia.android.domain.locale.LocaleApplicationInjectorProvider import org.oppia.android.util.data.DataProvidersInjector import org.oppia.android.util.data.DataProvidersInjectorProvider +import org.oppia.android.util.logging.ConsoleLoggerInjector +import org.oppia.android.util.logging.ConsoleLoggerInjectorProvider import org.oppia.android.util.system.OppiaClockInjector import org.oppia.android.util.system.OppiaClockInjectorProvider +import org.oppia.android.util.threading.DispatcherInjector +import org.oppia.android.util.threading.DispatcherInjectorProvider /** Provider for [ApplicationInjector]. The application context will implement this interface. */ interface ApplicationInjectorProvider : DataProvidersInjectorProvider, AppLanguageApplicationInjectorProvider, OppiaClockInjectorProvider, - LocaleApplicationInjectorProvider { + LocaleApplicationInjectorProvider, + DispatcherInjectorProvider, + ConsoleLoggerInjectorProvider { fun getApplicationInjector(): ApplicationInjector override fun getDataProvidersInjector(): DataProvidersInjector = getApplicationInjector() @@ -25,4 +31,8 @@ interface ApplicationInjectorProvider : override fun getOppiaClockInjector(): OppiaClockInjector = getApplicationInjector() override fun getLocaleApplicationInjector(): LocaleApplicationInjector = getApplicationInjector() + + override fun getDispatcherInjector(): DispatcherInjector = getApplicationInjector() + + override fun getConsoleLoggerInjector(): ConsoleLoggerInjector = getApplicationInjector() } diff --git a/app/src/sharedTest/java/org/oppia/android/app/options/OptionsFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/options/OptionsFragmentTest.kt index 76956e4d84e..fba620b014a 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/options/OptionsFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/options/OptionsFragmentTest.kt @@ -104,6 +104,10 @@ import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode import javax.inject.Inject import javax.inject.Singleton +import org.oppia.android.util.platformparameter.CACHE_LATEX_RENDERING +import org.oppia.android.util.platformparameter.CACHE_LATEX_RENDERING_DEFAULT_VALUE +import org.oppia.android.util.platformparameter.CacheLatexRendering +import org.oppia.android.util.platformparameter.PlatformParameterSingleton /** Tests for [OptionsFragment]. */ @RunWith(AndroidJUnit4::class) @@ -654,6 +658,15 @@ class OptionsFragmentTest { fun provideEnableLanguageSelectionUi(): PlatformParameterValue { return PlatformParameterValue.createDefaultParameter(forceEnableLanguageSelectionUi) } + + @Provides + @CacheLatexRendering + fun provideCacheLatexRendering( + platformParameterSingleton: PlatformParameterSingleton + ): PlatformParameterValue { + return platformParameterSingleton.getBooleanPlatformParameter(CACHE_LATEX_RENDERING) + ?: PlatformParameterValue.createDefaultParameter(CACHE_LATEX_RENDERING_DEFAULT_VALUE) + } } // TODO(#59): Figure out a way to reuse modules instead of needing to re-declare them. diff --git a/app/src/test/java/org/oppia/android/app/testing/options/OptionsFragmentTest.kt b/app/src/test/java/org/oppia/android/app/testing/options/OptionsFragmentTest.kt index 41d04fdc19e..3bc06d099b5 100644 --- a/app/src/test/java/org/oppia/android/app/testing/options/OptionsFragmentTest.kt +++ b/app/src/test/java/org/oppia/android/app/testing/options/OptionsFragmentTest.kt @@ -93,6 +93,10 @@ import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode import javax.inject.Inject import javax.inject.Singleton +import org.oppia.android.util.platformparameter.CACHE_LATEX_RENDERING +import org.oppia.android.util.platformparameter.CACHE_LATEX_RENDERING_DEFAULT_VALUE +import org.oppia.android.util.platformparameter.CacheLatexRendering +import org.oppia.android.util.platformparameter.PlatformParameterSingleton @RunWith(AndroidJUnit4::class) @Config(application = OptionsFragmentTest.TestApplication::class, qualifiers = "sw600dp") @@ -251,6 +255,15 @@ class OptionsFragmentTest { fun provideEnableLanguageSelectionUi(): PlatformParameterValue { return PlatformParameterValue.createDefaultParameter(forceEnableLanguageSelectionUi) } + + @Provides + @CacheLatexRendering + fun provideCacheLatexRendering( + platformParameterSingleton: PlatformParameterSingleton + ): PlatformParameterValue { + return platformParameterSingleton.getBooleanPlatformParameter(CACHE_LATEX_RENDERING) + ?: PlatformParameterValue.createDefaultParameter(CACHE_LATEX_RENDERING_DEFAULT_VALUE) + } } // TODO(#59): Figure out a way to reuse modules instead of needing to re-declare them. diff --git a/scripts/assets/test_file_exemptions.textproto b/scripts/assets/test_file_exemptions.textproto index 09ce31eb350..9b9063ad3fe 100644 --- a/scripts/assets/test_file_exemptions.textproto +++ b/scripts/assets/test_file_exemptions.textproto @@ -713,6 +713,8 @@ exempted_file_path: "utility/src/main/java/org/oppia/android/util/gcsresource/Gc exempted_file_path: "utility/src/main/java/org/oppia/android/util/locale/OppiaBidiFormatter.kt" exempted_file_path: "utility/src/main/java/org/oppia/android/util/locale/OppiaLocale.kt" exempted_file_path: "utility/src/main/java/org/oppia/android/util/logging/ConsoleLogger.kt" +exempted_file_path: "utility/src/main/java/org/oppia/android/util/logging/ConsoleLoggerInjector.kt" +exempted_file_path: "utility/src/main/java/org/oppia/android/util/logging/ConsoleLoggerInjectorProvider.kt" exempted_file_path: "utility/src/main/java/org/oppia/android/util/logging/EventLogger.kt" exempted_file_path: "utility/src/main/java/org/oppia/android/util/logging/ExceptionLogger.kt" exempted_file_path: "utility/src/main/java/org/oppia/android/util/logging/LogLevel.kt" @@ -769,4 +771,6 @@ exempted_file_path: "utility/src/main/java/org/oppia/android/util/system/OppiaCl exempted_file_path: "utility/src/main/java/org/oppia/android/util/threading/BackgroundDispatcher.kt" exempted_file_path: "utility/src/main/java/org/oppia/android/util/threading/BlockingDispatcher.kt" exempted_file_path: "utility/src/main/java/org/oppia/android/util/threading/ConcurrentCollections.kt" +exempted_file_path: "utility/src/main/java/org/oppia/android/util/threading/DispatcherInjector.kt" +exempted_file_path: "utility/src/main/java/org/oppia/android/util/threading/DispatcherInjectorProvider.kt" exempted_file_path: "utility/src/main/java/org/oppia/android/util/threading/DispatcherModule.kt" diff --git a/utility/src/main/java/org/oppia/android/util/logging/BUILD.bazel b/utility/src/main/java/org/oppia/android/util/logging/BUILD.bazel index 21b2fa9154c..4e9ed4bd036 100644 --- a/utility/src/main/java/org/oppia/android/util/logging/BUILD.bazel +++ b/utility/src/main/java/org/oppia/android/util/logging/BUILD.bazel @@ -32,6 +32,32 @@ kt_android_library( ], ) +kt_android_library( + name = "console_logger_injector", + srcs = [ + "ConsoleLoggerInjector.kt", + ], + visibility = [ + "//:oppia_api_visibility", + ], + deps = [ + ":console_logger", + ], +) + +kt_android_library( + name = "console_logger_injector_provider", + srcs = [ + "ConsoleLoggerInjectorProvider.kt", + ], + visibility = [ + "//:oppia_api_visibility", + ], + deps = [ + ":console_logger_injector", + ], +) + kt_android_library( name = "event_bundle_creator", srcs = [ diff --git a/utility/src/main/java/org/oppia/android/util/logging/ConsoleLoggerInjector.kt b/utility/src/main/java/org/oppia/android/util/logging/ConsoleLoggerInjector.kt new file mode 100644 index 00000000000..0c2f2c42b2d --- /dev/null +++ b/utility/src/main/java/org/oppia/android/util/logging/ConsoleLoggerInjector.kt @@ -0,0 +1,7 @@ +package org.oppia.android.util.logging + +/** Injector for [ConsoleLogger]. Implemented by a generated Dagger application component. */ +interface ConsoleLoggerInjector { + /** Returns the application-level [ConsoleLogger]. */ + fun getConsoleLogger(): ConsoleLogger +} diff --git a/utility/src/main/java/org/oppia/android/util/logging/ConsoleLoggerInjectorProvider.kt b/utility/src/main/java/org/oppia/android/util/logging/ConsoleLoggerInjectorProvider.kt new file mode 100644 index 00000000000..8391577f9ea --- /dev/null +++ b/utility/src/main/java/org/oppia/android/util/logging/ConsoleLoggerInjectorProvider.kt @@ -0,0 +1,7 @@ +package org.oppia.android.util.logging + +/** Provider for [ConsoleLoggerInjector]s. To be implemented by the application class. */ +interface ConsoleLoggerInjectorProvider { + /** Returns the [ConsoleLoggerInjector] corresponding to the current application context. */ + fun getConsoleLoggerInjector(): ConsoleLoggerInjector +} diff --git a/utility/src/main/java/org/oppia/android/util/parser/math/BUILD.bazel b/utility/src/main/java/org/oppia/android/util/parser/math/BUILD.bazel index 0592c099da9..2beb8af1107 100644 --- a/utility/src/main/java/org/oppia/android/util/parser/math/BUILD.bazel +++ b/utility/src/main/java/org/oppia/android/util/parser/math/BUILD.bazel @@ -28,5 +28,7 @@ kt_android_library( ":math_latex_model", "//third_party:com_github_bumptech_glide_glide", "//third_party:io_github_karino2_kotlitex", + "//utility/src/main/java/org/oppia/android/util/logging:console_logger_injector_provider", + "//utility/src/main/java/org/oppia/android/util/threading:dispatcher_injector_provider", ], ) diff --git a/utility/src/main/java/org/oppia/android/util/parser/math/MathBitmapModelLoader.kt b/utility/src/main/java/org/oppia/android/util/parser/math/MathBitmapModelLoader.kt index 978b4794e3a..d88cc7d37bc 100644 --- a/utility/src/main/java/org/oppia/android/util/parser/math/MathBitmapModelLoader.kt +++ b/utility/src/main/java/org/oppia/android/util/parser/math/MathBitmapModelLoader.kt @@ -19,6 +19,13 @@ import com.bumptech.glide.request.target.Target import io.github.karino2.kotlitex.view.MathExpressionSpan import java.io.ByteArrayOutputStream import java.nio.ByteBuffer +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.oppia.android.util.logging.ConsoleLogger +import org.oppia.android.util.logging.ConsoleLoggerInjectorProvider +import org.oppia.android.util.threading.DispatcherInjectorProvider /** * [ModelLoader] for rendering and caching bitmap representations of LaTeX represented by @@ -35,6 +42,24 @@ class MathBitmapModelLoader private constructor( ) : ModelLoader { // Ref: https://bumptech.github.io/glide/tut/custom-modelloader.html#writing-the-modelloader. + private val backgroundDispatcher by lazy { + val injectorProvider = applicationContext.applicationContext as DispatcherInjectorProvider + val injector = injectorProvider.getDispatcherInjector() + injector.getBackgroundDispatcher() + } + + private val blockingDispatcher by lazy { + val injectorProvider = applicationContext.applicationContext as DispatcherInjectorProvider + val injector = injectorProvider.getDispatcherInjector() + injector.getBlockingDispatcher() + } + + private val consoleLogger by lazy { + val injectorProvider = applicationContext as ConsoleLoggerInjectorProvider + val injector = injectorProvider.getConsoleLoggerInjector() + injector.getConsoleLogger() + } + override fun buildLoadData( model: MathModel, width: Int, @@ -42,7 +67,16 @@ class MathBitmapModelLoader private constructor( options: Options ): ModelLoader.LoadData { return ModelLoader.LoadData( - model.toKeySignature(), LatexModelDataFetcher(applicationContext, model, width, height) + model.toKeySignature(), + LatexModelDataFetcher( + applicationContext, + model, + width, + height, + backgroundDispatcher, + blockingDispatcher, + consoleLogger + ) ) } @@ -52,55 +86,72 @@ class MathBitmapModelLoader private constructor( private val applicationContext: Context, private val model: MathModel, private val targetWidth: Int, - private val targetHeight: Int + private val targetHeight: Int, + private val backgroundDispatcher: CoroutineDispatcher, + private val blockingDispatcher: CoroutineDispatcher, + private val consoleLogger: ConsoleLogger ) : DataFetcher { override fun loadData(priority: Priority, callback: DataFetcher.DataCallback) { - val span = MathExpressionSpan( - model.rawLatex, model.lineHeight, applicationContext.assets, !model.useInlineRendering - ) - val renderableText = SpannableStringBuilder("\uFFFC").apply { - setSpan(span, /* start= */ 0, /* end= */ 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + // Defer execution to the app's dispatchers since synchronization is needed (and more + // performant and easier to achieve with coroutines). + CoroutineScope(backgroundDispatcher).launch { + // KotliTeX drawable initialization loads shared static state that's susceptible to race + // conditions. This synchronizes span creation so that the race condition can't happen, + // though it will likely slow down LaTeX loading a bit. Fortunately, rendering & PNG + // creation can still happen in parallel, and those are the more expensive steps. + val span = withContext(CoroutineScope(blockingDispatcher).coroutineContext) { + MathExpressionSpan( + model.rawLatex, model.lineHeight, applicationContext.assets, !model.useInlineRendering + ).also { it.ensureDrawable() } + } + val renderableText = SpannableStringBuilder("\uFFFC").apply { + setSpan(span, /* start= */ 0, /* end= */ 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + } + + // Use Android's StaticLayout to ensure the text is rendered correctly. Note that the + // constants are derived from TextView's defaults (except width which is defaulted to 0 since + // the width isn't necessarily known ahead of time). + @Suppress("DEPRECATION") // This call is necessary for the supported min API version. + val staticTextLayout = + StaticLayout( + renderableText, + TextPaint(), // Any TextPaint can be used since the span will use its own. + /* width= */ 0, + Layout.Alignment.ALIGN_NORMAL, + /* spacingmult= */ 1f, + /* spacingadd= */ 0f, + /* includepad= */ true + ) + + // Reference for how Android manages different parts of text during rendering: + // https://stackoverflow.com/a/27631737/3689782. Note that the specifics of how text + // properties are used to compute these bounds are in the modified KotliTeX implementation + // (see the getter implementation for the property below). + val bounds = span.drawableBounds + val canvasBitmap = + Bitmap.createBitmap(bounds.width(), bounds.height(), Bitmap.Config.ARGB_8888) + val bitmapCanvas = Canvas(canvasBitmap) + staticTextLayout.draw(bitmapCanvas) + + val finalWidth = if (targetWidth == Target.SIZE_ORIGINAL) bounds.width() else targetWidth + val finalHeight = if (targetHeight == Target.SIZE_ORIGINAL) bounds.height() else targetHeight + + // Compute the final bitmap (which might need to be scaled depending on options). + val bitmap = if (canvasBitmap.width != finalWidth || canvasBitmap.height != finalHeight) { + // Glide is requesting the image in a different size, so adjust it. + Bitmap.createScaledBitmap(canvasBitmap, finalWidth, finalHeight, /* filter= */ true) + } else canvasBitmap // Otherwise, the original bitmap is the correct size. + + // Convert the bitmap to a PNG to store within Glide's cache for later retrieval. + val rawBitmap = ByteArrayOutputStream().also { outputStream -> + bitmap.compress(Bitmap.CompressFormat.PNG, /* quality= */ 100, outputStream) + }.toByteArray() + callback.onDataReady(ByteBuffer.wrap(rawBitmap)) + }.invokeOnCompletion { + if (it != null) { + consoleLogger.e("ImageLoading", "Failed to convert LaTeX to SVG (model: $model)", it) + } } - - // Use Android's StaticLayout to ensure the text is rendered correctly. Note that the - // constants are derived from TextView's defaults (except width which is defaulted to 0 since - // the width isn't necessarily known ahead of time). - @Suppress("DEPRECATION") // This call is necessary for the supported min API version. - val staticTextLayout = - StaticLayout( - renderableText, - TextPaint(), // Any TextPaint can be used since the span will use its own. - /* width= */ 0, - Layout.Alignment.ALIGN_NORMAL, - /* spacingmult= */ 1f, - /* spacingadd= */ 0f, - /* includepad= */ true - ) - - // Reference for how Android manages different parts of text during rendering: - // https://stackoverflow.com/a/27631737/3689782. Note that the specifics of how text - // properties are used to compute these bounds are in the modified KotliTeX implementation - // (see the getter implementation for the property below). - val bounds = span.drawableBounds - val canvasBitmap = - Bitmap.createBitmap(bounds.width(), bounds.height(), Bitmap.Config.ARGB_8888) - val bitmapCanvas = Canvas(canvasBitmap) - staticTextLayout.draw(bitmapCanvas) - - val finalWidth = if (targetWidth == Target.SIZE_ORIGINAL) bounds.width() else targetWidth - val finalHeight = if (targetHeight == Target.SIZE_ORIGINAL) bounds.height() else targetHeight - - // Compute the final bitmap (which might need to be scaled depending on options). - val bitmap = if (canvasBitmap.width != finalWidth || canvasBitmap.height != finalHeight) { - // Glide is requesting the image in a different size, so adjust it. - Bitmap.createScaledBitmap(canvasBitmap, finalWidth, finalHeight, /* filter= */ true) - } else canvasBitmap // Otherwise, the original bitmap is the correct size. - - // Convert the bitmap to a PNG to store within Glide's cache for later retrieval. - val rawBitmap = ByteArrayOutputStream().also { outputStream -> - bitmap.compress(Bitmap.CompressFormat.PNG, /* quality= */ 100, outputStream) - }.toByteArray() - callback.onDataReady(ByteBuffer.wrap(rawBitmap)) } override fun cleanup() {} diff --git a/utility/src/main/java/org/oppia/android/util/threading/BUILD.bazel b/utility/src/main/java/org/oppia/android/util/threading/BUILD.bazel index 5fb72794b0f..bb5b44721bc 100644 --- a/utility/src/main/java/org/oppia/android/util/threading/BUILD.bazel +++ b/utility/src/main/java/org/oppia/android/util/threading/BUILD.bazel @@ -17,6 +17,33 @@ kt_android_library( ], ) +kt_android_library( + name = "dispatcher_injector", + srcs = [ + "DispatcherInjector.kt", + ], + visibility = [ + "//:oppia_api_visibility", + ], + deps = [ + ":annotations", + "//third_party:org_jetbrains_kotlinx_kotlinx-coroutines-core", + ], +) + +kt_android_library( + name = "dispatcher_injector_provider", + srcs = [ + "DispatcherInjectorProvider.kt", + ], + visibility = [ + "//:oppia_api_visibility", + ], + deps = [ + ":dispatcher_injector", + ], +) + kt_android_library( name = "prod_module", srcs = [ diff --git a/utility/src/main/java/org/oppia/android/util/threading/DispatcherInjector.kt b/utility/src/main/java/org/oppia/android/util/threading/DispatcherInjector.kt new file mode 100644 index 00000000000..19a94f90615 --- /dev/null +++ b/utility/src/main/java/org/oppia/android/util/threading/DispatcherInjector.kt @@ -0,0 +1,12 @@ +package org.oppia.android.util.threading + +import kotlinx.coroutines.CoroutineDispatcher + +/** Injector for [CoroutineDispatcher]. Implemented by a generated Dagger application component. */ +interface DispatcherInjector { + /** Returns the [BackgroundDispatcher] [CoroutineDispatcher]. */ + @BackgroundDispatcher fun getBackgroundDispatcher(): CoroutineDispatcher + + /** Returns the [BlockingDispatcher] [CoroutineDispatcher]. */ + @BlockingDispatcher fun getBlockingDispatcher(): CoroutineDispatcher +} diff --git a/utility/src/main/java/org/oppia/android/util/threading/DispatcherInjectorProvider.kt b/utility/src/main/java/org/oppia/android/util/threading/DispatcherInjectorProvider.kt new file mode 100644 index 00000000000..ee43f9b74f6 --- /dev/null +++ b/utility/src/main/java/org/oppia/android/util/threading/DispatcherInjectorProvider.kt @@ -0,0 +1,7 @@ +package org.oppia.android.util.threading + +/** Provider for [DispatcherInjector]s. To be implemented by the application class. */ +interface DispatcherInjectorProvider { + /** Returns the [DispatcherInjector] corresponding to the current application context. */ + fun getDispatcherInjector(): DispatcherInjector +} From 34d90a71611b96de9f337c282b429fe333d260fb Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 10 Feb 2022 18:52:27 -0800 Subject: [PATCH 134/162] Remove new deps from Maven list. They were incorrectly pulled in by KotliTeX. --- scripts/assets/maven_dependencies.textproto | 143 -------------------- 1 file changed, 143 deletions(-) diff --git a/scripts/assets/maven_dependencies.textproto b/scripts/assets/maven_dependencies.textproto index ea321c8f560..88b52fa3628 100644 --- a/scripts/assets/maven_dependencies.textproto +++ b/scripts/assets/maven_dependencies.textproto @@ -262,83 +262,6 @@ maven_dependency { } } } -maven_dependency { - artifact_name: "androidx.test.espresso:espresso-core:3.2.0" - artifact_version: "3.2.0" - license { - license_name: "The Apache Software License, Version 2.0" - original_link: "https://www.apache.org/licenses/LICENSE-2.0.txt" - scrapable_link { - url: "https://www.apache.org/licenses/LICENSE-2.0.txt" - } - } -} -maven_dependency { - artifact_name: "androidx.test.espresso:espresso-idling-resource:3.2.0" - artifact_version: "3.2.0" - license { - license_name: "The Apache Software License, Version 2.0" - original_link: "https://www.apache.org/licenses/LICENSE-2.0.txt" - scrapable_link { - url: "https://www.apache.org/licenses/LICENSE-2.0.txt" - } - } -} -maven_dependency { - artifact_name: "androidx.test.ext:junit:1.1.1" - artifact_version: "1.1.1" - license { - license_name: "The Apache Software License, Version 2.0" - original_link: "https://www.apache.org/licenses/LICENSE-2.0.txt" - scrapable_link { - url: "https://www.apache.org/licenses/LICENSE-2.0.txt" - } - } -} -maven_dependency { - artifact_name: "androidx.test:core:1.4.0" - artifact_version: "1.4.0" - license { - license_name: "The Apache Software License, Version 2.0" - original_link: "https://www.apache.org/licenses/LICENSE-2.0.txt" - scrapable_link { - url: "https://www.apache.org/licenses/LICENSE-2.0.txt" - } - } -} -maven_dependency { - artifact_name: "androidx.test:monitor:1.4.0" - artifact_version: "1.4.0" - license { - license_name: "The Apache Software License, Version 2.0" - original_link: "https://www.apache.org/licenses/LICENSE-2.0.txt" - scrapable_link { - url: "https://www.apache.org/licenses/LICENSE-2.0.txt" - } - } -} -maven_dependency { - artifact_name: "androidx.test:rules:1.1.0" - artifact_version: "1.1.0" - license { - license_name: "The Apache Software License, Version 2.0" - original_link: "https://www.apache.org/licenses/LICENSE-2.0.txt" - scrapable_link { - url: "https://www.apache.org/licenses/LICENSE-2.0.txt" - } - } -} -maven_dependency { - artifact_name: "androidx.test:runner:1.2.0" - artifact_version: "1.2.0" - license { - license_name: "The Apache Software License, Version 2.0" - original_link: "https://www.apache.org/licenses/LICENSE-2.0.txt" - scrapable_link { - url: "https://www.apache.org/licenses/LICENSE-2.0.txt" - } - } -} maven_dependency { artifact_name: "androidx.vectordrawable:vectordrawable-animated:1.1.0" artifact_version: "1.1.0" @@ -727,17 +650,6 @@ maven_dependency { } } } -maven_dependency { - artifact_name: "com.squareup:javawriter:2.1.1" - artifact_version: "2.1.1" - license { - license_name: "Apache 2.0" - original_link: "https://www.apache.org/licenses/LICENSE-2.0.txt" - scrapable_link { - url: "https://www.apache.org/licenses/LICENSE-2.0.txt" - } - } -} maven_dependency { artifact_name: "com.squareup:kotlinpoet:1.6.0" artifact_version: "1.6.0" @@ -801,17 +713,6 @@ maven_dependency { } } } -maven_dependency { - artifact_name: "junit:junit:4.13.2" - artifact_version: "4.13.2" - license { - license_name: "Eclipse Public License 1.0" - original_link: "https://www.eclipse.org/legal/epl-v10.html" - extracted_copy_link { - url: "https://raw.githubusercontent.com/oppia/oppia-android-licenses/develop/eclipse-public-license-1.0.txt" - } - } -} maven_dependency { artifact_name: "net.ltgt.gradle.incap:incap:0.3" artifact_version: "0.3" @@ -823,17 +724,6 @@ maven_dependency { } } } -maven_dependency { - artifact_name: "net.sf.kxml:kxml2:2.3.0" - artifact_version: "2.3.0" - license { - license_name: "The MIT License" - original_link: "https://github.com/kobjects/kxml2/blob/master/license.txt" - scrapable_link { - url: "https://raw.githubusercontent.com/kobjects/kxml2/master/license.txt" - } - } -} maven_dependency { artifact_name: "nl.dionsegijn:konfetti:1.2.5" artifact_version: "1.2.5" @@ -863,39 +753,6 @@ maven_dependency { } } } -maven_dependency { - artifact_name: "org.hamcrest:hamcrest-core:1.3" - artifact_version: "1.3" - license { - license_name: "BSD-3-Clause" - original_link: "https://opensource.org/licenses/BSD-3-Clause" - extracted_copy_link { - url: "https://raw.githubusercontent.com/oppia/oppia-android-licenses/develop/bsd-3-clause-license.txt" - } - } -} -maven_dependency { - artifact_name: "org.hamcrest:hamcrest-integration:1.3" - artifact_version: "1.3" - license { - license_name: "BSD-3-Clause" - original_link: "https://opensource.org/licenses/BSD-3-Clause" - extracted_copy_link { - url: "https://raw.githubusercontent.com/oppia/oppia-android-licenses/develop/bsd-3-clause-license.txt" - } - } -} -maven_dependency { - artifact_name: "org.hamcrest:hamcrest-library:1.3" - artifact_version: "1.3" - license { - license_name: "BSD-3-Clause" - original_link: "https://opensource.org/licenses/BSD-3-Clause" - extracted_copy_link { - url: "https://raw.githubusercontent.com/oppia/oppia-android-licenses/develop/bsd-3-clause-license.txt" - } - } -} maven_dependency { artifact_name: "org.jetbrains.kotlin:kotlin-reflect:1.5.0" artifact_version: "1.5.0" From 551071c91c91eeb7c22a3b15fe2b0d4d7502a0d9 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 10 Feb 2022 21:03:12 -0800 Subject: [PATCH 135/162] Add argument partitioning. This fixes cases where argument calls may be very large and fail to execute due to exceeding system limitations. --- .../android/scripts/common/BazelClient.kt | 65 ++++++++++++++++--- 1 file changed, 55 insertions(+), 10 deletions(-) diff --git a/scripts/src/java/org/oppia/android/scripts/common/BazelClient.kt b/scripts/src/java/org/oppia/android/scripts/common/BazelClient.kt index 2df225f632f..5d7772a7cfb 100644 --- a/scripts/src/java/org/oppia/android/scripts/common/BazelClient.kt +++ b/scripts/src/java/org/oppia/android/scripts/common/BazelClient.kt @@ -2,6 +2,7 @@ package org.oppia.android.scripts.common import java.io.File import java.lang.IllegalArgumentException +import java.util.Locale /** * Utility class to query & interact with a Bazel workspace on the local filesystem (residing within @@ -21,11 +22,11 @@ class BazelClient( /** Returns all Bazel file targets that correspond to each of the relative file paths provided. */ fun retrieveBazelTargets(changedFileRelativePaths: Iterable): List { return correctPotentiallyBrokenTargetNames( - executeBazelCommand( - "query", + runPotentiallyShardedQueryCommand( + "set(%s)", + changedFileRelativePaths, "--noshow_progress", "--keep_going", - "set(${changedFileRelativePaths.joinToString(" ")})", allowPartialFailures = true ) ) @@ -34,12 +35,12 @@ class BazelClient( /** Returns all test targets in the workspace that are affected by the list of file targets. */ fun retrieveRelatedTestTargets(fileTargets: Iterable): List { return correctPotentiallyBrokenTargetNames( - executeBazelCommand( - "query", + runPotentiallyShardedQueryCommand( + "kind(test, allrdeps(set(%s)))", + fileTargets, "--noshow_progress", "--universe_scope=//...", - "--order_output=no", - "kind(test, allrdeps(set(${fileTargets.joinToString(" ")})))" + "--order_output=no" ) ) } @@ -71,12 +72,12 @@ class BazelClient( retrieveFilteredSiblings(filterRuleType = "android_library", buildFileTarget) }.toSet() return correctPotentiallyBrokenTargetNames( - executeBazelCommand( - "query", + runPotentiallyShardedQueryCommand( + "filter('^[^@]', kind(test, allrdeps(set(%s))))", + relevantSiblings, "--noshow_progress", "--universe_scope=//...", "--order_output=no", - "filter('^[^@]', kind(test, allrdeps(set(${relevantSiblings.joinToString(" ")}))))", ) ) } else listOf() @@ -129,6 +130,46 @@ class BazelClient( return correctedTargets } + /** + * Returns the results of a query command with a potentially large list of [values] that will be + * split up into multiple commands to avoid overflow the system's maximum argument limit. + * + * Note that [queryFormatStr] is expected to have 1 string variable (which will be the + * space-separated join of [values] or a partition of [values]). + */ + @Suppress("SameParameterValue") // This check doesn't work correctly for varargs. + private fun runPotentiallyShardedQueryCommand( + queryFormatStr: String, + values: Iterable, + vararg prefixArgs: String, + allowPartialFailures: Boolean = false + ): List { + // Split up values into partitions to ensure that the argument calls don't over-run the limit. + var partitionCount = 0 + lateinit var partitions: List> + do { + partitionCount++ + partitions = values.chunked((values.count() + 1) / partitionCount) + } while (computeMaxArgumentLength(partitions) >= MAX_ALLOWED_ARG_STR_LENGTH) + + // Fragment the query across the partitions to ensure all values can be considered. + return partitions.flatMap { partition -> + val lastArgument = queryFormatStr.format(Locale.US, partition.joinToString(" ")) + val allArguments = prefixArgs.toList() + lastArgument + executeBazelCommand( + "query", *allArguments.toTypedArray(), allowPartialFailures = allowPartialFailures + ) + } + } + + private fun computeMaxArgumentLength(partitions: List>): Int { + return checkNotNull(partitions.map(this::computeArgumentLength).maxOrNull()) { + "Expected at least one partition when computing argument lengths." + } + } + + private fun computeArgumentLength(args: List) = args.joinToString(" ").length + @Suppress("SameParameterValue") // This check doesn't work correctly for varargs. private fun executeBazelCommand( vararg arguments: String, @@ -150,6 +191,10 @@ class BazelClient( } return result.output } + + private companion object { + private const val MAX_ALLOWED_ARG_STR_LENGTH = 50_000 + } } /** Returns a list of indexes where the specified [needle] occurs in this string. */ From 80162520959038256d246472922b4e01ee116d9a Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 11 Feb 2022 00:17:45 -0800 Subject: [PATCH 136/162] Make allowance for empty cases to fix tests. These tests correspond to real scenarios. --- .../java/org/oppia/android/scripts/common/BazelClient.kt | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/scripts/src/java/org/oppia/android/scripts/common/BazelClient.kt b/scripts/src/java/org/oppia/android/scripts/common/BazelClient.kt index 5d7772a7cfb..b6071d86b76 100644 --- a/scripts/src/java/org/oppia/android/scripts/common/BazelClient.kt +++ b/scripts/src/java/org/oppia/android/scripts/common/BazelClient.kt @@ -162,11 +162,8 @@ class BazelClient( } } - private fun computeMaxArgumentLength(partitions: List>): Int { - return checkNotNull(partitions.map(this::computeArgumentLength).maxOrNull()) { - "Expected at least one partition when computing argument lengths." - } - } + private fun computeMaxArgumentLength(partitions: List>) = + partitions.map(this::computeArgumentLength).maxOrNull() ?: 0 private fun computeArgumentLength(args: List) = args.joinToString(" ").length From 3c791e68a5bd988cfe323801a7e0244767a66009 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 11 Feb 2022 00:20:58 -0800 Subject: [PATCH 137/162] Lint fixes. --- .../oppia/android/app/options/OptionsFragmentTest.kt | 8 ++++---- .../app/testing/options/OptionsFragmentTest.kt | 8 ++++---- .../android/util/parser/math/MathBitmapModelLoader.kt | 11 ++++++----- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/app/src/sharedTest/java/org/oppia/android/app/options/OptionsFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/options/OptionsFragmentTest.kt index fba620b014a..098d29ee351 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/options/OptionsFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/options/OptionsFragmentTest.kt @@ -94,7 +94,11 @@ import org.oppia.android.util.networking.NetworkConnectionUtilDebugModule import org.oppia.android.util.parser.html.HtmlParserEntityTypeModule import org.oppia.android.util.parser.image.GlideImageLoaderModule import org.oppia.android.util.parser.image.ImageParsingModule +import org.oppia.android.util.platformparameter.CACHE_LATEX_RENDERING +import org.oppia.android.util.platformparameter.CACHE_LATEX_RENDERING_DEFAULT_VALUE +import org.oppia.android.util.platformparameter.CacheLatexRendering import org.oppia.android.util.platformparameter.EnableLanguageSelectionUi +import org.oppia.android.util.platformparameter.PlatformParameterSingleton import org.oppia.android.util.platformparameter.PlatformParameterValue import org.oppia.android.util.platformparameter.SPLASH_SCREEN_WELCOME_MSG_DEFAULT_VALUE import org.oppia.android.util.platformparameter.SYNC_UP_WORKER_TIME_PERIOD_IN_HOURS_DEFAULT_VALUE @@ -104,10 +108,6 @@ import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode import javax.inject.Inject import javax.inject.Singleton -import org.oppia.android.util.platformparameter.CACHE_LATEX_RENDERING -import org.oppia.android.util.platformparameter.CACHE_LATEX_RENDERING_DEFAULT_VALUE -import org.oppia.android.util.platformparameter.CacheLatexRendering -import org.oppia.android.util.platformparameter.PlatformParameterSingleton /** Tests for [OptionsFragment]. */ @RunWith(AndroidJUnit4::class) diff --git a/app/src/test/java/org/oppia/android/app/testing/options/OptionsFragmentTest.kt b/app/src/test/java/org/oppia/android/app/testing/options/OptionsFragmentTest.kt index 3bc06d099b5..9e1106b1301 100644 --- a/app/src/test/java/org/oppia/android/app/testing/options/OptionsFragmentTest.kt +++ b/app/src/test/java/org/oppia/android/app/testing/options/OptionsFragmentTest.kt @@ -83,7 +83,11 @@ import org.oppia.android.util.networking.NetworkConnectionUtilDebugModule import org.oppia.android.util.parser.html.HtmlParserEntityTypeModule import org.oppia.android.util.parser.image.GlideImageLoaderModule import org.oppia.android.util.parser.image.ImageParsingModule +import org.oppia.android.util.platformparameter.CACHE_LATEX_RENDERING +import org.oppia.android.util.platformparameter.CACHE_LATEX_RENDERING_DEFAULT_VALUE +import org.oppia.android.util.platformparameter.CacheLatexRendering import org.oppia.android.util.platformparameter.EnableLanguageSelectionUi +import org.oppia.android.util.platformparameter.PlatformParameterSingleton import org.oppia.android.util.platformparameter.PlatformParameterValue import org.oppia.android.util.platformparameter.SPLASH_SCREEN_WELCOME_MSG_DEFAULT_VALUE import org.oppia.android.util.platformparameter.SYNC_UP_WORKER_TIME_PERIOD_IN_HOURS_DEFAULT_VALUE @@ -93,10 +97,6 @@ import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode import javax.inject.Inject import javax.inject.Singleton -import org.oppia.android.util.platformparameter.CACHE_LATEX_RENDERING -import org.oppia.android.util.platformparameter.CACHE_LATEX_RENDERING_DEFAULT_VALUE -import org.oppia.android.util.platformparameter.CacheLatexRendering -import org.oppia.android.util.platformparameter.PlatformParameterSingleton @RunWith(AndroidJUnit4::class) @Config(application = OptionsFragmentTest.TestApplication::class, qualifiers = "sw600dp") diff --git a/utility/src/main/java/org/oppia/android/util/parser/math/MathBitmapModelLoader.kt b/utility/src/main/java/org/oppia/android/util/parser/math/MathBitmapModelLoader.kt index d88cc7d37bc..7dc3212152a 100644 --- a/utility/src/main/java/org/oppia/android/util/parser/math/MathBitmapModelLoader.kt +++ b/utility/src/main/java/org/oppia/android/util/parser/math/MathBitmapModelLoader.kt @@ -17,8 +17,6 @@ import com.bumptech.glide.load.model.ModelLoaderFactory import com.bumptech.glide.load.model.MultiModelLoaderFactory import com.bumptech.glide.request.target.Target import io.github.karino2.kotlitex.view.MathExpressionSpan -import java.io.ByteArrayOutputStream -import java.nio.ByteBuffer import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -26,6 +24,8 @@ import kotlinx.coroutines.withContext import org.oppia.android.util.logging.ConsoleLogger import org.oppia.android.util.logging.ConsoleLoggerInjectorProvider import org.oppia.android.util.threading.DispatcherInjectorProvider +import java.io.ByteArrayOutputStream +import java.nio.ByteBuffer /** * [ModelLoader] for rendering and caching bitmap representations of LaTeX represented by @@ -109,8 +109,8 @@ class MathBitmapModelLoader private constructor( } // Use Android's StaticLayout to ensure the text is rendered correctly. Note that the - // constants are derived from TextView's defaults (except width which is defaulted to 0 since - // the width isn't necessarily known ahead of time). + // constants are derived from TextView's defaults (except width which is defaulted to 0 + // since the width isn't necessarily known ahead of time). @Suppress("DEPRECATION") // This call is necessary for the supported min API version. val staticTextLayout = StaticLayout( @@ -134,7 +134,8 @@ class MathBitmapModelLoader private constructor( staticTextLayout.draw(bitmapCanvas) val finalWidth = if (targetWidth == Target.SIZE_ORIGINAL) bounds.width() else targetWidth - val finalHeight = if (targetHeight == Target.SIZE_ORIGINAL) bounds.height() else targetHeight + val finalHeight = + if (targetHeight == Target.SIZE_ORIGINAL) bounds.height() else targetHeight // Compute the final bitmap (which might need to be scaled depending on options). val bitmap = if (canvasBitmap.width != finalWidth || canvasBitmap.height != finalHeight) { From a6dc7d42a6a39fa9065b632c32e7a8b3992d402b Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 18 Feb 2022 11:01:21 -0800 Subject: [PATCH 138/162] Address reviewer comment. Clarifies the documentation in the test runner around parameter injection. --- .../testing/junit/OppiaParameterizedTestRunner.kt | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/testing/src/main/java/org/oppia/android/testing/junit/OppiaParameterizedTestRunner.kt b/testing/src/main/java/org/oppia/android/testing/junit/OppiaParameterizedTestRunner.kt index 6638a801db0..10a446665d5 100644 --- a/testing/src/main/java/org/oppia/android/testing/junit/OppiaParameterizedTestRunner.kt +++ b/testing/src/main/java/org/oppia/android/testing/junit/OppiaParameterizedTestRunner.kt @@ -29,22 +29,25 @@ import kotlin.reflect.KClass * * To introduce parameterized tests, add this runner along with one or more [Parameter]-annotated * fields and one or more [RunParameterized]-annotated methods (where each method should have - * multiple [Iteration]s defined to describe each test iteration). Here's a simple example: + * multiple [Iteration]s defined to describe each test iteration). Note that only strings and + * primitive types (e.g. [Int], [Long], [Float], [Double], and [Boolean]) are supported for + * parameter injection. Here's a simple example: * * ```kotlin * @RunWith(OppiaParameterizedTestRunner::class) * @SelectRunnerPlatform(ParameterizedRobolectricTestRunner::class) * class ExampleParameterizedTest { - * @Parameter lateinit var parameter: String + * @Parameter lateinit var strParam: String + * @Parameter var intParam: Int = Int.MIN_VALUE // Inited because primitives can't be lateinit. * * @Test * @RunParameterized( - * Iteration("first", "parameter=first value"), - * Iteration("second", "parameter=second value"), - * Iteration("third", "parameter=third value") + * Iteration("first", "strParam=first value", "intParam=12"), + * Iteration("second", "strParam=second value", "intParam=-72"), + * Iteration("third", "strParam=third value", "intParam=15") * ) * fun testParams_multipleVals_isConsistent() { - * val result = performOperation(parameter) + * val result = performOperation(strParam, intParam) * assertThat(result).isEqualTo(consistentExpectedValue) * } * } From fd9ec1f3c402714c539e16659fac6c93a1ffacbe Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 18 Feb 2022 13:20:00 -0800 Subject: [PATCH 139/162] Fix broken build. --- .../org/oppia/android/domain/classify/BUILD.bazel | 12 ++++++------ .../oppia/android/domain/classify/rules/BUILD.bazel | 6 +++--- .../classify/rules/dragAndDropSortInput/BUILD.bazel | 4 ++-- .../domain/classify/rules/fractioninput/BUILD.bazel | 4 ++-- .../classify/rules/imageClickInput/BUILD.bazel | 4 ++-- .../classify/rules/itemselectioninput/BUILD.bazel | 4 ++-- .../classify/rules/multiplechoiceinput/BUILD.bazel | 4 ++-- .../classify/rules/numberwithunits/BUILD.bazel | 4 ++-- .../domain/classify/rules/numericinput/BUILD.bazel | 4 ++-- .../domain/classify/rules/ratioinput/BUILD.bazel | 4 ++-- .../domain/classify/rules/textinput/BUILD.bazel | 6 +++--- 11 files changed, 28 insertions(+), 28 deletions(-) diff --git a/domain/src/main/java/org/oppia/android/domain/classify/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/classify/BUILD.bazel index 6b7d7c7fe59..b2222c9b536 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/classify/BUILD.bazel @@ -14,9 +14,9 @@ kt_android_library( deps = [ ":classification_result", ":interaction_classifier", - "//model:exploration_java_proto_lite", - "//model:interaction_object_java_proto_lite", - "//model:translation_java_proto_lite", + "//model/src/main/proto:exploration_java_proto_lite", + "//model/src/main/proto:interaction_object_java_proto_lite", + "//model/src/main/proto:translation_java_proto_lite", "//third_party:javax_inject_javax_inject", ], ) @@ -28,7 +28,7 @@ kt_android_library( ], visibility = ["//:oppia_api_visibility"], deps = [ - "//model:exploration_java_proto_lite", + "//model/src/main/proto:exploration_java_proto_lite", ], ) @@ -77,8 +77,8 @@ kt_android_library( ], visibility = ["//:__subpackages__"], deps = [ - "//model:interaction_object_java_proto_lite", - "//model:translation_java_proto_lite", + "//model/src/main/proto:interaction_object_java_proto_lite", + "//model/src/main/proto:translation_java_proto_lite", ], ) diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/classify/rules/BUILD.bazel index 95880ffd7c6..f581741e245 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/BUILD.bazel @@ -15,8 +15,8 @@ kt_android_library( deps = [ ":rule_classifier_provider", "//domain/src/main/java/org/oppia/android/domain/classify:rule_classifier", - "//model:interaction_object_java_proto_lite", - "//model:translation_java_proto_lite", + "//model/src/main/proto:interaction_object_java_proto_lite", + "//model/src/main/proto:translation_java_proto_lite", "//third_party:javax_inject_javax_inject", ], ) @@ -30,7 +30,7 @@ kt_android_library( visibility = ["//:__subpackages__"], deps = [ "//domain/src/main/java/org/oppia/android/domain/classify:rule_classifier", - "//model:translation_java_proto_lite", + "//model/src/main/proto:translation_java_proto_lite", "//third_party:javax_inject_javax_inject", ], ) diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/BUILD.bazel index dfed4bcfc94..b77cd0d460b 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/BUILD.bazel @@ -19,8 +19,8 @@ kt_android_library( "//domain/src/main/java/org/oppia/android/domain/classify/rules:generic_rule_classifier", "//domain/src/main/java/org/oppia/android/domain/classify/rules:rule_classifier_provider", "//domain/src/main/java/org/oppia/android/domain/util:extensions", - "//model:interaction_object_java_proto_lite", - "//model:translation_java_proto_lite", + "//model/src/main/proto:interaction_object_java_proto_lite", + "//model/src/main/proto:translation_java_proto_lite", "//third_party:javax_inject_javax_inject", ], ) diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/BUILD.bazel index 0de2f2b01a3..796ebb8680f 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/BUILD.bazel @@ -25,8 +25,8 @@ kt_android_library( "//domain/src/main/java/org/oppia/android/domain/classify/rules:generic_rule_classifier", "//domain/src/main/java/org/oppia/android/domain/classify/rules:rule_classifier_provider", "//domain/src/main/java/org/oppia/android/domain/util:extensions", - "//model:interaction_object_java_proto_lite", - "//model:translation_java_proto_lite", + "//model/src/main/proto:interaction_object_java_proto_lite", + "//model/src/main/proto:translation_java_proto_lite", "//third_party:javax_inject_javax_inject", ], ) diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/imageClickInput/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/classify/rules/imageClickInput/BUILD.bazel index a6edda847b8..d31ece53126 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/imageClickInput/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/imageClickInput/BUILD.bazel @@ -16,8 +16,8 @@ kt_android_library( "//domain/src/main/java/org/oppia/android/domain/classify/rules:generic_rule_classifier", "//domain/src/main/java/org/oppia/android/domain/classify/rules:rule_classifier_provider", "//domain/src/main/java/org/oppia/android/domain/util:extensions", - "//model:interaction_object_java_proto_lite", - "//model:translation_java_proto_lite", + "//model/src/main/proto:interaction_object_java_proto_lite", + "//model/src/main/proto:translation_java_proto_lite", "//third_party:javax_inject_javax_inject", ], ) diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput/BUILD.bazel index 0827ee61f10..a17f0aeed77 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput/BUILD.bazel @@ -19,8 +19,8 @@ kt_android_library( "//domain/src/main/java/org/oppia/android/domain/classify/rules:generic_rule_classifier", "//domain/src/main/java/org/oppia/android/domain/classify/rules:rule_classifier_provider", "//domain/src/main/java/org/oppia/android/domain/util:extensions", - "//model:interaction_object_java_proto_lite", - "//model:translation_java_proto_lite", + "//model/src/main/proto:interaction_object_java_proto_lite", + "//model/src/main/proto:translation_java_proto_lite", "//third_party:javax_inject_javax_inject", ], ) diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/multiplechoiceinput/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/classify/rules/multiplechoiceinput/BUILD.bazel index 62b9e517e08..40560cba8cd 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/multiplechoiceinput/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/multiplechoiceinput/BUILD.bazel @@ -16,8 +16,8 @@ kt_android_library( "//domain/src/main/java/org/oppia/android/domain/classify/rules:generic_rule_classifier", "//domain/src/main/java/org/oppia/android/domain/classify/rules:rule_classifier_provider", "//domain/src/main/java/org/oppia/android/domain/util:extensions", - "//model:interaction_object_java_proto_lite", - "//model:translation_java_proto_lite", + "//model/src/main/proto:interaction_object_java_proto_lite", + "//model/src/main/proto:translation_java_proto_lite", "//third_party:javax_inject_javax_inject", ], ) diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits/BUILD.bazel index f3e3b8c5c38..93b26be66de 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits/BUILD.bazel @@ -17,8 +17,8 @@ kt_android_library( "//domain/src/main/java/org/oppia/android/domain/classify/rules:generic_rule_classifier", "//domain/src/main/java/org/oppia/android/domain/classify/rules:rule_classifier_provider", "//domain/src/main/java/org/oppia/android/domain/util:extensions", - "//model:interaction_object_java_proto_lite", - "//model:translation_java_proto_lite", + "//model/src/main/proto:interaction_object_java_proto_lite", + "//model/src/main/proto:translation_java_proto_lite", "//third_party:javax_inject_javax_inject", ], ) diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/BUILD.bazel index 0276f06dec7..16eb2cb8cb3 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/BUILD.bazel @@ -22,8 +22,8 @@ kt_android_library( "//domain/src/main/java/org/oppia/android/domain/classify/rules:generic_rule_classifier", "//domain/src/main/java/org/oppia/android/domain/classify/rules:rule_classifier_provider", "//domain/src/main/java/org/oppia/android/domain/util:extensions", - "//model:interaction_object_java_proto_lite", - "//model:translation_java_proto_lite", + "//model/src/main/proto:interaction_object_java_proto_lite", + "//model/src/main/proto:translation_java_proto_lite", "//third_party:javax_inject_javax_inject", ], ) diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput/BUILD.bazel index 042ed4652a6..9ae12eba548 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput/BUILD.bazel @@ -19,8 +19,8 @@ kt_android_library( "//domain/src/main/java/org/oppia/android/domain/classify/rules:generic_rule_classifier", "//domain/src/main/java/org/oppia/android/domain/classify/rules:rule_classifier_provider", "//domain/src/main/java/org/oppia/android/domain/util:extensions", - "//model:interaction_object_java_proto_lite", - "//model:translation_java_proto_lite", + "//model/src/main/proto:interaction_object_java_proto_lite", + "//model/src/main/proto:translation_java_proto_lite", "//third_party:javax_inject_javax_inject", ], ) diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/BUILD.bazel index 4acd72a6675..0701b29ff46 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/BUILD.bazel @@ -20,9 +20,9 @@ kt_android_library( "//domain/src/main/java/org/oppia/android/domain/classify/rules:rule_classifier_provider", "//domain/src/main/java/org/oppia/android/domain/translation:translation_controller", "//domain/src/main/java/org/oppia/android/domain/util:extensions", - "//model:exploration_java_proto_lite", - "//model:interaction_object_java_proto_lite", - "//model:translation_java_proto_lite", + "//model/src/main/proto:exploration_java_proto_lite", + "//model/src/main/proto:interaction_object_java_proto_lite", + "//model/src/main/proto:translation_java_proto_lite", "//third_party:javax_inject_javax_inject", "//utility/src/main/java/org/oppia/android/util/locale:oppia_locale", ], From 0fc8a1b81df9a5c0537da6c7ffdc88a008f59669 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 18 Feb 2022 13:30:14 -0800 Subject: [PATCH 140/162] Fix broken build post-merge. --- .../oppia/android/domain/classify/rules/textinput/BUILD.bazel | 1 + 1 file changed, 1 insertion(+) diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/BUILD.bazel index 0701b29ff46..a28d873bc5a 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/BUILD.bazel @@ -24,6 +24,7 @@ kt_android_library( "//model/src/main/proto:interaction_object_java_proto_lite", "//model/src/main/proto:translation_java_proto_lite", "//third_party:javax_inject_javax_inject", + "//utility/src/main/java/org/oppia/android/util/extensions:string_extensions", "//utility/src/main/java/org/oppia/android/util/locale:oppia_locale", ], ) From c59598c9e5ae60b5a120d2a4f2fcb05158a66e15 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 18 Feb 2022 13:51:58 -0800 Subject: [PATCH 141/162] Fix broken post-merge classifier. --- .../rules/numericexpressioninput/BUILD.bazel | 46 +++++++++++++++++++ .../rules/numericexpressioninput/BUILD.bazel | 4 ++ 2 files changed, 50 insertions(+) create mode 100644 domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/BUILD.bazel diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/BUILD.bazel new file mode 100644 index 00000000000..f8a8eb828e3 --- /dev/null +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/BUILD.bazel @@ -0,0 +1,46 @@ +""" +Classifiers for the 'NumericExpressionInput' interaction. +""" + +load("@dagger//:workspace_defs.bzl", "dagger_rules") +load("@io_bazel_rules_kotlin//kotlin:kotlin.bzl", "kt_android_library") + +kt_android_library( + name = "numeric_expression_input_providers", + srcs = [ + "NumericExpressionInputIsEquivalentToRuleClassifierProvider.kt", + "NumericExpressionInputMatchesExactlyWithRuleClassifierProvider.kt", + "NumericExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt", + ], + visibility = ["//domain:domain_testing_visibility"], + deps = [ + ":dagger", + "//domain/src/main/java/org/oppia/android/domain/classify:rule_classifier", + "//domain/src/main/java/org/oppia/android/domain/classify/rules:generic_rule_classifier", + "//domain/src/main/java/org/oppia/android/domain/classify/rules:rule_classifier_provider", + "//model/src/main/proto:exploration_java_proto_lite", + "//model/src/main/proto:interaction_object_java_proto_lite", + "//model/src/main/proto:translation_java_proto_lite", + "//third_party:javax_inject_javax_inject", + "//utility/src/main/java/org/oppia/android/util/locale:oppia_locale", + "//utility/src/main/java/org/oppia/android/util/logging:console_logger", + "//utility/src/main/java/org/oppia/android/util/math:extensions", + "//utility/src/main/java/org/oppia/android/util/math:math_expression_parser", + ], +) + +kt_android_library( + name = "numeric_expression_input_rule_module", + srcs = [ + "NumericExpressionInputModule.kt", + ], + visibility = ["//:oppia_prod_module_visibility"], + deps = [ + ":dagger", + ":numeric_expression_input_providers", + "//domain/src/main/java/org/oppia/android/domain/classify:rule_classifier", + "//domain/src/main/java/org/oppia/android/domain/classify/rules:rule_classifier_provider", + ], +) + +dagger_rules() diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/BUILD.bazel b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/BUILD.bazel index f757f37b29e..7755b5ca94a 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/BUILD.bazel +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/numericexpressioninput/BUILD.bazel @@ -14,6 +14,7 @@ oppia_android_test( deps = [ ":dagger", "//domain", + "//domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput:numeric_expression_input_providers", "//model/src/main/proto:interaction_object_java_proto_lite", "//testing/src/main/java/org/oppia/android/testing/junit:oppia_parameterized_test_runner", "//testing/src/main/java/org/oppia/android/testing/junit:parameterized_robolectric_test_runner", @@ -39,6 +40,7 @@ oppia_android_test( deps = [ ":dagger", "//domain", + "//domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput:numeric_expression_input_providers", "//model/src/main/proto:interaction_object_java_proto_lite", "//testing/src/main/java/org/oppia/android/testing/junit:oppia_parameterized_test_runner", "//testing/src/main/java/org/oppia/android/testing/junit:parameterized_robolectric_test_runner", @@ -64,6 +66,7 @@ oppia_android_test( deps = [ ":dagger", "//domain", + "//domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput:numeric_expression_input_providers", "//model/src/main/proto:interaction_object_java_proto_lite", "//testing/src/main/java/org/oppia/android/testing/junit:oppia_parameterized_test_runner", "//testing/src/main/java/org/oppia/android/testing/junit:parameterized_robolectric_test_runner", @@ -89,6 +92,7 @@ oppia_android_test( deps = [ ":dagger", "//domain", + "//domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput:numeric_expression_input_rule_module", "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module", "//testing/src/main/java/org/oppia/android/testing/threading:test_module", "//testing/src/main/java/org/oppia/android/testing/time:test_module", From 41be40ee97ce84c818dfc1928d37b14d6bd36b06 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 18 Feb 2022 13:55:31 -0800 Subject: [PATCH 142/162] Address reviewer comment. --- .../android/domain/classify/AnswerClassificationController.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/domain/src/main/java/org/oppia/android/domain/classify/AnswerClassificationController.kt b/domain/src/main/java/org/oppia/android/domain/classify/AnswerClassificationController.kt index 55eb6023584..979b8a28fc6 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/AnswerClassificationController.kt +++ b/domain/src/main/java/org/oppia/android/domain/classify/AnswerClassificationController.kt @@ -36,7 +36,6 @@ class AnswerClassificationController @Inject constructor( "expected one of: ${interactionClassifiers.keys}" } // TODO(#207): Add support for additional classification types. - interaction.customizationArgsMap return classifyAnswer( answer, interaction.answerGroupsList, From 1ea22417c6b34ab900d26e45c5c36fd0da6e3574 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 18 Feb 2022 14:06:08 -0800 Subject: [PATCH 143/162] Post-merge build fixes. --- .../oppia/android/domain/classify/BUILD.bazel | 15 ++++++ .../algebraicexpressioninput/BUILD.bazel | 47 +++++++++++++++++++ .../rules/dragAndDropSortInput/BUILD.bazel | 1 + .../classify/rules/fractioninput/BUILD.bazel | 1 + .../rules/imageClickInput/BUILD.bazel | 1 + .../rules/itemselectioninput/BUILD.bazel | 1 + .../rules/multiplechoiceinput/BUILD.bazel | 1 + .../rules/numberwithunits/BUILD.bazel | 1 + .../rules/numericexpressioninput/BUILD.bazel | 1 + .../classify/rules/numericinput/BUILD.bazel | 1 + .../classify/rules/ratioinput/BUILD.bazel | 1 + .../classify/rules/textinput/BUILD.bazel | 1 + .../algebraicexpressioninput/BUILD.bazel | 4 ++ 13 files changed, 76 insertions(+) create mode 100644 domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/BUILD.bazel diff --git a/domain/src/main/java/org/oppia/android/domain/classify/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/classify/BUILD.bazel index b2222c9b536..7c523f15e2f 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/classify/BUILD.bazel @@ -12,6 +12,7 @@ kt_android_library( ], visibility = ["//:oppia_api_visibility"], deps = [ + ":classification_context", ":classification_result", ":interaction_classifier", "//model/src/main/proto:exploration_java_proto_lite", @@ -21,6 +22,18 @@ kt_android_library( ], ) +kt_android_library( + name = "classification_context", + srcs = [ + "ClassificationContext.kt", + ], + visibility = ["//:oppia_api_visibility"], + deps = [ + "//model/src/main/proto:exploration_java_proto_lite", + "//model/src/main/proto:translation_java_proto_lite", + ], +) + kt_android_library( name = "classification_result", srcs = [ @@ -38,6 +51,7 @@ kt_android_library( "GenericInteractionClassifier.kt", ], deps = [ + ":classification_context", ":interaction_classifier", ":rule_classifier", ], @@ -77,6 +91,7 @@ kt_android_library( ], visibility = ["//:__subpackages__"], deps = [ + ":classification_context", "//model/src/main/proto:interaction_object_java_proto_lite", "//model/src/main/proto:translation_java_proto_lite", ], diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/BUILD.bazel new file mode 100644 index 00000000000..c59365719f5 --- /dev/null +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/BUILD.bazel @@ -0,0 +1,47 @@ +""" +Classifiers for the 'AlgebraicExpressionInput' interaction. +""" + +load("@dagger//:workspace_defs.bzl", "dagger_rules") +load("@io_bazel_rules_kotlin//kotlin:kotlin.bzl", "kt_android_library") + +kt_android_library( + name = "algebraic_expression_input_providers", + srcs = [ + "AlgebraicExpressionInputIsEquivalentToRuleClassifierProvider.kt", + "AlgebraicExpressionInputMatchesExactlyWithRuleClassifierProvider.kt", + "AlgebraicExpressionInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt", + ], + visibility = ["//domain:domain_testing_visibility"], + deps = [ + ":dagger", + "//domain/src/main/java/org/oppia/android/domain/classify:classification_context", + "//domain/src/main/java/org/oppia/android/domain/classify:rule_classifier", + "//domain/src/main/java/org/oppia/android/domain/classify/rules:generic_rule_classifier", + "//domain/src/main/java/org/oppia/android/domain/classify/rules:rule_classifier_provider", + "//model/src/main/proto:exploration_java_proto_lite", + "//model/src/main/proto:interaction_object_java_proto_lite", + "//model/src/main/proto:translation_java_proto_lite", + "//third_party:javax_inject_javax_inject", + "//utility/src/main/java/org/oppia/android/util/locale:oppia_locale", + "//utility/src/main/java/org/oppia/android/util/logging:console_logger", + "//utility/src/main/java/org/oppia/android/util/math:extensions", + "//utility/src/main/java/org/oppia/android/util/math:math_expression_parser", + ], +) + +kt_android_library( + name = "algebraic_expression_input_rule_module", + srcs = [ + "AlgebraicExpressionInputModule.kt", + ], + visibility = ["//:oppia_prod_module_visibility"], + deps = [ + ":algebraic_expression_input_providers", + ":dagger", + "//domain/src/main/java/org/oppia/android/domain/classify:rule_classifier", + "//domain/src/main/java/org/oppia/android/domain/classify/rules:rule_classifier_provider", + ], +) + +dagger_rules() diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/BUILD.bazel index b77cd0d460b..5dadd526a23 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput/BUILD.bazel @@ -15,6 +15,7 @@ kt_android_library( ], deps = [ ":dagger", + "//domain/src/main/java/org/oppia/android/domain/classify:classification_context", "//domain/src/main/java/org/oppia/android/domain/classify:rule_classifier", "//domain/src/main/java/org/oppia/android/domain/classify/rules:generic_rule_classifier", "//domain/src/main/java/org/oppia/android/domain/classify/rules:rule_classifier_provider", diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/BUILD.bazel index 796ebb8680f..5da32a18e7c 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput/BUILD.bazel @@ -21,6 +21,7 @@ kt_android_library( ], deps = [ ":dagger", + "//domain/src/main/java/org/oppia/android/domain/classify:classification_context", "//domain/src/main/java/org/oppia/android/domain/classify:rule_classifier", "//domain/src/main/java/org/oppia/android/domain/classify/rules:generic_rule_classifier", "//domain/src/main/java/org/oppia/android/domain/classify/rules:rule_classifier_provider", diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/imageClickInput/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/classify/rules/imageClickInput/BUILD.bazel index d31ece53126..eb216ecc013 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/imageClickInput/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/imageClickInput/BUILD.bazel @@ -12,6 +12,7 @@ kt_android_library( ], deps = [ ":dagger", + "//domain/src/main/java/org/oppia/android/domain/classify:classification_context", "//domain/src/main/java/org/oppia/android/domain/classify:rule_classifier", "//domain/src/main/java/org/oppia/android/domain/classify/rules:generic_rule_classifier", "//domain/src/main/java/org/oppia/android/domain/classify/rules:rule_classifier_provider", diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput/BUILD.bazel index a17f0aeed77..b1ae3ef0a1f 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput/BUILD.bazel @@ -15,6 +15,7 @@ kt_android_library( ], deps = [ ":dagger", + "//domain/src/main/java/org/oppia/android/domain/classify:classification_context", "//domain/src/main/java/org/oppia/android/domain/classify:rule_classifier", "//domain/src/main/java/org/oppia/android/domain/classify/rules:generic_rule_classifier", "//domain/src/main/java/org/oppia/android/domain/classify/rules:rule_classifier_provider", diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/multiplechoiceinput/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/classify/rules/multiplechoiceinput/BUILD.bazel index 40560cba8cd..19a34a097ca 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/multiplechoiceinput/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/multiplechoiceinput/BUILD.bazel @@ -12,6 +12,7 @@ kt_android_library( ], deps = [ ":dagger", + "//domain/src/main/java/org/oppia/android/domain/classify:classification_context", "//domain/src/main/java/org/oppia/android/domain/classify:rule_classifier", "//domain/src/main/java/org/oppia/android/domain/classify/rules:generic_rule_classifier", "//domain/src/main/java/org/oppia/android/domain/classify/rules:rule_classifier_provider", diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits/BUILD.bazel index 93b26be66de..1954b42da92 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits/BUILD.bazel @@ -13,6 +13,7 @@ kt_android_library( ], deps = [ ":dagger", + "//domain/src/main/java/org/oppia/android/domain/classify:classification_context", "//domain/src/main/java/org/oppia/android/domain/classify:rule_classifier", "//domain/src/main/java/org/oppia/android/domain/classify/rules:generic_rule_classifier", "//domain/src/main/java/org/oppia/android/domain/classify/rules:rule_classifier_provider", diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/BUILD.bazel index f8a8eb828e3..5a11936c237 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput/BUILD.bazel @@ -15,6 +15,7 @@ kt_android_library( visibility = ["//domain:domain_testing_visibility"], deps = [ ":dagger", + "//domain/src/main/java/org/oppia/android/domain/classify:classification_context", "//domain/src/main/java/org/oppia/android/domain/classify:rule_classifier", "//domain/src/main/java/org/oppia/android/domain/classify/rules:generic_rule_classifier", "//domain/src/main/java/org/oppia/android/domain/classify/rules:rule_classifier_provider", diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/BUILD.bazel index 16eb2cb8cb3..2d8f8e7136e 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput/BUILD.bazel @@ -18,6 +18,7 @@ kt_android_library( ], deps = [ ":dagger", + "//domain/src/main/java/org/oppia/android/domain/classify:classification_context", "//domain/src/main/java/org/oppia/android/domain/classify:rule_classifier", "//domain/src/main/java/org/oppia/android/domain/classify/rules:generic_rule_classifier", "//domain/src/main/java/org/oppia/android/domain/classify/rules:rule_classifier_provider", diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput/BUILD.bazel index 9ae12eba548..6fb01de8afb 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput/BUILD.bazel @@ -15,6 +15,7 @@ kt_android_library( ], deps = [ ":dagger", + "//domain/src/main/java/org/oppia/android/domain/classify:classification_context", "//domain/src/main/java/org/oppia/android/domain/classify:rule_classifier", "//domain/src/main/java/org/oppia/android/domain/classify/rules:generic_rule_classifier", "//domain/src/main/java/org/oppia/android/domain/classify/rules:rule_classifier_provider", diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/BUILD.bazel index a28d873bc5a..4a8a59451fa 100644 --- a/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/textinput/BUILD.bazel @@ -15,6 +15,7 @@ kt_android_library( ], deps = [ ":dagger", + "//domain/src/main/java/org/oppia/android/domain/classify:classification_context", "//domain/src/main/java/org/oppia/android/domain/classify:rule_classifier", "//domain/src/main/java/org/oppia/android/domain/classify/rules:generic_rule_classifier", "//domain/src/main/java/org/oppia/android/domain/classify/rules:rule_classifier_provider", diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/BUILD.bazel b/domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/BUILD.bazel index a9bacbd93ba..786cd962d4d 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/BUILD.bazel +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput/BUILD.bazel @@ -14,6 +14,7 @@ oppia_android_test( deps = [ ":dagger", "//domain", + "//domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput:algebraic_expression_input_providers", "//model/src/main/proto:interaction_object_java_proto_lite", "//testing/src/main/java/org/oppia/android/testing/junit:oppia_parameterized_test_runner", "//testing/src/main/java/org/oppia/android/testing/junit:parameterized_robolectric_test_runner", @@ -39,6 +40,7 @@ oppia_android_test( deps = [ ":dagger", "//domain", + "//domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput:algebraic_expression_input_providers", "//model/src/main/proto:interaction_object_java_proto_lite", "//testing/src/main/java/org/oppia/android/testing/junit:oppia_parameterized_test_runner", "//testing/src/main/java/org/oppia/android/testing/junit:parameterized_robolectric_test_runner", @@ -64,6 +66,7 @@ oppia_android_test( deps = [ ":dagger", "//domain", + "//domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput:algebraic_expression_input_providers", "//model/src/main/proto:interaction_object_java_proto_lite", "//testing/src/main/java/org/oppia/android/testing/junit:oppia_parameterized_test_runner", "//testing/src/main/java/org/oppia/android/testing/junit:parameterized_robolectric_test_runner", @@ -89,6 +92,7 @@ oppia_android_test( deps = [ ":dagger", "//domain", + "//domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput:algebraic_expression_input_rule_module", "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module", "//testing/src/main/java/org/oppia/android/testing/threading:test_module", "//testing/src/main/java/org/oppia/android/testing/time:test_module", From 3407bf47ef7b3122c499dbfcb0d0be205feb33dd Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 18 Feb 2022 14:11:31 -0800 Subject: [PATCH 144/162] Post-merge build fixes for new classifiers. --- .../rules/mathequationinput/BUILD.bazel | 47 +++++++++++++++++++ .../rules/mathequationinput/BUILD.bazel | 4 ++ 2 files changed, 51 insertions(+) create mode 100644 domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/BUILD.bazel diff --git a/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/BUILD.bazel new file mode 100644 index 00000000000..0a70ea19348 --- /dev/null +++ b/domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput/BUILD.bazel @@ -0,0 +1,47 @@ +""" +Classifiers for the 'MathEquationInput' interaction. +""" + +load("@dagger//:workspace_defs.bzl", "dagger_rules") +load("@io_bazel_rules_kotlin//kotlin:kotlin.bzl", "kt_android_library") + +kt_android_library( + name = "math_equation_input_providers", + srcs = [ + "MathEquationInputIsEquivalentToRuleClassifierProvider.kt", + "MathEquationInputMatchesExactlyWithRuleClassifierProvider.kt", + "MathEquationInputMatchesUpToTrivialManipulationsRuleClassifierProvider.kt", + ], + visibility = ["//domain:domain_testing_visibility"], + deps = [ + ":dagger", + "//domain/src/main/java/org/oppia/android/domain/classify:classification_context", + "//domain/src/main/java/org/oppia/android/domain/classify:rule_classifier", + "//domain/src/main/java/org/oppia/android/domain/classify/rules:generic_rule_classifier", + "//domain/src/main/java/org/oppia/android/domain/classify/rules:rule_classifier_provider", + "//model/src/main/proto:exploration_java_proto_lite", + "//model/src/main/proto:interaction_object_java_proto_lite", + "//model/src/main/proto:translation_java_proto_lite", + "//third_party:javax_inject_javax_inject", + "//utility/src/main/java/org/oppia/android/util/locale:oppia_locale", + "//utility/src/main/java/org/oppia/android/util/logging:console_logger", + "//utility/src/main/java/org/oppia/android/util/math:extensions", + "//utility/src/main/java/org/oppia/android/util/math:math_expression_parser", + ], +) + +kt_android_library( + name = "math_equation_input_rule_module", + srcs = [ + "MathEquationInputModule.kt", + ], + visibility = ["//:oppia_prod_module_visibility"], + deps = [ + ":dagger", + ":math_equation_input_providers", + "//domain/src/main/java/org/oppia/android/domain/classify:rule_classifier", + "//domain/src/main/java/org/oppia/android/domain/classify/rules:rule_classifier_provider", + ], +) + +dagger_rules() diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/mathequationinput/BUILD.bazel b/domain/src/test/java/org/oppia/android/domain/classify/rules/mathequationinput/BUILD.bazel index 9cfc129f250..88686b47484 100644 --- a/domain/src/test/java/org/oppia/android/domain/classify/rules/mathequationinput/BUILD.bazel +++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/mathequationinput/BUILD.bazel @@ -14,6 +14,7 @@ oppia_android_test( deps = [ ":dagger", "//domain", + "//domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput:math_equation_input_providers", "//model/src/main/proto:interaction_object_java_proto_lite", "//testing/src/main/java/org/oppia/android/testing/junit:oppia_parameterized_test_runner", "//testing/src/main/java/org/oppia/android/testing/junit:parameterized_robolectric_test_runner", @@ -39,6 +40,7 @@ oppia_android_test( deps = [ ":dagger", "//domain", + "//domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput:math_equation_input_providers", "//model/src/main/proto:interaction_object_java_proto_lite", "//testing/src/main/java/org/oppia/android/testing/junit:oppia_parameterized_test_runner", "//testing/src/main/java/org/oppia/android/testing/junit:parameterized_robolectric_test_runner", @@ -64,6 +66,7 @@ oppia_android_test( deps = [ ":dagger", "//domain", + "//domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput:math_equation_input_providers", "//model/src/main/proto:interaction_object_java_proto_lite", "//testing/src/main/java/org/oppia/android/testing/junit:oppia_parameterized_test_runner", "//testing/src/main/java/org/oppia/android/testing/junit:parameterized_robolectric_test_runner", @@ -89,6 +92,7 @@ oppia_android_test( deps = [ ":dagger", "//domain", + "//domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput:math_equation_input_rule_module", "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module", "//testing/src/main/java/org/oppia/android/testing/threading:test_module", "//testing/src/main/java/org/oppia/android/testing/time:test_module", From 4328d564ce1352ff5ff70ecc365ac4a0a4754b47 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 18 Feb 2022 15:10:32 -0800 Subject: [PATCH 145/162] Post-merge build fixes. --- app/BUILD.bazel | 6 ++++++ domain/BUILD.bazel | 3 +++ .../java/org/oppia/android/testing/junit/BUILD.bazel | 9 +++++++++ 3 files changed, 18 insertions(+) diff --git a/app/BUILD.bazel b/app/BUILD.bazel index 1946d16f34d..88637d4fdfa 100644 --- a/app/BUILD.bazel +++ b/app/BUILD.bazel @@ -733,13 +733,16 @@ kt_android_library( "//data/src/main/java/org/oppia/android/data/backends/gae:network_config_prod_module", "//data/src/main/java/org/oppia/android/data/backends/gae:prod_module", "//domain/src/main/java/org/oppia/android/domain/classify:interactions_module", + "//domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput:algebraic_expression_input_rule_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/continueinteraction:continue_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput:drag_and_drop_sort_input_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput:fraction_input_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/imageClickInput:image_click_input_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput:item_selection_input_module", + "//domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput:math_equation_input_rule_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/multiplechoiceinput:multiple_choice_input_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits:number_with_units_rule_module", + "//domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput:numeric_expression_input_rule_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput:numeric_input_rule_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput:ratio_input_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/textinput:text_input_rule_module", @@ -825,13 +828,16 @@ TEST_DEPS = [ "//app/src/main/java/org/oppia/android/app/translation/testing:test_module", "//domain", "//domain/src/main/java/org/oppia/android/domain/audio:audio_player_controller", + "//domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput:algebraic_expression_input_rule_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/continueinteraction:continue_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput:drag_and_drop_sort_input_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput:fraction_input_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/imageClickInput:image_click_input_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput:item_selection_input_module", + "//domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput:math_equation_input_rule_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/multiplechoiceinput:multiple_choice_input_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits:number_with_units_rule_module", + "//domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput:numeric_expression_input_rule_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput:numeric_input_rule_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput:ratio_input_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/textinput:text_input_rule_module", diff --git a/domain/BUILD.bazel b/domain/BUILD.bazel index d02df93f2ef..ef961b09449 100755 --- a/domain/BUILD.bazel +++ b/domain/BUILD.bazel @@ -173,13 +173,16 @@ TEST_DEPS = [ "//domain/src/main/java/org/oppia/android/domain/audio:audio_player_controller", "//domain/src/main/java/org/oppia/android/domain/audio:cellular_audio_dialog_controller", "//domain/src/main/java/org/oppia/android/domain/classify:interactions_module", + "//domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput:algebraic_expression_input_rule_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/continueinteraction:continue_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput:drag_and_drop_sort_input_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput:fraction_input_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/imageClickInput:image_click_input_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput:item_selection_input_module", + "//domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput:math_equation_input_rule_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/multiplechoiceinput:multiple_choice_input_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits:number_with_units_rule_module", + "//domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput:numeric_expression_input_rule_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput:numeric_input_rule_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput:ratio_input_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/textinput:text_input_rule_module", diff --git a/testing/src/test/java/org/oppia/android/testing/junit/BUILD.bazel b/testing/src/test/java/org/oppia/android/testing/junit/BUILD.bazel index 6c09229ee41..afae4ea110d 100644 --- a/testing/src/test/java/org/oppia/android/testing/junit/BUILD.bazel +++ b/testing/src/test/java/org/oppia/android/testing/junit/BUILD.bazel @@ -18,13 +18,16 @@ oppia_android_test( "//app/src/main/java/org/oppia/android/app/translation/testing:test_module", "//domain", "//domain/src/main/java/org/oppia/android/domain/classify:interactions_module", + "//domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput:algebraic_expression_input_rule_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/continueinteraction:continue_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput:drag_and_drop_sort_input_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput:fraction_input_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/imageClickInput:image_click_input_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput:item_selection_input_module", + "//domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput:math_equation_input_rule_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/multiplechoiceinput:multiple_choice_input_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits:number_with_units_rule_module", + "//domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput:numeric_expression_input_rule_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput:numeric_input_rule_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput:ratio_input_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/textinput:text_input_rule_module", @@ -63,13 +66,16 @@ oppia_android_test( "//app/src/main/java/org/oppia/android/app/translation/testing:test_module", "//domain", "//domain/src/main/java/org/oppia/android/domain/classify:interactions_module", + "//domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput:algebraic_expression_input_rule_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/continueinteraction:continue_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput:drag_and_drop_sort_input_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput:fraction_input_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/imageClickInput:image_click_input_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput:item_selection_input_module", + "//domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput:math_equation_input_rule_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/multiplechoiceinput:multiple_choice_input_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits:number_with_units_rule_module", + "//domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput:numeric_expression_input_rule_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput:numeric_input_rule_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput:ratio_input_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/textinput:text_input_rule_module", @@ -107,13 +113,16 @@ oppia_android_test( "//app/src/main/java/org/oppia/android/app/translation/testing:test_module", "//domain", "//domain/src/main/java/org/oppia/android/domain/classify:interactions_module", + "//domain/src/main/java/org/oppia/android/domain/classify/rules/algebraicexpressioninput:algebraic_expression_input_rule_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/continueinteraction:continue_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/dragAndDropSortInput:drag_and_drop_sort_input_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/fractioninput:fraction_input_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/imageClickInput:image_click_input_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/itemselectioninput:item_selection_input_module", + "//domain/src/main/java/org/oppia/android/domain/classify/rules/mathequationinput:math_equation_input_rule_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/multiplechoiceinput:multiple_choice_input_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/numberwithunits:number_with_units_rule_module", + "//domain/src/main/java/org/oppia/android/domain/classify/rules/numericexpressioninput:numeric_expression_input_rule_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/numericinput:numeric_input_rule_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/ratioinput:ratio_input_module", "//domain/src/main/java/org/oppia/android/domain/classify/rules/textinput:text_input_rule_module", From 0e9402a15ea1ba88d036098caf142f7ec8ea08ae Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 18 Feb 2022 15:27:13 -0800 Subject: [PATCH 146/162] Correct reference document link. --- .../app/utility/math/MathExpressionAccessibilityUtil.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/src/main/java/org/oppia/android/app/utility/math/MathExpressionAccessibilityUtil.kt b/app/src/main/java/org/oppia/android/app/utility/math/MathExpressionAccessibilityUtil.kt index 6995cf901b1..3f6c1249efa 100644 --- a/app/src/main/java/org/oppia/android/app/utility/math/MathExpressionAccessibilityUtil.kt +++ b/app/src/main/java/org/oppia/android/app/utility/math/MathExpressionAccessibilityUtil.kt @@ -106,8 +106,7 @@ class MathExpressionAccessibilityUtil @Inject constructor( } private fun MathExpression.toHumanReadableEnglishString(divAsFraction: Boolean): String? { - // Reference: - // https://docs.google.com/document/d/1P-dldXQ08O-02ZRG978paiWOSz0dsvcKpDgiV_rKH_Y/view. + // Ref: https://docs.google.com/document/d/1SkzAD4k7SWLp5_3L5WNxsnR79ATlOk8pz4irfE2ls-4/view. // Note that extra bidi wrapping is occurring here since there's not an obvious way to wrap "at // the end" for non-equations. From 3b7c424b6b28bbf453596f49e502f57c02ada570 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 23 Feb 2022 14:38:10 -0800 Subject: [PATCH 147/162] Ensure LaTeX isn't stretched or cut-off. The comments in-code have much more specifics on the approach. --- WORKSPACE | 3 +- domain/src/main/assets/GJ2rLXRKD5hw_1.json | 2 +- .../src/main/assets/GJ2rLXRKD5hw_1.textproto | 2 +- utility/build.gradle | 2 +- .../util/parser/image/UrlImageParser.kt | 89 ++++--- .../util/parser/math/MathBitmapModelLoader.kt | 242 +++++++++++++++++- 6 files changed, 286 insertions(+), 54 deletions(-) diff --git a/WORKSPACE b/WORKSPACE index e7c86e31247..19d38342978 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -133,8 +133,9 @@ git_repository( # min target SDK version to be compatible with Oppia. git_repository( name = "kotlitex", - commit = "ba1d5febba7642589f5441bb4affbb619156dbad", + commit = "ba51aaca442d248b7b44b4eeb5bb9846ad45b245", remote = "https://github.com/oppia/kotlitex", + shallow_since = "1645642252 -0800", ) bind( diff --git a/domain/src/main/assets/GJ2rLXRKD5hw_1.json b/domain/src/main/assets/GJ2rLXRKD5hw_1.json index 3b818d497fd..56a8e13bcde 100644 --- a/domain/src/main/assets/GJ2rLXRKD5hw_1.json +++ b/domain/src/main/assets/GJ2rLXRKD5hw_1.json @@ -4,7 +4,7 @@ "page_contents": { "subtitled_html": { "content_id": "content", - "html": "

Description of subtopic is here.

.

This is sample subtopic with dummy content related to Fractions: A lot more text that hopefully wraps to the next line in order to see whether the fraction correctly breaks subsequent text lines since we definitely don't want it to overlap since then it would most certainly not be readable in the least.

Fractions represent equal parts of a whole or a collection. Fraction of a whole: When we divide a whole into equal parts, each part is a fraction of the whole. A fraction has two parts. The number on the top of the line is called the numerator. It tells how many equal parts of the whole or collection are taken. The number below the line is called the denominator. It shows the total divisible number of equal parts the whole into or the total number of equal parts which are there in a collection.

" + "html": "

Description of subtopic is here.

.

This is sample subtopic with dummy content related to Fractions: A lot more text that hopefully wraps to the next line in order to see whether the fraction correctly breaks subsequent text lines since we definitely don't want it to overlap since then it would most certainly not be readable in the least.

Fractions represent equal parts of a whole or a collection. Fraction of a whole: When we divide a whole into equal parts, each part is a fraction of the whole. A fraction has two parts. The number on the top of the line is called the numerator. It tells how many equal parts of the whole or collection are taken. The number below the line is called the denominator. It shows the total divisible number of equal parts the whole into or the total number of equal parts which are there in a collection. Also, here's the equation for a line using in-line LaTeX:

" }, "recorded_voiceovers": { "voiceovers_mapping": { diff --git a/domain/src/main/assets/GJ2rLXRKD5hw_1.textproto b/domain/src/main/assets/GJ2rLXRKD5hw_1.textproto index 92a7e98d761..80adadf336e 100644 --- a/domain/src/main/assets/GJ2rLXRKD5hw_1.textproto +++ b/domain/src/main/assets/GJ2rLXRKD5hw_1.textproto @@ -1,6 +1,6 @@ subtopic_title: "What is a Fraction?" page_contents { - html: "

Description of subtopic is here.

.

This is sample subtopic with dummy content related to Fractions: A lot more text that hopefully wraps to the next line in order to see whether the fraction correctly breaks subsequent text lines since we definitely don't want it to overlap since then it would most certainly not be readable in the least.

Fractions represent equal parts of a whole or a collection. Fraction of a whole: When we divide a whole into equal parts, each part is a fraction of the whole. A fraction has two parts. The number on the top of the line is called the numerator. It tells how many equal parts of the whole or collection are taken. The number below the line is called the denominator. It shows the total divisible number of equal parts the whole into or the total number of equal parts which are there in a collection.

" + html: "

Description of subtopic is here.

.

This is sample subtopic with dummy content related to Fractions: A lot more text that hopefully wraps to the next line in order to see whether the fraction correctly breaks subsequent text lines since we definitely don't want it to overlap since then it would most certainly not be readable in the least.

Fractions represent equal parts of a whole or a collection. Fraction of a whole: When we divide a whole into equal parts, each part is a fraction of the whole. A fraction has two parts. The number on the top of the line is called the numerator. It tells how many equal parts of the whole or collection are taken. The number below the line is called the denominator. It shows the total divisible number of equal parts the whole into or the total number of equal parts which are there in a collection. Also, here's the equation for a line using in-line LaTeX:

" content_id: "content" } recorded_voiceover { diff --git a/utility/build.gradle b/utility/build.gradle index e8b517da751..e90341009c9 100644 --- a/utility/build.gradle +++ b/utility/build.gradle @@ -65,7 +65,7 @@ dependencies { 'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0-alpha03', 'androidx.work:work-runtime-ktx:2.4.0', 'com.github.oppia:androidsvg:6bd15f69caee3e6857fcfcd123023716b4adec1d', - 'com.github.oppia:kotlitex:ba1d5febba7642589f5441bb4affbb619156dbad', + 'com.github.oppia:kotlitex:ba51aaca442d248b7b44b4eeb5bb9846ad45b245', 'com.github.bumptech.glide:glide:4.11.0', 'com.google.dagger:dagger:2.24', 'com.google.firebase:firebase-analytics-ktx:17.5.0', diff --git a/utility/src/main/java/org/oppia/android/util/parser/image/UrlImageParser.kt b/utility/src/main/java/org/oppia/android/util/parser/image/UrlImageParser.kt index a72a533170d..87960a83faa 100644 --- a/utility/src/main/java/org/oppia/android/util/parser/image/UrlImageParser.kt +++ b/utility/src/main/java/org/oppia/android/util/parser/image/UrlImageParser.kt @@ -106,7 +106,13 @@ class UrlImageParser private constructor( createCustomTarget(drawable) { when (type) { INLINE_TEXT_IMAGE -> AutoAdjustingImageTarget.InlineTextImage.createForMath(context, it) - BLOCK_IMAGE -> AutoAdjustingImageTarget.BlockImageTarget.BitmapTarget.create(it) + BLOCK_IMAGE -> { + // Render the LaTeX as a block image, but don't automatically resize it since it's + // text (which means resizing may make it unreadable). + AutoAdjustingImageTarget.BlockImageTarget.BitmapTarget.create( + it, autoResizeImage = false + ) + } } } ) @@ -167,7 +173,8 @@ class UrlImageParser private constructor( * display them in a "block" fashion. */ sealed class BlockImageTarget( - targetConfiguration: TargetConfiguration + targetConfiguration: TargetConfiguration, + private val autoResizeImage: Boolean ) : AutoAdjustingImageTarget(targetConfiguration) { override fun computeBounds(drawable: D, viewWidth: Int): Rect { @@ -185,40 +192,42 @@ class UrlImageParser private constructor( var drawableWidth = drawable.intrinsicWidth.toFloat() var drawableHeight = drawable.intrinsicHeight.toFloat() - val minimumImageSize = context.resources.getDimensionPixelSize(R.dimen.minimum_image_size) - if (drawableHeight <= minimumImageSize || drawableWidth <= minimumImageSize) { - // The multipleFactor value is used to make sure that the aspect ratio of the image - // remains the same. - // Example: Height is 90, width is 60 and minimumImageSize is 120. - // Then multipleFactor will be 2 (120/60). - // The new height will be 180 and new width will be 120. - val multipleFactor = if (drawableHeight <= drawableWidth) { - // If height is less then the width, multipleFactor value is determined by height. - minimumImageSize.toFloat() / drawableHeight - } else { - // If height is less then the width, multipleFactor value is determined by width. - minimumImageSize.toFloat() / drawableWidth + if (autoResizeImage) { + val minimumImageSize = context.resources.getDimensionPixelSize(R.dimen.minimum_image_size) + if (drawableHeight <= minimumImageSize || drawableWidth <= minimumImageSize) { + // The multipleFactor value is used to make sure that the aspect ratio of the image + // remains the same. + // Example: Height is 90, width is 60 and minimumImageSize is 120. + // Then multipleFactor will be 2 (120/60). + // The new height will be 180 and new width will be 120. + val multipleFactor = if (drawableHeight <= drawableWidth) { + // If height is less then the width, multipleFactor value is determined by height. + minimumImageSize.toFloat() / drawableHeight + } else { + // If height is less then the width, multipleFactor value is determined by width. + minimumImageSize.toFloat() / drawableWidth + } + drawableHeight *= multipleFactor + drawableWidth *= multipleFactor } - drawableHeight *= multipleFactor - drawableWidth *= multipleFactor - } - val maxContentItemPadding = - context.resources.getDimensionPixelSize(R.dimen.maximum_content_item_padding) - val maximumImageSize = maxAvailableWidth - maxContentItemPadding - if (drawableWidth >= maximumImageSize) { - // The multipleFactor value is used to make sure that the aspect ratio of the image - // remains the same. Example: Height is 420, width is 440 and maximumImageSize is 200. - // Then multipleFactor will be (200/440). The new height will be 191 and new width will - // be 200. - val multipleFactor = if (drawableHeight >= drawableWidth) { - // If height is greater then the width, multipleFactor value is determined by height. - (maximumImageSize.toFloat() / drawableHeight) - } else { - // If height is greater then the width, multipleFactor value is determined by width. - (maximumImageSize.toFloat() / drawableWidth) + val maxContentItemPadding = + context.resources.getDimensionPixelSize(R.dimen.maximum_content_item_padding) + val maximumImageSize = maxAvailableWidth - maxContentItemPadding + if (drawableWidth >= maximumImageSize) { + // The multipleFactor value is used to make sure that the aspect ratio of the image + // remains the same. Example: Height is 420, width is 440 and maximumImageSize is 200. + // Then multipleFactor will be (200/440). The new height will be 191 and new width will + // be 200. + val multipleFactor = if (drawableHeight >= drawableWidth) { + // If height is greater then the width, multipleFactor value is determined by height. + (maximumImageSize.toFloat() / drawableHeight) + } else { + // If height is greater then the width, multipleFactor value is determined by width. + (maximumImageSize.toFloat() / drawableWidth) + } + drawableHeight *= multipleFactor + drawableWidth *= multipleFactor } - drawableHeight *= multipleFactor - drawableWidth *= multipleFactor } val drawableLeft = if (imageCenterAlign) { calculateInitialMargin(maxAvailableWidth, drawableWidth) @@ -236,7 +245,9 @@ class UrlImageParser private constructor( /** A [BlockImageTarget] used to load & arrange SVGs. */ internal class SvgTarget( targetConfiguration: TargetConfiguration - ) : BlockImageTarget(targetConfiguration) { + ) : BlockImageTarget( + targetConfiguration, autoResizeImage = true + ) { override fun retrieveDrawable(resource: BlockPictureDrawable): BlockPictureDrawable = resource @@ -248,15 +259,17 @@ class UrlImageParser private constructor( /** A [BlockImageTarget] used to load & arrange bitmaps. */ internal class BitmapTarget( - targetConfiguration: TargetConfiguration - ) : BlockImageTarget(targetConfiguration) { + targetConfiguration: TargetConfiguration, + autoResizeImage: Boolean + ) : BlockImageTarget(targetConfiguration, autoResizeImage) { override fun retrieveDrawable(resource: Bitmap): BitmapDrawable { return BitmapDrawable(context.resources, resource) } companion object { /** Returns a new [BitmapTarget] for the specified configuration. */ - fun create(targetConfiguration: TargetConfiguration) = BitmapTarget(targetConfiguration) + fun create(targetConfiguration: TargetConfiguration, autoResizeImage: Boolean = true) = + BitmapTarget(targetConfiguration, autoResizeImage) } } } diff --git a/utility/src/main/java/org/oppia/android/util/parser/math/MathBitmapModelLoader.kt b/utility/src/main/java/org/oppia/android/util/parser/math/MathBitmapModelLoader.kt index 7dc3212152a..e53cd82e71b 100644 --- a/utility/src/main/java/org/oppia/android/util/parser/math/MathBitmapModelLoader.kt +++ b/utility/src/main/java/org/oppia/android/util/parser/math/MathBitmapModelLoader.kt @@ -2,7 +2,11 @@ package org.oppia.android.util.parser.math import android.content.Context import android.graphics.Bitmap +import android.graphics.Bitmap.Config.ARGB_8888 import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.Path +import android.graphics.RectF import android.text.Layout import android.text.Spannable import android.text.SpannableStringBuilder @@ -16,6 +20,7 @@ import com.bumptech.glide.load.model.ModelLoader import com.bumptech.glide.load.model.ModelLoaderFactory import com.bumptech.glide.load.model.MultiModelLoaderFactory import com.bumptech.glide.request.target.Target +import io.github.karino2.kotlitex.view.DrawableSurface import io.github.karino2.kotlitex.view.MathExpressionSpan import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope @@ -26,6 +31,9 @@ import org.oppia.android.util.logging.ConsoleLoggerInjectorProvider import org.oppia.android.util.threading.DispatcherInjectorProvider import java.io.ByteArrayOutputStream import java.nio.ByteBuffer +import kotlin.math.max +import kotlin.math.min +import kotlin.math.roundToInt /** * [ModelLoader] for rendering and caching bitmap representations of LaTeX represented by @@ -111,11 +119,13 @@ class MathBitmapModelLoader private constructor( // Use Android's StaticLayout to ensure the text is rendered correctly. Note that the // constants are derived from TextView's defaults (except width which is defaulted to 0 // since the width isn't necessarily known ahead of time). + // Any TextPaint can be used since the span will use its own. + val textPaint = TextPaint() @Suppress("DEPRECATION") // This call is necessary for the supported min API version. val staticTextLayout = StaticLayout( renderableText, - TextPaint(), // Any TextPaint can be used since the span will use its own. + textPaint, /* width= */ 0, Layout.Alignment.ALIGN_NORMAL, /* spacingmult= */ 1f, @@ -123,21 +133,29 @@ class MathBitmapModelLoader private constructor( /* includepad= */ true ) - // Reference for how Android manages different parts of text during rendering: - // https://stackoverflow.com/a/27631737/3689782. Note that the specifics of how text - // properties are used to compute these bounds are in the modified KotliTeX implementation - // (see the getter implementation for the property below). - val bounds = span.drawableBounds + // Estimate the surface necessary for rendering the LaTeX, then compute a tightly-packed + // bitmap containing rendered pixels. See drawText in BoundsCalculatingSurface and + // renderAutoSizingBitmap for more details. + val surface = BoundsCalculatingSurface() + val totalBounds = surface.also { + // The x and y are mostly unused by the draw routine. + span.draw(it, renderableText, x = 0f, y = 0, textPaint) + }.computeTotalBounds() + val boundsWidth = totalBounds.width().roundToInt() + val boundsHeight = totalBounds.height().roundToInt() val canvasBitmap = - Bitmap.createBitmap(bounds.width(), bounds.height(), Bitmap.Config.ARGB_8888) - val bitmapCanvas = Canvas(canvasBitmap) - staticTextLayout.draw(bitmapCanvas) + renderToAutoSizingBitmap(estimatedWidth = boundsWidth, estimatedHeight = boundsHeight) { + staticTextLayout.draw(it) + } - val finalWidth = if (targetWidth == Target.SIZE_ORIGINAL) bounds.width() else targetWidth + val finalWidth = + if (targetWidth == Target.SIZE_ORIGINAL) canvasBitmap.width else targetWidth val finalHeight = - if (targetHeight == Target.SIZE_ORIGINAL) bounds.height() else targetHeight + if (targetHeight == Target.SIZE_ORIGINAL) canvasBitmap.height else targetHeight - // Compute the final bitmap (which might need to be scaled depending on options). + // Compute the final bitmap (which might need to be scaled depending on options). Note that + // any actual scaling here is likely to distort the image since it can be automatically + // cropped to minimize excess whitespace during rendering. val bitmap = if (canvasBitmap.width != finalWidth || canvasBitmap.height != finalHeight) { // Glide is requesting the image in a different size, so adjust it. Bitmap.createScaledBitmap(canvasBitmap, finalWidth, finalHeight, /* filter= */ true) @@ -163,6 +181,206 @@ class MathBitmapModelLoader private constructor( // 'Retrieval' is expensive in this case since a rendering operation is needed. override fun getDataSource(): DataSource = DataSource.REMOTE + + /** + * A [DrawableSurface] which tracks the bounds necessary to draw each constituent part of LaTeX + * (rendered by KotliTeX) in order to estimate the bounds necessary to render specific LaTeX. + */ + private class BoundsCalculatingSurface : DrawableSurface { + private val initialClipRect = + RectF(-Float.MAX_VALUE, -Float.MAX_VALUE, Float.MAX_VALUE, Float.MAX_VALUE) + private var currentClip = initialClipRect + private val pastClips = mutableListOf() + private val currentBounds = RectF() + + /** + * Returns the [RectF] encompassing the estimated space required to render the entirety of all + * previous render operations called to this surface. + * + * Note that the returned [RectF] is a copy so changes to it will not change this class's + * internal state. Note also that the returned [RectF] is always starting at (0, 0) so its + * right and bottom values represent the space's width and height, respectively. + */ + fun computeTotalBounds(): RectF = RectF(currentBounds).apply { offsetTo(0f, 0f) } + + override fun clipRect(rect: RectF) { + currentClip = currentClip.intersection(rect) + } + + override fun drawLine(x0: Float, y0: Float, x1: Float, y1: Float, paint: Paint) { + currentBounds.ensureIncludes(x0, y0) + currentBounds.ensureIncludes(x1, y1) + } + + override fun drawPath(path: Path, paint: Paint) { + val pathBounds = RectF().also { path.computeBounds(it, /* unusedExact= */ true) } + currentBounds.union(pathBounds.intersection(currentClip)) + } + + override fun drawRect(rect: RectF, paint: Paint) { + currentBounds.union(rect.intersection(currentClip)) + } + + override fun drawText(text: String, x: Float, y: Float, paint: Paint) { + /* + * Text is particularly difficult to track size for since it's not obvious to actually get + * the dimensions and position of the space that the actual rendered pixels will occupy. + * https://stackoverflow.com/a/27631737/3689782 provides context both on how text is laid + * out, and provides examples of glyphs that can exceed the expected size of a line. + * + * This problem is exacerbated by KotliTeX manually positioning glyphs both horizontally and + * vertically rather than relying on built-in font kerning, tracking, and other rules (for + * a high-level reference on these, see: https://proandroiddev.com/5f06722dd611). + * + * One way to measure text is by using the Paint object (see + * https://stackoverflow.com/a/18260682/3689782), but this doesn't account for the extra + * vertical or horizontal space needed for a specific glyph. + * + * The chosen solution is to approximate vertical alignment by appending a tall character + * (such as a parenthesis) on a line below the glyph, then to compute the bounds of the + * first line and treat this as the size of the glyph. The use of StaticLayout came as a + * suggestion from https://stackoverflow.com/a/7643312/3689782 and + * https://stackoverflow.com/a/42091739/3689782. While this still is generally an + * under-approximation, it's close to the necessary space and pairs well with rendering to a + * larger canvas that can be cropped down. + */ + @Suppress("DEPRECATION") // This call is necessary for the supported min API version. + val staticLayout = + StaticLayout( + "$text\n(", + paint as TextPaint, + /* width= */ 0, + Layout.Alignment.ALIGN_NORMAL, + /* spacingmult= */ 1f, + /* spacingadd= */ 0f, + /* includepad= */ true + ) + val textBounds = staticLayout.getLineBounds().apply { offsetTo(x, y) } + currentBounds.union(textBounds.intersection(currentClip)) + } + + override fun restore() { + currentClip = pastClips.removeLast() + } + + override fun save() { + pastClips += currentClip + } + } + + private companion object { + /** + * Returns a new [Bitmap] with the contents produced by [render]. + * + * This function is useful for cases when the exact dimension requirement of results from + * [render] may not be known, but a close approximation can be computed. + * + * The size of the bitmap is initialized based on heuristic initial width/heights (defined by + * [estimatedWidth] and [estimatedHeight]). Note that it's possible the rendered contents + * exceed the size of the bitmap in which case they will be cut off. Otherwise, the returned + * bitmap will be the smallest bitmap possible to hold the results [render] in a bitmap up to + * 2x the initial specified dimensions. + * + * This method requires creating 2 [Bitmap]s at once, so it may utilize quite a bit of memory. + */ + private fun renderToAutoSizingBitmap( + estimatedWidth: Int, + estimatedHeight: Int, + render: (Canvas) -> Unit + ): Bitmap { + val fullWidth = estimatedWidth * 2 + val fullHeight = estimatedHeight * 2 + val drawX = (fullWidth.toFloat() / 2) - (estimatedWidth.toFloat() / 2) + val drawY = (fullHeight.toFloat() / 2) - (estimatedHeight.toFloat() / 2) + val fullRender = Bitmap.createBitmap(fullWidth, fullHeight, ARGB_8888).also { bitmap -> + Canvas(bitmap).also { canvas -> + canvas.save() + // Move initial drawing such that there's a width/2 and height/2 boundary around the + // entire drawing space for rendering that may overflow. + canvas.translate(drawX, drawY) + render(canvas) + canvas.restore() + } + } + + // Initialize with the largest possible "empty" (inverted) rectangle so that *any* pixel + // will become the entire initial rectangular region. + val filledRegion = + RectF(Float.MAX_VALUE, Float.MAX_VALUE, -Float.MAX_VALUE, -Float.MAX_VALUE) + for (x in 0 until fullRender.width) { + for (y in 0 until fullRender.height) { + val pixel = fullRender.getPixel(x, y) + if ((pixel.toLong() and 0xff000000L) != 0L) { + // Any not-fully transparent pixels are considered "filled in" parts of the render. + filledRegion.ensureIncludes(x.toFloat(), y.toFloat()) + } + } + } + + return if (!filledRegion.isEmpty) { + // At least some pixels have been filled. + val neededWidth = filledRegion.width().roundToInt() + val neededHeight = filledRegion.height().roundToInt() + if (neededWidth != fullWidth || neededHeight != fullHeight) { + // Less space is needed than the original bitmap which means it can be cropped to save + // on space & memory. + Bitmap.createBitmap( + fullRender, + filledRegion.left.toInt(), + filledRegion.top.toInt(), + neededWidth, + neededHeight + ) + } else fullRender // Otherwise, just return the original (since the full space is needed). + } else { + // The entire render is empty so default to a 1x1 bitmap to conserve memory. + Bitmap.createBitmap(/* width= */ 1, /* height= */ 1, ARGB_8888) + } + } + + private fun RectF.getActualLeft(): Float = min(left, right) + private fun RectF.getActualRight(): Float = max(left, right) + private fun RectF.getActualTop(): Float = min(top, bottom) + private fun RectF.getActualBottom(): Float = max(top, bottom) + + private fun RectF.intersection(other: RectF): RectF { + // https://stackoverflow.com/a/19754915/3689782 provided a simple approach. + val intersectedLeft = max(getActualLeft(), other.getActualLeft()) + val intersectedTop = max(getActualTop(), other.getActualTop()) + val intersectedRight = min(getActualRight(), other.getActualRight()) + val intersectedBottom = min(getActualBottom(), other.getActualBottom()) + + // Make sure that rectangles which don't at least partially overlap result in a degenerate + // rectangle rather than a negative one (which would actually represent the union along + // whichever axis doesn't overlap). + val (actualLeft, actualRight) = if (intersectedRight < intersectedLeft) { + 0f to 0f + } else intersectedLeft to intersectedRight + val (actualTop, actualBottom) = if (intersectedBottom < intersectedTop) { + 0f to 0f + } else intersectedTop to intersectedBottom + return RectF(actualLeft, actualTop, actualRight, actualBottom) + } + + private fun RectF.ensureIncludes(x: Float, y: Float) { + // Note the '+1' here is necessary since 'right' and 'bottom' are exclusive bounds in the + // rectangle class (in order for the 'width' and 'height' computations to operate + // correctly). + left = min(left, x) + right = max(right, x + 1) + top = min(top, y) + bottom = max(bottom, y + 1) + } + + private fun StaticLayout.getLineBounds(line: Int = 0): RectF { + return RectF( + getLineLeft(line), + getLineTop(line).toFloat(), + getLineRight(line), + getLineBottom(line).toFloat() + ) + } + } } /** [ModelLoaderFactory] for creating new [MathBitmapModelLoader]s. */ From 8691e43e2ed7e4d9a2aa473c69aa5b3fa8c02a42 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 23 Feb 2022 18:18:46 -0800 Subject: [PATCH 148/162] Add and fix missing test (was broken on Gradle). --- .../oppia/android/app/databinding/BUILD.bazel | 41 +++++++++++++++++++ .../TextViewBindingAdaptersTest.kt | 7 +++- 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/app/src/sharedTest/java/org/oppia/android/app/databinding/BUILD.bazel b/app/src/sharedTest/java/org/oppia/android/app/databinding/BUILD.bazel index 341b24b5d13..1279ddff613 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/databinding/BUILD.bazel +++ b/app/src/sharedTest/java/org/oppia/android/app/databinding/BUILD.bazel @@ -51,6 +51,17 @@ genrule( """, ) +genrule( + name = "update_TextViewBindingAdaptersTest", + srcs = ["TextViewBindingAdaptersTest.kt"], + outs = ["TextViewBindingAdaptersTest_updated.kt"], + cmd = """ + cat $(SRCS) | + sed 's/import org.oppia.android.R/import org.oppia.android.app.databinding.R/g' | + sed 's/import org.oppia.android.app.databinding.TextViewBindingAdapters./import org.oppia.android.app.databinding.TextViewBindingAdapters_updated./g' > $(OUTS) + """, +) + oppia_android_test( name = "DrawableBindingAdaptersTest", srcs = ["DrawableBindingAdaptersTest_updated.kt"], @@ -201,6 +212,36 @@ oppia_android_test( ], ) +oppia_android_test( + name = "TextViewBindingAdaptersTest", + srcs = ["TextViewBindingAdaptersTest_updated.kt"], + custom_package = "org.oppia.android.app.databinding", + test_class = "org.oppia.android.app.databinding.TextViewBindingAdaptersTest", + test_manifest = "//app:test_manifest", + deps = [ + ":dagger", + "//app", + "//app:test_deps", + "//app/src/main/java/org/oppia/android/app/activity:activity_intent_factories_shim", + "//app/src/main/java/org/oppia/android/app/translation/testing:test_module", + "//domain", + "//testing", + "//testing/src/main/java/org/oppia/android/testing/junit:initialize_default_locale_rule", + "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module", + "//testing/src/main/java/org/oppia/android/testing/threading:test_module", + "//testing/src/main/java/org/oppia/android/testing/time:test_module", + "//third_party:androidx_test_ext_junit", + "//third_party:androidx_test_ext_truth", + "//third_party:junit_junit", + "//third_party:org_robolectric_robolectric", + "//third_party:robolectric_android-all", + "//utility/src/main/java/org/oppia/android/util/accessibility:test_module", + "//utility/src/main/java/org/oppia/android/util/caching/testing:caching_test_module", + "//utility/src/main/java/org/oppia/android/util/data:data_providers", + "//utility/src/main/java/org/oppia/android/util/logging:prod_module", + ], +) + oppia_android_test( name = "ViewBindingAdaptersTest", srcs = ["ViewBindingAdaptersTest_updated.kt"], diff --git a/app/src/sharedTest/java/org/oppia/android/app/databinding/TextViewBindingAdaptersTest.kt b/app/src/sharedTest/java/org/oppia/android/app/databinding/TextViewBindingAdaptersTest.kt index 0586b7ef7ef..46fe147948b 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/databinding/TextViewBindingAdaptersTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/databinding/TextViewBindingAdaptersTest.kt @@ -36,13 +36,16 @@ import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule @@ -320,7 +323,9 @@ class TextViewBindingAdaptersTest { DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, - AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class ] ) interface TestApplicationComponent : ApplicationComponent { From 7cd00203d8aa4bdec03b1de42b5767c8b3ce9eb4 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Sat, 5 Mar 2022 00:44:23 -0800 Subject: [PATCH 149/162] Refactor AsyncResult into a sealed class. This also introduces an AsyncResultSubject, and more or less fully fixes issue #3813 in all tests. This is a cherry-pick from the fix-progress-controller-deadlock branch since it ended up being quite large (it made more sense to split it into a pre-requisite PR). Conflicts: app/src/main/java/org/oppia/android/app/player/state/testing/StateFragmentTestActivityPresenter.kt domain/src/main/java/org/oppia/android/domain/exploration/ExplorationProgressController.kt domain/src/main/java/org/oppia/android/domain/question/QuestionAssessmentProgressController.kt domain/src/test/java/org/oppia/android/domain/exploration/lightweightcheckpointing/BUILD.bazel testing/src/main/java/org/oppia/android/testing/data/DataProviderTestMonitor.kt utility/src/main/java/org/oppia/android/util/data/DataProviders.kt --- .../AdministratorControlsViewModel.kt | 16 +- ...torControlsDownloadPermissionsViewModel.kt | 9 +- .../CompletedStoryListViewModel.kt | 18 +- .../MarkChaptersCompletedViewModel.kt | 16 +- .../MarkStoriesCompletedViewModel.kt | 16 +- .../MarkTopicsCompletedViewModel.kt | 16 +- .../NavigationDrawerFragmentPresenter.kt | 52 +- .../oppia/android/app/home/HomeViewModel.kt | 19 +- .../RecentlyPlayedFragmentPresenter.kt | 28 +- .../OngoingTopicListViewModel.kt | 18 +- .../app/options/OptionControlsViewModel.kt | 10 +- .../app/options/OptionsFragmentPresenter.kt | 182 ++-- .../player/audio/AudioFragmentPresenter.kt | 18 +- .../app/player/audio/AudioViewModel.kt | 16 +- .../ExplorationActivityPresenter.kt | 50 +- .../ExplorationManagerFragmentPresenter.kt | 18 +- ...tionExplorationManagerFragmentPresenter.kt | 35 +- .../player/state/StateFragmentPresenter.kt | 40 +- .../StateFragmentTestActivityPresenter.kt | 28 +- .../profile/AddProfileActivityPresenter.kt | 40 +- .../app/profile/AdminPinActivityPresenter.kt | 5 +- .../profile/PinPasswordActivityPresenter.kt | 5 +- .../app/profile/PinPasswordViewModel.kt | 14 +- .../ProfileChooserFragmentPresenter.kt | 20 +- .../app/profile/ProfileChooserViewModel.kt | 18 +- .../ResetPinDialogFragmentPresenter.kt | 5 +- .../ProfilePictureActivityPresenter.kt | 14 +- .../ProfileProgressViewModel.kt | 70 +- .../ResumeLessonFragmentPresenter.kt | 31 +- .../profile/ProfileEditFragmentPresenter.kt | 9 +- .../settings/profile/ProfileEditViewModel.kt | 18 +- .../settings/profile/ProfileListViewModel.kt | 16 +- .../profile/ProfileRenameFragmentPresenter.kt | 8 +- .../ProfileResetPinFragmentPresenter.kt | 5 +- .../app/splash/SplashActivityPresenter.kt | 17 +- .../app/story/StoryFragmentPresenter.kt | 13 +- .../oppia/android/app/story/StoryViewModel.kt | 15 +- .../StoryChapterSummaryViewModel.kt | 6 +- .../ExplorationTestActivityPresenter.kt | 16 +- .../testing/SplashTestActivityPresenter.kt | 2 +- .../oppia/android/app/topic/TopicViewModel.kt | 15 +- .../topic/conceptcard/ConceptCardViewModel.kt | 16 +- .../topic/info/TopicInfoFragmentPresenter.kt | 10 +- .../app/topic/lessons/TopicLessonViewModel.kt | 14 +- .../lessons/TopicLessonsFragmentPresenter.kt | 19 +- .../topic/practice/TopicPracticeViewModel.kt | 14 +- ...olutionQuestionManagerFragmentPresenter.kt | 37 +- .../QuestionPlayerActivityPresenter.kt | 37 +- .../QuestionPlayerFragmentPresenter.kt | 45 +- .../topic/revision/TopicRevisionViewModel.kt | 14 +- .../RevisionCardActivityPresenter.kt | 18 +- .../revisioncard/RevisionCardViewModel.kt | 16 +- .../translation/AppLanguageWatcherMixin.kt | 50 +- .../end/WalkthroughFinalFragmentPresenter.kt | 14 +- .../topiclist/WalkthroughTopicViewModel.kt | 18 +- .../WalkthroughWelcomeFragmentPresenter.kt | 16 +- .../app/profile/ProfileChooserFragmentTest.kt | 5 +- .../profile/ProfileEditActivityTest.kt | 13 +- data/BUILD.bazel | 2 + .../data/persistence/PersistentCacheStore.kt | 6 +- .../persistence/PersistentCacheStoreTest.kt | 435 +++------ domain/BUILD.bazel | 1 + .../domain/audio/AudioPlayerController.kt | 17 +- .../ModifyLessonProgressController.kt | 6 +- .../exploration/ExplorationDataController.kt | 6 +- .../ExplorationProgressController.kt | 32 +- .../ExplorationCheckpointController.kt | 43 +- .../android/domain/locale/LocaleController.kt | 14 +- .../PlatformParameterController.kt | 2 +- .../syncup/PlatformParameterSyncUpWorker.kt | 5 +- .../profile/ProfileManagementController.kt | 74 +- .../QuestionAssessmentProgressController.kt | 28 +- .../question/QuestionTrainingController.kt | 4 +- .../domain/topic/StoryProgressController.kt | 12 +- .../android/domain/topic/TopicController.kt | 19 +- .../domain/topic/TopicListController.kt | 4 +- .../translation/TranslationController.kt | 8 +- .../domain/audio/AudioPlayerControllerTest.kt | 67 +- .../CellularAudioDialogControllerTest.kt | 103 +- .../ModifyLessonProgressControllerTest.kt | 155 ++- .../lightweightcheckpointing/BUILD.bazel | 57 ++ .../ExplorationCheckpointControllerTest.kt | 338 +++---- .../AppStartupStateControllerTest.kt | 171 +--- .../analytics/AnalyticsControllerTest.kt | 140 +-- .../exceptions/ExceptionsControllerTest.kt | 164 +--- ...aughtExceptionLoggerStartupListenerTest.kt | 64 +- .../PlatformParameterControllerTest.kt | 100 +- .../PlatformParameterSyncUpWorkerTest.kt | 55 +- .../ProfileManagementControllerTest.kt | 894 +++++------------- .../topic/StoryProgressControllerTest.kt | 424 ++++----- .../domain/topic/TopicControllerTest.kt | 574 +++-------- .../domain/topic/TopicListControllerTest.kt | 122 +-- testing/BUILD.bazel | 3 + .../testing/data/AsyncResultSubject.kt | 104 ++ .../oppia/android/testing/data/BUILD.bazel | 15 + .../testing/data/DataProviderTestMonitor.kt | 35 +- .../ExplorationCheckpointTestHelper.kt | 14 +- .../testing/story/StoryProgressTestHelper.kt | 2 +- .../threading/TestCoroutineDispatcher.kt | 3 +- .../oppia/android/testing/data/BUILD.bazel | 1 + .../data/DataProviderTestMonitorTest.kt | 123 ++- .../ExplorationCheckpointTestHelperTest.kt | 174 ++-- .../testing/profile/ProfileTestHelperTest.kt | 73 +- .../story/StoryProgressTestHelperTest.kt | 98 +- .../threading/CoroutineExecutorServiceTest.kt | 16 +- .../oppia/android/util/data/AsyncResult.kt | 144 +-- .../oppia/android/util/data/DataProviders.kt | 11 +- .../android/util/data/AsyncResultTest.kt | 595 +++++------- .../org/oppia/android/util/data/BUILD.bazel | 86 ++ .../android/util/data/DataProvidersTest.kt | 505 ++++------ .../util/data/InMemoryBlockingCacheTest.kt | 2 + 111 files changed, 2961 insertions(+), 4512 deletions(-) create mode 100644 domain/src/test/java/org/oppia/android/domain/exploration/lightweightcheckpointing/BUILD.bazel create mode 100644 testing/src/main/java/org/oppia/android/testing/data/AsyncResultSubject.kt create mode 100644 utility/src/test/java/org/oppia/android/util/data/BUILD.bazel diff --git a/app/src/main/java/org/oppia/android/app/administratorcontrols/AdministratorControlsViewModel.kt b/app/src/main/java/org/oppia/android/app/administratorcontrols/AdministratorControlsViewModel.kt index 5bd386403e0..003c40369b5 100644 --- a/app/src/main/java/org/oppia/android/app/administratorcontrols/AdministratorControlsViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/administratorcontrols/AdministratorControlsViewModel.kt @@ -51,14 +51,16 @@ class AdministratorControlsViewModel @Inject constructor( private fun processGetDeviceSettingsResult( deviceSettingsResult: AsyncResult ): DeviceSettings { - if (deviceSettingsResult.isFailure()) { - oppiaLogger.e( - "AdministratorControlsFragment", - "Failed to retrieve profile", - deviceSettingsResult.getErrorOrNull()!! - ) + return when (deviceSettingsResult) { + is AsyncResult.Failure -> { + oppiaLogger.e( + "AdministratorControlsFragment", "Failed to retrieve profile", deviceSettingsResult.error + ) + DeviceSettings.getDefaultInstance() + } + is AsyncResult.Pending -> DeviceSettings.getDefaultInstance() + is AsyncResult.Success -> deviceSettingsResult.value } - return deviceSettingsResult.getOrDefault(DeviceSettings.getDefaultInstance()) } private fun processAdministratorControlsList( diff --git a/app/src/main/java/org/oppia/android/app/administratorcontrols/administratorcontrolsitemviewmodel/AdministratorControlsDownloadPermissionsViewModel.kt b/app/src/main/java/org/oppia/android/app/administratorcontrols/administratorcontrolsitemviewmodel/AdministratorControlsDownloadPermissionsViewModel.kt index 8dded74ea84..861862fe597 100644 --- a/app/src/main/java/org/oppia/android/app/administratorcontrols/administratorcontrolsitemviewmodel/AdministratorControlsDownloadPermissionsViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/administratorcontrols/administratorcontrolsitemviewmodel/AdministratorControlsDownloadPermissionsViewModel.kt @@ -7,6 +7,7 @@ import org.oppia.android.app.model.DeviceSettings import org.oppia.android.app.model.ProfileId import org.oppia.android.domain.oppialogger.OppiaLogger import org.oppia.android.domain.profile.ProfileManagementController +import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.data.DataProviders.Companion.toLiveData /** [ViewModel] for the recycler view in [AdministratorControlsFragment]. */ @@ -31,11 +32,11 @@ class AdministratorControlsDownloadPermissionsViewModel( .observe( fragment, Observer { - if (it.isFailure()) { + if (it is AsyncResult.Failure) { oppiaLogger.e( "AdministratorControlsFragment", "Failed to update topic update on wifi permission", - it.getErrorOrNull()!! + it.error ) } } @@ -49,11 +50,11 @@ class AdministratorControlsDownloadPermissionsViewModel( ).toLiveData().observe( fragment, Observer { - if (it.isFailure()) { + if (it is AsyncResult.Failure) { oppiaLogger.e( "AdministratorControlsFragment", "Failed to update topic auto update permission", - it.getErrorOrNull()!! + it.error ) } } diff --git a/app/src/main/java/org/oppia/android/app/completedstorylist/CompletedStoryListViewModel.kt b/app/src/main/java/org/oppia/android/app/completedstorylist/CompletedStoryListViewModel.kt index 4db3b84d049..4086552343e 100644 --- a/app/src/main/java/org/oppia/android/app/completedstorylist/CompletedStoryListViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/completedstorylist/CompletedStoryListViewModel.kt @@ -48,14 +48,18 @@ class CompletedStoryListViewModel @Inject constructor( private fun processCompletedStoryListResult( completedStoryListResult: AsyncResult ): CompletedStoryList { - if (completedStoryListResult.isFailure()) { - oppiaLogger.e( - "CompletedStoryListFragment", - "Failed to retrieve CompletedStory list: ", - completedStoryListResult.getErrorOrNull()!! - ) + return when (completedStoryListResult) { + is AsyncResult.Failure -> { + oppiaLogger.e( + "CompletedStoryListFragment", + "Failed to retrieve CompletedStory list: ", + completedStoryListResult.error + ) + CompletedStoryList.getDefaultInstance() + } + is AsyncResult.Pending -> CompletedStoryList.getDefaultInstance() + is AsyncResult.Success -> completedStoryListResult.value } - return completedStoryListResult.getOrDefault(CompletedStoryList.getDefaultInstance()) } private fun processCompletedStoryList( diff --git a/app/src/main/java/org/oppia/android/app/devoptions/markchapterscompleted/MarkChaptersCompletedViewModel.kt b/app/src/main/java/org/oppia/android/app/devoptions/markchapterscompleted/MarkChaptersCompletedViewModel.kt index bcc09604a72..24d1b7fc4e7 100644 --- a/app/src/main/java/org/oppia/android/app/devoptions/markchapterscompleted/MarkChaptersCompletedViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/devoptions/markchapterscompleted/MarkChaptersCompletedViewModel.kt @@ -48,14 +48,16 @@ class MarkChaptersCompletedViewModel @Inject constructor( private fun processStoryMapResult( storyMap: AsyncResult>> ): Map> { - if (storyMap.isFailure()) { - oppiaLogger.e( - "MarkChaptersCompletedFragment", - "Failed to retrieve storyList", - storyMap.getErrorOrNull()!! - ) + return when (storyMap) { + is AsyncResult.Failure -> { + oppiaLogger.e( + "MarkChaptersCompletedFragment", "Failed to retrieve storyList", storyMap.error + ) + mapOf() + } + is AsyncResult.Pending -> mapOf() + is AsyncResult.Success -> storyMap.value } - return storyMap.getOrDefault(mapOf()) } private fun processStoryMap( diff --git a/app/src/main/java/org/oppia/android/app/devoptions/markstoriescompleted/MarkStoriesCompletedViewModel.kt b/app/src/main/java/org/oppia/android/app/devoptions/markstoriescompleted/MarkStoriesCompletedViewModel.kt index b105bbc5a31..8ec76b5d69e 100644 --- a/app/src/main/java/org/oppia/android/app/devoptions/markstoriescompleted/MarkStoriesCompletedViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/devoptions/markstoriescompleted/MarkStoriesCompletedViewModel.kt @@ -48,14 +48,16 @@ class MarkStoriesCompletedViewModel @Inject constructor( private fun processStoryMapResult( storyMap: AsyncResult>> ): Map> { - if (storyMap.isFailure()) { - oppiaLogger.e( - "MarkStoriesCompletedFragment", - "Failed to retrieve storyList", - storyMap.getErrorOrNull()!! - ) + return when (storyMap) { + is AsyncResult.Failure -> { + oppiaLogger.e( + "MarkStoriesCompletedFragment", "Failed to retrieve storyList", storyMap.error + ) + mapOf() + } + is AsyncResult.Pending -> mapOf() + is AsyncResult.Success -> storyMap.value } - return storyMap.getOrDefault(mapOf()) } private fun processStoryMap( diff --git a/app/src/main/java/org/oppia/android/app/devoptions/marktopicscompleted/MarkTopicsCompletedViewModel.kt b/app/src/main/java/org/oppia/android/app/devoptions/marktopicscompleted/MarkTopicsCompletedViewModel.kt index ca48064520c..fd2d06e094b 100644 --- a/app/src/main/java/org/oppia/android/app/devoptions/marktopicscompleted/MarkTopicsCompletedViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/devoptions/marktopicscompleted/MarkTopicsCompletedViewModel.kt @@ -45,14 +45,16 @@ class MarkTopicsCompletedViewModel @Inject constructor( } private fun processAllTopicsResult(allTopics: AsyncResult>): List { - if (allTopics.isFailure()) { - oppiaLogger.e( - "MarkTopicsCompletedFragment", - "Failed to retrieve all topics", - allTopics.getErrorOrNull()!! - ) + return when (allTopics) { + is AsyncResult.Failure -> { + oppiaLogger.e( + "MarkTopicsCompletedFragment", "Failed to retrieve all topics", allTopics.error + ) + mutableListOf() + } + is AsyncResult.Pending -> mutableListOf() + is AsyncResult.Success -> allTopics.value } - return allTopics.getOrDefault(mutableListOf()) } private fun processAllTopics(allTopics: List): List { diff --git a/app/src/main/java/org/oppia/android/app/drawer/NavigationDrawerFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/drawer/NavigationDrawerFragmentPresenter.kt index d367016774a..c8e6b46c7bd 100644 --- a/app/src/main/java/org/oppia/android/app/drawer/NavigationDrawerFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/drawer/NavigationDrawerFragmentPresenter.kt @@ -7,7 +7,6 @@ import android.view.ViewGroup import androidx.appcompat.app.ActionBarDrawerToggle import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.Toolbar -import androidx.core.view.forEach import androidx.drawerlayout.widget.DrawerLayout import androidx.fragment.app.Fragment import androidx.lifecycle.LiveData @@ -41,6 +40,7 @@ import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.data.DataProviders.Companion.toLiveData import org.oppia.android.util.statusbar.StatusBarColor import javax.inject.Inject +import androidx.core.view.forEach const val NAVIGATION_PROFILE_ID_ARGUMENT_KEY = "NavigationDrawerFragmentPresenter.navigation_profile_id" @@ -164,14 +164,14 @@ class NavigationDrawerFragmentPresenter @Inject constructor( } private fun processGetProfileResult(profileResult: AsyncResult): Profile { - if (profileResult.isFailure()) { - oppiaLogger.e( - "NavigationDrawerFragment", - "Failed to retrieve profile", - profileResult.getErrorOrNull()!! - ) + return when (profileResult) { + is AsyncResult.Failure -> { + oppiaLogger.e("NavigationDrawerFragment", "Failed to retrieve profile", profileResult.error) + Profile.getDefaultInstance() + } + is AsyncResult.Pending -> Profile.getDefaultInstance() + is AsyncResult.Success -> profileResult.value } - return profileResult.getOrDefault(Profile.getDefaultInstance()) } private fun getCompletedStoryListCount(): LiveData { @@ -193,14 +193,18 @@ class NavigationDrawerFragmentPresenter @Inject constructor( private fun processGetCompletedStoryListResult( completedStoryListResult: AsyncResult ): CompletedStoryList { - if (completedStoryListResult.isFailure()) { - oppiaLogger.e( - "NavigationDrawerFragment", - "Failed to retrieve completed story list", - completedStoryListResult.getErrorOrNull()!! - ) + return when (completedStoryListResult) { + is AsyncResult.Failure -> { + oppiaLogger.e( + "NavigationDrawerFragment", + "Failed to retrieve completed story list", + completedStoryListResult.error + ) + CompletedStoryList.getDefaultInstance() + } + is AsyncResult.Pending -> CompletedStoryList.getDefaultInstance() + is AsyncResult.Success -> completedStoryListResult.value } - return completedStoryListResult.getOrDefault(CompletedStoryList.getDefaultInstance()) } private fun getOngoingTopicListCount(): LiveData { @@ -222,14 +226,18 @@ class NavigationDrawerFragmentPresenter @Inject constructor( private fun processGetOngoingTopicListResult( ongoingTopicListResult: AsyncResult ): OngoingTopicList { - if (ongoingTopicListResult.isFailure()) { - oppiaLogger.e( - "NavigationDrawerFragment", - "Failed to retrieve ongoing topic list", - ongoingTopicListResult.getErrorOrNull()!! - ) + return when (ongoingTopicListResult) { + is AsyncResult.Failure -> { + oppiaLogger.e( + "NavigationDrawerFragment", + "Failed to retrieve ongoing topic list", + ongoingTopicListResult.error + ) + OngoingTopicList.getDefaultInstance() + } + is AsyncResult.Pending -> OngoingTopicList.getDefaultInstance() + is AsyncResult.Success -> ongoingTopicListResult.value } - return ongoingTopicListResult.getOrDefault(OngoingTopicList.getDefaultInstance()) } private fun openActivityByMenuItemId(menuItemId: Int) { diff --git a/app/src/main/java/org/oppia/android/app/home/HomeViewModel.kt b/app/src/main/java/org/oppia/android/app/home/HomeViewModel.kt index 24a7efb509f..16e3011628b 100644 --- a/app/src/main/java/org/oppia/android/app/home/HomeViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/home/HomeViewModel.kt @@ -25,6 +25,7 @@ import org.oppia.android.app.viewmodel.ObservableViewModel import org.oppia.android.domain.oppialogger.OppiaLogger import org.oppia.android.domain.profile.ProfileManagementController import org.oppia.android.domain.topic.TopicListController +import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.data.DataProvider import org.oppia.android.util.data.DataProviders.Companion.combineWith import org.oppia.android.util.data.DataProviders.Companion.toLiveData @@ -94,14 +95,18 @@ class HomeViewModel( */ val homeItemViewModelListLiveData: LiveData> by lazy { Transformations.map(homeItemViewModelListDataProvider.toLiveData()) { itemListResult -> - if (itemListResult.isFailure()) { - oppiaLogger.e( - "HomeFragment", - "No home fragment available -- failed to retrieve fragment data.", - itemListResult.getErrorOrNull() - ) + return@map when (itemListResult) { + is AsyncResult.Failure -> { + oppiaLogger.e( + "HomeFragment", + "No home fragment available -- failed to retrieve fragment data.", + itemListResult.error + ) + listOf() + } + is AsyncResult.Pending -> listOf() + is AsyncResult.Success -> itemListResult.value } - return@map itemListResult.getOrDefault(listOf()) } } diff --git a/app/src/main/java/org/oppia/android/app/home/recentlyplayed/RecentlyPlayedFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/home/recentlyplayed/RecentlyPlayedFragmentPresenter.kt index c86e6e050bc..33554540ec3 100755 --- a/app/src/main/java/org/oppia/android/app/home/recentlyplayed/RecentlyPlayedFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/home/recentlyplayed/RecentlyPlayedFragmentPresenter.kt @@ -171,11 +171,12 @@ class RecentlyPlayedFragmentPresenter @Inject constructor( } private fun getAssumedSuccessfulPromotedActivityList(): LiveData { - // If there's an error loading the data, assume the default. return Transformations.map(ongoingStoryListSummaryResultLiveData) { - it.getOrDefault( - PromotedActivityList.getDefaultInstance() - ) + when (it) { + // If there's an error loading the data, assume the default. + is AsyncResult.Failure, is AsyncResult.Pending -> PromotedActivityList.getDefaultInstance() + is AsyncResult.Success -> it.value + } } } @@ -252,7 +253,7 @@ class RecentlyPlayedFragmentPresenter @Inject constructor( fragment, object : Observer> { override fun onChanged(it: AsyncResult) { - if (it.isSuccess()) { + if (it is AsyncResult.Success) { explorationCheckpointLiveData.removeObserver(this) routeToResumeLessonListener.routeToResumeLesson( internalProfileId, @@ -260,9 +261,9 @@ class RecentlyPlayedFragmentPresenter @Inject constructor( promotedStory.storyId, promotedStory.explorationId, backflowScreen = null, - explorationCheckpoint = it.getOrThrow() + explorationCheckpoint = it.value ) - } else if (it.isFailure()) { + } else if (it is AsyncResult.Failure) { explorationCheckpointLiveData.removeObserver(this) playExploration( promotedStory.topicId, @@ -301,14 +302,11 @@ class RecentlyPlayedFragmentPresenter @Inject constructor( ).observe( fragment, Observer> { result -> - when { - result.isPending() -> oppiaLogger.d("RecentlyPlayedFragment", "Loading exploration") - result.isFailure() -> oppiaLogger.e( - "RecentlyPlayedFragment", - "Failed to load exploration", - result.getErrorOrNull()!! - ) - else -> { + when (result) { + is AsyncResult.Pending -> oppiaLogger.d("RecentlyPlayedFragment", "Loading exploration") + is AsyncResult.Failure -> + oppiaLogger.e("RecentlyPlayedFragment", "Failed to load exploration", result.error) + is AsyncResult.Success -> { oppiaLogger.d("RecentlyPlayedFragment", "Successfully loaded exploration") routeToExplorationListener.routeToExploration( internalProfileId, diff --git a/app/src/main/java/org/oppia/android/app/ongoingtopiclist/OngoingTopicListViewModel.kt b/app/src/main/java/org/oppia/android/app/ongoingtopiclist/OngoingTopicListViewModel.kt index bac0611a5b7..9d3e2fbd9fb 100644 --- a/app/src/main/java/org/oppia/android/app/ongoingtopiclist/OngoingTopicListViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/ongoingtopiclist/OngoingTopicListViewModel.kt @@ -50,14 +50,18 @@ class OngoingTopicListViewModel @Inject constructor( private fun processOngoingTopicResult( ongoingTopicListResult: AsyncResult ): OngoingTopicList { - if (ongoingTopicListResult.isFailure()) { - oppiaLogger.e( - "OngoingTopicListFragment", - "Failed to retrieve OngoingTopicList: ", - ongoingTopicListResult.getErrorOrNull()!! - ) + return when (ongoingTopicListResult) { + is AsyncResult.Failure -> { + oppiaLogger.e( + "OngoingTopicListFragment", + "Failed to retrieve OngoingTopicList: ", + ongoingTopicListResult.error + ) + OngoingTopicList.getDefaultInstance() + } + is AsyncResult.Pending -> OngoingTopicList.getDefaultInstance() + is AsyncResult.Success -> ongoingTopicListResult.value } - return ongoingTopicListResult.getOrDefault(OngoingTopicList.getDefaultInstance()) } private fun processOngoingTopicList( diff --git a/app/src/main/java/org/oppia/android/app/options/OptionControlsViewModel.kt b/app/src/main/java/org/oppia/android/app/options/OptionControlsViewModel.kt index 4c27f79469d..acb9712e054 100644 --- a/app/src/main/java/org/oppia/android/app/options/OptionControlsViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/options/OptionControlsViewModel.kt @@ -73,10 +73,14 @@ class OptionControlsViewModel @Inject constructor( } private fun processProfileResult(profile: AsyncResult): Profile { - if (profile.isFailure()) { - oppiaLogger.e("OptionsFragment", "Failed to retrieve profile", profile.getErrorOrNull()!!) + return when (profile) { + is AsyncResult.Failure -> { + oppiaLogger.e("OptionsFragment", "Failed to retrieve profile", profile.error) + Profile.getDefaultInstance() + } + is AsyncResult.Pending -> Profile.getDefaultInstance() + is AsyncResult.Success -> profile.value } - return profile.getOrDefault(Profile.getDefaultInstance()) } private fun processProfileList(profile: Profile): List { diff --git a/app/src/main/java/org/oppia/android/app/options/OptionsFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/options/OptionsFragmentPresenter.kt index e4b632d3d32..4e03543a077 100644 --- a/app/src/main/java/org/oppia/android/app/options/OptionsFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/options/OptionsFragmentPresenter.kt @@ -24,6 +24,7 @@ import org.oppia.android.domain.profile.ProfileManagementController import org.oppia.android.util.data.DataProviders.Companion.toLiveData import java.security.InvalidParameterException import javax.inject.Inject +import org.oppia.android.util.data.AsyncResult const val READING_TEXT_SIZE = "READING_TEXT_SIZE" const val APP_LANGUAGE = "APP_LANGUAGE" @@ -193,14 +194,14 @@ class OptionsFragmentPresenter @Inject constructor( ).toLiveData().observe( fragment, Observer { - if (it.isSuccess()) { - readingTextSize = ReadingTextSize.SMALL_TEXT_SIZE - } else { - oppiaLogger.e( - READING_TEXT_SIZE_TAG, - "$READING_TEXT_SIZE_ERROR: small text size", - it.getErrorOrNull() - ) + when (it) { + is AsyncResult.Success -> readingTextSize = ReadingTextSize.SMALL_TEXT_SIZE + is AsyncResult.Failure -> { + oppiaLogger.e( + READING_TEXT_SIZE_TAG, "$READING_TEXT_SIZE_ERROR: small text size", it.error + ) + } + is AsyncResult.Pending -> {} // Wait for a result. } } ) @@ -212,14 +213,14 @@ class OptionsFragmentPresenter @Inject constructor( ).toLiveData().observe( fragment, Observer { - if (it.isSuccess()) { - readingTextSize = ReadingTextSize.MEDIUM_TEXT_SIZE - } else { - oppiaLogger.e( - READING_TEXT_SIZE_TAG, - "$READING_TEXT_SIZE_ERROR: medium text size", - it.getErrorOrNull() - ) + when (it) { + is AsyncResult.Success -> readingTextSize = ReadingTextSize.MEDIUM_TEXT_SIZE + is AsyncResult.Failure -> { + oppiaLogger.e( + READING_TEXT_SIZE_TAG, "$READING_TEXT_SIZE_ERROR: medium text size", it.error + ) + } + is AsyncResult.Pending -> {} // Wait for a result. } } ) @@ -231,14 +232,14 @@ class OptionsFragmentPresenter @Inject constructor( ).toLiveData().observe( fragment, Observer { - if (it.isSuccess()) { - readingTextSize = ReadingTextSize.LARGE_TEXT_SIZE - } else { - oppiaLogger.e( - READING_TEXT_SIZE_TAG, - "$READING_TEXT_SIZE_ERROR: large text size", - it.getErrorOrNull() - ) + when (it) { + is AsyncResult.Success -> readingTextSize = ReadingTextSize.LARGE_TEXT_SIZE + is AsyncResult.Failure -> { + oppiaLogger.e( + READING_TEXT_SIZE_TAG, "$READING_TEXT_SIZE_ERROR: large text size", it.error + ) + } + is AsyncResult.Pending -> {} // Wait for a result. } } ) @@ -251,14 +252,14 @@ class OptionsFragmentPresenter @Inject constructor( ).toLiveData().observe( fragment, Observer { - if (it.isSuccess()) { - readingTextSize = ReadingTextSize.EXTRA_LARGE_TEXT_SIZE - } else { - oppiaLogger.e( - READING_TEXT_SIZE_TAG, - "$READING_TEXT_SIZE_ERROR: extra large text size", - it.getErrorOrNull() - ) + when (it) { + is AsyncResult.Success -> readingTextSize = ReadingTextSize.EXTRA_LARGE_TEXT_SIZE + is AsyncResult.Failure -> { + oppiaLogger.e( + READING_TEXT_SIZE_TAG, "$READING_TEXT_SIZE_ERROR: extra large text size", it.error + ) + } + is AsyncResult.Pending -> {} // Wait for a result. } } ) @@ -276,14 +277,11 @@ class OptionsFragmentPresenter @Inject constructor( ).toLiveData().observe( fragment, Observer { - if (it.isSuccess()) { - appLanguage = AppLanguage.ENGLISH_APP_LANGUAGE - } else { - oppiaLogger.e( - APP_LANGUAGE_TAG, - "$APP_LANGUAGE_ERROR: English", - it.getErrorOrNull() - ) + when (it) { + is AsyncResult.Success -> appLanguage = AppLanguage.ENGLISH_APP_LANGUAGE + is AsyncResult.Failure -> + oppiaLogger.e(APP_LANGUAGE_TAG, "$APP_LANGUAGE_ERROR: English", it.error) + is AsyncResult.Pending -> {} // Wait for a result. } } ) @@ -295,14 +293,11 @@ class OptionsFragmentPresenter @Inject constructor( ).toLiveData().observe( fragment, Observer { - if (it.isSuccess()) { - appLanguage = AppLanguage.HINDI_APP_LANGUAGE - } else { - oppiaLogger.e( - APP_LANGUAGE_TAG, - "$APP_LANGUAGE_ERROR: Hindi", - it.getErrorOrNull() - ) + when (it) { + is AsyncResult.Success -> appLanguage = AppLanguage.HINDI_APP_LANGUAGE + is AsyncResult.Failure -> + oppiaLogger.e(APP_LANGUAGE_TAG, "$APP_LANGUAGE_ERROR: Hindi", it.error) + is AsyncResult.Pending -> {} // Wait for a result. } } ) @@ -314,14 +309,11 @@ class OptionsFragmentPresenter @Inject constructor( ).toLiveData().observe( fragment, Observer { - if (it.isSuccess()) { - appLanguage = AppLanguage.CHINESE_APP_LANGUAGE - } else { - oppiaLogger.e( - APP_LANGUAGE_TAG, - "$APP_LANGUAGE_ERROR: Chinese", - it.getErrorOrNull() - ) + when (it) { + is AsyncResult.Success -> appLanguage = AppLanguage.CHINESE_APP_LANGUAGE + is AsyncResult.Failure -> + oppiaLogger.e(APP_LANGUAGE_TAG, "$APP_LANGUAGE_ERROR: Chinese", it.error) + is AsyncResult.Pending -> {} // Wait for a result. } } ) @@ -333,14 +325,11 @@ class OptionsFragmentPresenter @Inject constructor( ).toLiveData().observe( fragment, Observer { - if (it.isSuccess()) { - appLanguage = AppLanguage.FRENCH_APP_LANGUAGE - } else { - oppiaLogger.e( - APP_LANGUAGE_TAG, - "$APP_LANGUAGE_ERROR: French", - it.getErrorOrNull() - ) + when (it) { + is AsyncResult.Success -> appLanguage = AppLanguage.FRENCH_APP_LANGUAGE + is AsyncResult.Failure -> + oppiaLogger.e(APP_LANGUAGE_TAG, "$APP_LANGUAGE_ERROR: French", it.error) + is AsyncResult.Pending -> {} // Wait for a result. } } ) @@ -359,14 +348,11 @@ class OptionsFragmentPresenter @Inject constructor( ).toLiveData().observe( fragment, Observer { - if (it.isSuccess()) { - audioLanguage = AudioLanguage.NO_AUDIO - } else { - oppiaLogger.e( - AUDIO_LANGUAGE_TAG, - "$AUDIO_LANGUAGE_ERROR: No Audio", - it.getErrorOrNull() - ) + when (it) { + is AsyncResult.Success -> audioLanguage = AudioLanguage.NO_AUDIO + is AsyncResult.Failure -> + oppiaLogger.e(AUDIO_LANGUAGE_TAG, "$AUDIO_LANGUAGE_ERROR: No Audio", it.error) + is AsyncResult.Pending -> {} // Wait for a result. } } ) @@ -378,14 +364,11 @@ class OptionsFragmentPresenter @Inject constructor( ).toLiveData().observe( fragment, Observer { - if (it.isSuccess()) { - audioLanguage = AudioLanguage.ENGLISH_AUDIO_LANGUAGE - } else { - oppiaLogger.e( - AUDIO_LANGUAGE_TAG, - "$AUDIO_LANGUAGE_ERROR: English", - it.getErrorOrNull() - ) + when (it) { + is AsyncResult.Success -> audioLanguage = AudioLanguage.ENGLISH_AUDIO_LANGUAGE + is AsyncResult.Failure -> + oppiaLogger.e(AUDIO_LANGUAGE_TAG, "$AUDIO_LANGUAGE_ERROR: English", it.error) + is AsyncResult.Pending -> {} // Wait for a result. } } ) @@ -397,14 +380,11 @@ class OptionsFragmentPresenter @Inject constructor( ).toLiveData().observe( fragment, Observer { - if (it.isSuccess()) { - audioLanguage = AudioLanguage.HINDI_AUDIO_LANGUAGE - } else { - oppiaLogger.e( - AUDIO_LANGUAGE_TAG, - "$AUDIO_LANGUAGE_ERROR: Hindi", - it.getErrorOrNull() - ) + when (it) { + is AsyncResult.Success -> audioLanguage = AudioLanguage.HINDI_AUDIO_LANGUAGE + is AsyncResult.Failure -> + oppiaLogger.e(AUDIO_LANGUAGE_TAG, "$AUDIO_LANGUAGE_ERROR: Hindi", it.error) + is AsyncResult.Pending -> {} // Wait for a result. } } ) @@ -416,14 +396,11 @@ class OptionsFragmentPresenter @Inject constructor( ).toLiveData().observe( fragment, Observer { - if (it.isSuccess()) { - audioLanguage = AudioLanguage.CHINESE_AUDIO_LANGUAGE - } else { - oppiaLogger.e( - AUDIO_LANGUAGE_TAG, - "$AUDIO_LANGUAGE_ERROR: Chinese", - it.getErrorOrNull() - ) + when (it) { + is AsyncResult.Success -> audioLanguage = AudioLanguage.CHINESE_AUDIO_LANGUAGE + is AsyncResult.Failure -> + oppiaLogger.e(AUDIO_LANGUAGE_TAG, "$AUDIO_LANGUAGE_ERROR: Chinese", it.error) + is AsyncResult.Pending -> {} // Wait for a result. } } ) @@ -435,14 +412,11 @@ class OptionsFragmentPresenter @Inject constructor( ).toLiveData().observe( fragment, Observer { - if (it.isSuccess()) { - audioLanguage = AudioLanguage.FRENCH_AUDIO_LANGUAGE - } else { - oppiaLogger.e( - AUDIO_LANGUAGE_TAG, - "$AUDIO_LANGUAGE_ERROR: French", - it.getErrorOrNull() - ) + when (it) { + is AsyncResult.Success -> audioLanguage = AudioLanguage.FRENCH_AUDIO_LANGUAGE + is AsyncResult.Failure -> + oppiaLogger.e(AUDIO_LANGUAGE_TAG, "$AUDIO_LANGUAGE_ERROR: French", it.error) + is AsyncResult.Pending -> {} // Wait for a result. } } ) diff --git a/app/src/main/java/org/oppia/android/app/player/audio/AudioFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/player/audio/AudioFragmentPresenter.kt index d552408ca6e..14c5478b70f 100755 --- a/app/src/main/java/org/oppia/android/app/player/audio/AudioFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/player/audio/AudioFragmentPresenter.kt @@ -71,10 +71,9 @@ class AudioFragmentPresenter @Inject constructor( .observe( fragment, Observer> { - if (it.isSuccess()) { - val prefs = it.getOrDefault(CellularDataPreference.getDefaultInstance()) - showCellularDataDialog = !(prefs.hideDialog) - useCellularData = prefs.useCellularData + if (it is AsyncResult.Success) { + showCellularDataDialog = !it.value.hideDialog + useCellularData = it.value.useCellularData } } ) @@ -143,10 +142,15 @@ class AudioFragmentPresenter @Inject constructor( } private fun processGetProfileResult(profileResult: AsyncResult): String { - if (profileResult.isFailure()) { - oppiaLogger.e("AudioFragment", "Failed to retrieve profile", profileResult.getErrorOrNull()!!) + val profile = when (profileResult) { + is AsyncResult.Failure -> { + oppiaLogger.e("AudioFragment", "Failed to retrieve profile", profileResult.error) + Profile.getDefaultInstance() + } + is AsyncResult.Pending -> Profile.getDefaultInstance() + is AsyncResult.Success -> profileResult.value } - return getAudioLanguage(profileResult.getOrDefault(Profile.getDefaultInstance()).audioLanguage) + return getAudioLanguage(profile.audioLanguage) } /** Sets selected language code in presenter and ViewModel */ diff --git a/app/src/main/java/org/oppia/android/app/player/audio/AudioViewModel.kt b/app/src/main/java/org/oppia/android/app/player/audio/AudioViewModel.kt index d18048e74f1..5c1981790e5 100644 --- a/app/src/main/java/org/oppia/android/app/player/audio/AudioViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/player/audio/AudioViewModel.kt @@ -142,26 +142,26 @@ class AudioViewModel @Inject constructor( } private fun processDurationResultLiveData(playProgressResult: AsyncResult): Int { - if (!playProgressResult.isSuccess()) { + if (playProgressResult !is AsyncResult.Success) { return 0 } - return playProgressResult.getOrThrow().duration + return playProgressResult.value.duration } private fun processPositionResultLiveData(playProgressResult: AsyncResult): Int { - if (!playProgressResult.isSuccess()) { + if (playProgressResult !is AsyncResult.Success) { return 0 } - return playProgressResult.getOrThrow().position + return playProgressResult.value.position } private fun processPlayStatusResultLiveData( playProgressResult: AsyncResult ): UiAudioPlayStatus { - return when { - playProgressResult.isPending() -> UiAudioPlayStatus.LOADING - playProgressResult.isFailure() -> UiAudioPlayStatus.FAILED - else -> when (playProgressResult.getOrThrow().type) { + return when (playProgressResult) { + is AsyncResult.Pending -> UiAudioPlayStatus.LOADING + is AsyncResult.Failure -> UiAudioPlayStatus.FAILED + is AsyncResult.Success -> when (playProgressResult.value.type) { PlayStatus.PREPARED -> { if (autoPlay) audioPlayerController.play() autoPlay = false diff --git a/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationActivityPresenter.kt index aa1edfe9e04..e0cceec462f 100755 --- a/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationActivityPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationActivityPresenter.kt @@ -30,6 +30,7 @@ import org.oppia.android.domain.oppialogger.OppiaLogger import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.data.DataProviders.Companion.toLiveData import javax.inject.Inject +import org.oppia.android.app.model.ExplorationCheckpointDetails private const val TAG_UNSAVED_EXPLORATION_DIALOG = "UNSAVED_EXPLORATION_DIALOG" private const val TAG_STOP_EXPLORATION_DIALOG = "STOP_EXPLORATION_DIALOG" @@ -242,14 +243,11 @@ class ExplorationActivityPresenter @Inject constructor( .observe( activity, Observer> { - when { - it.isPending() -> oppiaLogger.d("ExplorationActivity", "Stopping exploration") - it.isFailure() -> oppiaLogger.e( - "ExplorationActivity", - "Failed to stop exploration", - it.getErrorOrNull()!! - ) - else -> { + when (it) { + is AsyncResult.Pending -> oppiaLogger.d("ExplorationActivity", "Stopping exploration") + is AsyncResult.Failure -> + oppiaLogger.e("ExplorationActivity", "Failed to stop exploration", it.error) + is AsyncResult.Success -> { oppiaLogger.d("ExplorationActivity", "Successfully stopped exploration") backPressActivitySelector(backflowScreen) (activity as ExplorationActivity).finish() @@ -320,14 +318,16 @@ class ExplorationActivityPresenter @Inject constructor( /** Helper for subscribeToExploration. */ private fun processExploration(ephemeralStateResult: AsyncResult): Exploration { - if (ephemeralStateResult.isFailure()) { - oppiaLogger.e( - "ExplorationActivity", - "Failed to retrieve answer outcome", - ephemeralStateResult.getErrorOrNull()!! - ) + return when (ephemeralStateResult) { + is AsyncResult.Failure -> { + oppiaLogger.e( + "ExplorationActivity", "Failed to retrieve answer outcome", ephemeralStateResult.error + ) + Exploration.getDefaultInstance() + } + is AsyncResult.Pending -> Exploration.getDefaultInstance() + is AsyncResult.Success -> ephemeralStateResult.value } - return ephemeralStateResult.getOrDefault(Exploration.getDefaultInstance()) } private fun backPressActivitySelector(backflowScreen: Int?) { @@ -420,15 +420,17 @@ class ExplorationActivityPresenter @Inject constructor( ).toLiveData().observe( activity, Observer { - if (it.isSuccess()) { - oldestCheckpointExplorationId = it.getOrThrow().explorationId - oldestCheckpointExplorationTitle = it.getOrThrow().explorationTitle - } else if (it.isFailure()) { - oppiaLogger.e( - "ExplorationActivity", - "Failed to retrieve oldest saved checkpoint details.", - it.getErrorOrNull() - ) + when (it) { + is AsyncResult.Success -> { + oldestCheckpointExplorationId = it.value.explorationId + oldestCheckpointExplorationTitle = it.value.explorationTitle + } + is AsyncResult.Failure -> { + oppiaLogger.e( + "ExplorationActivity", "Failed to retrieve oldest saved checkpoint details.", it.error + ) + } + is AsyncResult.Pending -> {} // Wait for an actual result. } } ) diff --git a/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationManagerFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationManagerFragmentPresenter.kt index 28dc4156aac..4df78475693 100644 --- a/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationManagerFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationManagerFragmentPresenter.kt @@ -45,13 +45,15 @@ class ExplorationManagerFragmentPresenter @Inject constructor( private fun processReadingTextSizeResult( readingTextSizeResult: AsyncResult ): ReadingTextSize { - if (readingTextSizeResult.isFailure()) { - oppiaLogger.e( - "ExplorationManagerFragment", - "Failed to retrieve profile", - readingTextSizeResult.getErrorOrNull()!! - ) - } - return readingTextSizeResult.getOrDefault(Profile.getDefaultInstance()).readingTextSize + return when (readingTextSizeResult) { + is AsyncResult.Failure -> { + oppiaLogger.e( + "ExplorationManagerFragment", "Failed to retrieve profile", readingTextSizeResult.error + ) + Profile.getDefaultInstance() + } + is AsyncResult.Pending -> Profile.getDefaultInstance() + is AsyncResult.Success -> readingTextSizeResult.value + }.readingTextSize } } diff --git a/app/src/main/java/org/oppia/android/app/player/exploration/HintsAndSolutionExplorationManagerFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/player/exploration/HintsAndSolutionExplorationManagerFragmentPresenter.kt index a8460e014ff..22f3f9f25e5 100644 --- a/app/src/main/java/org/oppia/android/app/player/exploration/HintsAndSolutionExplorationManagerFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/player/exploration/HintsAndSolutionExplorationManagerFragmentPresenter.kt @@ -40,25 +40,26 @@ class HintsAndSolutionExplorationManagerFragmentPresenter @Inject constructor( } private fun processEphemeralStateResult(result: AsyncResult) { - if (result.isFailure()) { - oppiaLogger.e( - "HintsAndSolutionExplorationManagerFragmentPresenter", - "Failed to retrieve ephemeral state", - result.getErrorOrNull()!! - ) - return - } else if (result.isPending()) { + when (result) { + is AsyncResult.Failure -> { + oppiaLogger.e( + "HintsAndSolutionExplorationManagerFragmentPresenter", + "Failed to retrieve ephemeral state", + result.error + ) + } // Display nothing until a valid result is available. - return + is AsyncResult.Pending -> {} + is AsyncResult.Success -> { + // Check if hints are available for this state. + val ephemeralState = result.value + if (ephemeralState.state.interaction.hintList.size != 0) { + (activity as HintsAndSolutionExplorationManagerListener).onExplorationStateLoaded( + ephemeralState.state, ephemeralState.writtenTranslationContext + ) + } + } } - val ephemeralState = result.getOrThrow() - - // Check if hints are available for this state. - if (ephemeralState.state.interaction.hintList.size != 0) { - (activity as HintsAndSolutionExplorationManagerListener).onExplorationStateLoaded( - ephemeralState.state, ephemeralState.writtenTranslationContext - ) - } } } diff --git a/app/src/main/java/org/oppia/android/app/player/state/StateFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/player/state/StateFragmentPresenter.kt index 4af453b01d8..06824276dd9 100755 --- a/app/src/main/java/org/oppia/android/app/player/state/StateFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/player/state/StateFragmentPresenter.kt @@ -282,19 +282,15 @@ class StateFragmentPresenter @Inject constructor( } private fun processEphemeralStateResult(result: AsyncResult) { - if (result.isFailure()) { - oppiaLogger.e( - "StateFragment", - "Failed to retrieve ephemeral state", - result.getErrorOrNull()!! - ) - return - } else if (result.isPending()) { - // Display nothing until a valid result is available. - return + when (result) { + is AsyncResult.Failure -> + oppiaLogger.e("StateFragment", "Failed to retrieve ephemeral state", result.error) + is AsyncResult.Pending -> {} // Display nothing until a valid result is available. + is AsyncResult.Success -> processEphemeralState(result.value) } + } - val ephemeralState = result.getOrThrow() + private fun processEphemeralState(ephemeralState: EphemeralState) { explorationCheckpointState = ephemeralState.checkpointState val shouldSplit = splitScreenManager.shouldSplitScreen(ephemeralState.state.interaction.id) if (shouldSplit) { @@ -337,10 +333,8 @@ class StateFragmentPresenter @Inject constructor( resultLiveData.observe( fragment, { result -> - if (result.isFailure()) { - oppiaLogger.e( - "StateFragment", "Failed to retrieve hint/solution", result.getErrorOrNull()!! - ) + if (result is AsyncResult.Failure) { + oppiaLogger.e("StateFragment", "Failed to retrieve hint/solution", result.error) } else { // If the hint/solution, was revealed remove dot and radar. viewModel.setHintOpenedAndUnRevealedVisibility(false) @@ -389,14 +383,16 @@ class StateFragmentPresenter @Inject constructor( private fun processAnswerOutcome( ephemeralStateResult: AsyncResult ): AnswerOutcome { - if (ephemeralStateResult.isFailure()) { - oppiaLogger.e( - "StateFragment", - "Failed to retrieve answer outcome", - ephemeralStateResult.getErrorOrNull()!! - ) + return when (ephemeralStateResult) { + is AsyncResult.Failure -> { + oppiaLogger.e( + "StateFragment", "Failed to retrieve answer outcome", ephemeralStateResult.error + ) + AnswerOutcome.getDefaultInstance() + } + is AsyncResult.Pending -> AnswerOutcome.getDefaultInstance() + is AsyncResult.Success -> ephemeralStateResult.value } - return ephemeralStateResult.getOrDefault(AnswerOutcome.getDefaultInstance()) } private fun handleSubmitAnswer(answer: UserAnswer) { diff --git a/app/src/main/java/org/oppia/android/app/player/state/testing/StateFragmentTestActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/player/state/testing/StateFragmentTestActivityPresenter.kt index 3ca8d4771b4..3f5e8f6238e 100644 --- a/app/src/main/java/org/oppia/android/app/player/state/testing/StateFragmentTestActivityPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/player/state/testing/StateFragmentTestActivityPresenter.kt @@ -96,24 +96,20 @@ class StateFragmentTestActivityPresenter @Inject constructor( explorationId, shouldSavePartialProgress, explorationCheckpoint = ExplorationCheckpoint.getDefaultInstance() - ) - .observe( - activity, - Observer> { result -> - when { - result.isPending() -> oppiaLogger.d(TEST_ACTIVITY_TAG, "Loading exploration") - result.isFailure() -> oppiaLogger.e( - TEST_ACTIVITY_TAG, - "Failed to load exploration", - result.getErrorOrNull()!! - ) - else -> { - oppiaLogger.d(TEST_ACTIVITY_TAG, "Successfully loaded exploration") - initializeExploration(profileId, topicId, storyId, explorationId) - } + ).observe( + activity, + Observer> { result -> + when (result) { + is AsyncResult.Pending -> oppiaLogger.d(TEST_ACTIVITY_TAG, "Loading exploration") + is AsyncResult.Failure -> + oppiaLogger.e(TEST_ACTIVITY_TAG, "Failed to load exploration", result.error) + is AsyncResult.Success -> { + oppiaLogger.d(TEST_ACTIVITY_TAG, "Successfully loaded exploration") + initializeExploration(profileId, topicId, storyId, explorationId) } } - ) + } + ) } /** diff --git a/app/src/main/java/org/oppia/android/app/profile/AddProfileActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/profile/AddProfileActivityPresenter.kt index ce0a67e81db..9f67bd2bbc1 100644 --- a/app/src/main/java/org/oppia/android/app/profile/AddProfileActivityPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/profile/AddProfileActivityPresenter.kt @@ -25,13 +25,13 @@ import com.bumptech.glide.request.target.Target import org.oppia.android.R import org.oppia.android.app.activity.ActivityScope import org.oppia.android.app.translation.AppLanguageResourceHandler -import org.oppia.android.app.utility.TextInputEditTextHelper.Companion.onTextChanged import org.oppia.android.app.viewmodel.ViewModelProvider import org.oppia.android.databinding.AddProfileActivityBinding import org.oppia.android.domain.profile.ProfileManagementController import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.data.DataProviders.Companion.toLiveData import javax.inject.Inject +import org.oppia.android.app.utility.TextInputEditTextHelper.Companion.onTextChanged const val GALLERY_INTENT_RESULT_CODE = 1 @@ -274,26 +274,30 @@ class AddProfileActivityPresenter @Inject constructor( result: AsyncResult, binding: AddProfileActivityBinding ) { - if (result.isSuccess()) { - val intent = Intent(activity, ProfileChooserActivity::class.java) - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) - activity.startActivity(intent) - } else if (result.isFailure()) { - when (result.getErrorOrNull()) { - is ProfileManagementController.ProfileNameNotUniqueException -> - profileViewModel.nameErrorMsg.set( - resourceHandler.getStringInLocale( - R.string.add_profile_error_name_not_unique + when (result) { + is AsyncResult.Success -> { + val intent = Intent(activity, ProfileChooserActivity::class.java) + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + activity.startActivity(intent) + } + is AsyncResult.Failure -> { + when (result.error) { + is ProfileManagementController.ProfileNameNotUniqueException -> + profileViewModel.nameErrorMsg.set( + resourceHandler.getStringInLocale( + R.string.add_profile_error_name_not_unique + ) ) - ) - is ProfileManagementController.ProfileNameOnlyLettersException -> - profileViewModel.nameErrorMsg.set( - resourceHandler.getStringInLocale( - R.string.add_profile_error_name_only_letters + is ProfileManagementController.ProfileNameOnlyLettersException -> + profileViewModel.nameErrorMsg.set( + resourceHandler.getStringInLocale( + R.string.add_profile_error_name_only_letters + ) ) - ) + } + binding.addProfileActivityScrollView.smoothScrollTo(0, 0) } - binding.addProfileActivityScrollView.smoothScrollTo(0, 0) + is AsyncResult.Pending -> {} // Wait for an actual result. } } diff --git a/app/src/main/java/org/oppia/android/app/profile/AdminPinActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/profile/AdminPinActivityPresenter.kt index aae69e73de2..f4c2b160a13 100644 --- a/app/src/main/java/org/oppia/android/app/profile/AdminPinActivityPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/profile/AdminPinActivityPresenter.kt @@ -11,12 +11,13 @@ import org.oppia.android.app.activity.ActivityScope import org.oppia.android.app.administratorcontrols.AdministratorControlsActivity import org.oppia.android.app.model.ProfileId import org.oppia.android.app.translation.AppLanguageResourceHandler -import org.oppia.android.app.utility.TextInputEditTextHelper.Companion.onTextChanged import org.oppia.android.app.viewmodel.ViewModelProvider import org.oppia.android.databinding.AdminPinActivityBinding import org.oppia.android.domain.profile.ProfileManagementController import org.oppia.android.util.data.DataProviders.Companion.toLiveData import javax.inject.Inject +import org.oppia.android.util.data.AsyncResult +import org.oppia.android.app.utility.TextInputEditTextHelper.Companion.onTextChanged /** The presenter for [AdminPinActivity]. */ @ActivityScope @@ -114,7 +115,7 @@ class AdminPinActivityPresenter @Inject constructor( profileManagementController.updatePin(profileId, inputPin).toLiveData().observe( activity, Observer { - if (it.isSuccess()) { + if (it is AsyncResult.Success) { when (activity.intent.getIntExtra(ADMIN_PIN_ENUM_EXTRA_KEY, 0)) { AdminAuthEnum.PROFILE_ADMIN_CONTROLS.value -> { activity.startActivity( diff --git a/app/src/main/java/org/oppia/android/app/profile/PinPasswordActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/profile/PinPasswordActivityPresenter.kt index b8d4b3c8dab..51f940939ca 100644 --- a/app/src/main/java/org/oppia/android/app/profile/PinPasswordActivityPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/profile/PinPasswordActivityPresenter.kt @@ -14,12 +14,13 @@ import org.oppia.android.app.home.HomeActivity import org.oppia.android.app.model.ProfileId import org.oppia.android.app.translation.AppLanguageResourceHandler import org.oppia.android.app.utility.LifecycleSafeTimerFactory -import org.oppia.android.app.utility.TextInputEditTextHelper.Companion.onTextChanged import org.oppia.android.app.viewmodel.ViewModelProvider import org.oppia.android.databinding.PinPasswordActivityBinding import org.oppia.android.domain.profile.ProfileManagementController import org.oppia.android.util.data.DataProviders.Companion.toLiveData import javax.inject.Inject +import org.oppia.android.util.data.AsyncResult +import org.oppia.android.app.utility.TextInputEditTextHelper.Companion.onTextChanged private const val TAG_ADMIN_SETTINGS_DIALOG = "ADMIN_SETTINGS_DIALOG" private const val TAG_RESET_PIN_DIALOG = "RESET_PIN_DIALOG" @@ -88,7 +89,7 @@ class PinPasswordActivityPresenter @Inject constructor( .observe( activity, { - if (it.isSuccess()) { + if (it is AsyncResult.Success) { activity.startActivity((HomeActivity.createHomeActivity(activity, profileId))) } } diff --git a/app/src/main/java/org/oppia/android/app/profile/PinPasswordViewModel.kt b/app/src/main/java/org/oppia/android/app/profile/PinPasswordViewModel.kt index d7d111158ba..edd0ba366ac 100644 --- a/app/src/main/java/org/oppia/android/app/profile/PinPasswordViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/profile/PinPasswordViewModel.kt @@ -48,14 +48,14 @@ class PinPasswordViewModel @Inject constructor( } private fun processGetProfileResult(profileResult: AsyncResult): Profile { - if (profileResult.isFailure()) { - oppiaLogger.e( - "PinPasswordActivity", - "Failed to retrieve profile", - profileResult.getErrorOrNull()!! - ) + val profile = when (profileResult) { + is AsyncResult.Failure -> { + oppiaLogger.e("PinPasswordActivity", "Failed to retrieve profile", profileResult.error) + Profile.getDefaultInstance() + } + is AsyncResult.Pending -> Profile.getDefaultInstance() + is AsyncResult.Success -> profileResult.value } - val profile = profileResult.getOrDefault(Profile.getDefaultInstance()) correctPin.set(profile.pin) isAdmin.set(profile.isAdmin) name.set(profile.name) diff --git a/app/src/main/java/org/oppia/android/app/profile/ProfileChooserFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/profile/ProfileChooserFragmentPresenter.kt index d52fec7f4fe..a91de23b595 100644 --- a/app/src/main/java/org/oppia/android/app/profile/ProfileChooserFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/profile/ProfileChooserFragmentPresenter.kt @@ -124,14 +124,18 @@ class ProfileChooserFragmentPresenter @Inject constructor( private fun processWasProfileEverBeenAddedResult( wasProfileEverBeenAddedResult: AsyncResult ): Boolean { - if (wasProfileEverBeenAddedResult.isFailure()) { - oppiaLogger.e( - "ProfileChooserFragment", - "Failed to retrieve the information on wasProfileEverBeenAdded", - wasProfileEverBeenAddedResult.getErrorOrNull()!! - ) + return when (wasProfileEverBeenAddedResult) { + is AsyncResult.Failure -> { + oppiaLogger.e( + "ProfileChooserFragment", + "Failed to retrieve the information on wasProfileEverBeenAdded", + wasProfileEverBeenAddedResult.error + ) + false + } + is AsyncResult.Pending -> false + is AsyncResult.Success -> wasProfileEverBeenAddedResult.value } - return wasProfileEverBeenAddedResult.getOrDefault(/* defaultValue= */ false) } /** Randomly selects a color for the new profile that is not already in use. */ @@ -174,7 +178,7 @@ class ProfileChooserFragmentPresenter @Inject constructor( profileManagementController.loginToProfile(model.profile.id).toLiveData().observe( fragment, Observer { - if (it.isSuccess()) { + if (it is AsyncResult.Success) { activity.startActivity( ( HomeActivity.createHomeActivity( diff --git a/app/src/main/java/org/oppia/android/app/profile/ProfileChooserViewModel.kt b/app/src/main/java/org/oppia/android/app/profile/ProfileChooserViewModel.kt index 217a72fcbb1..d78a40767e7 100644 --- a/app/src/main/java/org/oppia/android/app/profile/ProfileChooserViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/profile/ProfileChooserViewModel.kt @@ -42,14 +42,16 @@ class ProfileChooserViewModel @Inject constructor( private fun processGetProfilesResult( profilesResult: AsyncResult> ): List { - if (profilesResult.isFailure()) { - oppiaLogger.e( - "ProfileChooserViewModel", - "Failed to retrieve the list of profiles", - profilesResult.getErrorOrNull()!! - ) - } - val profileList = profilesResult.getOrDefault(emptyList()).map { + val profileList = when (profilesResult) { + is AsyncResult.Failure -> { + oppiaLogger.e( + "ProfileChooserViewModel", "Failed to retrieve the list of profiles", profilesResult.error + ) + emptyList() + } + is AsyncResult.Pending -> emptyList() + is AsyncResult.Success -> profilesResult.value + }.map { ProfileChooserUiModel.newBuilder().setProfile(it).build() }.toMutableList() diff --git a/app/src/main/java/org/oppia/android/app/profile/ResetPinDialogFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/profile/ResetPinDialogFragmentPresenter.kt index 852417f4263..96a89d15112 100644 --- a/app/src/main/java/org/oppia/android/app/profile/ResetPinDialogFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/profile/ResetPinDialogFragmentPresenter.kt @@ -12,12 +12,13 @@ import org.oppia.android.R import org.oppia.android.app.fragment.FragmentScope import org.oppia.android.app.model.ProfileId import org.oppia.android.app.translation.AppLanguageResourceHandler -import org.oppia.android.app.utility.TextInputEditTextHelper.Companion.onTextChanged import org.oppia.android.app.viewmodel.ViewModelProvider import org.oppia.android.databinding.ResetPinDialogBinding import org.oppia.android.domain.profile.ProfileManagementController import org.oppia.android.util.data.DataProviders.Companion.toLiveData import javax.inject.Inject +import org.oppia.android.util.data.AsyncResult +import org.oppia.android.app.utility.TextInputEditTextHelper.Companion.onTextChanged /** The presenter for [ResetPinDialogFragment]. */ @FragmentScope @@ -95,7 +96,7 @@ class ResetPinDialogFragmentPresenter @Inject constructor( .observe( fragment, Observer { - if (it.isSuccess()) { + if (it is AsyncResult.Success) { routeDialogInterface.routeToSuccessDialog() } } diff --git a/app/src/main/java/org/oppia/android/app/profileprogress/ProfilePictureActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/profileprogress/ProfilePictureActivityPresenter.kt index 6c3a8a2fb67..9a487c48a1a 100644 --- a/app/src/main/java/org/oppia/android/app/profileprogress/ProfilePictureActivityPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/profileprogress/ProfilePictureActivityPresenter.kt @@ -80,14 +80,14 @@ class ProfilePictureActivityPresenter @Inject constructor( } private fun processGetProfileResult(profileResult: AsyncResult): Profile { - if (profileResult.isFailure()) { - oppiaLogger.e( - "ProfilePictureActivity", - "Failed to retrieve profile", - profileResult.getErrorOrNull()!! - ) + return when (profileResult) { + is AsyncResult.Failure -> { + oppiaLogger.e("ProfilePictureActivity", "Failed to retrieve profile", profileResult.error) + Profile.getDefaultInstance() + } + is AsyncResult.Pending -> Profile.getDefaultInstance() + is AsyncResult.Success -> profileResult.value } - return profileResult.getOrDefault(Profile.getDefaultInstance()) } private fun setProfileAvatar(avatar: ProfileAvatar) { diff --git a/app/src/main/java/org/oppia/android/app/profileprogress/ProfileProgressViewModel.kt b/app/src/main/java/org/oppia/android/app/profileprogress/ProfileProgressViewModel.kt index e84fea15ab5..c6036860a9d 100644 --- a/app/src/main/java/org/oppia/android/app/profileprogress/ProfileProgressViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/profileprogress/ProfileProgressViewModel.kt @@ -74,14 +74,14 @@ class ProfileProgressViewModel @Inject constructor( } private fun processGetProfileResult(profileResult: AsyncResult): Profile { - if (profileResult.isFailure()) { - oppiaLogger.e( - "ProfileProgressFragment", - "Failed to retrieve profile", - profileResult.getErrorOrNull()!! - ) + return when (profileResult) { + is AsyncResult.Failure -> { + oppiaLogger.e("ProfileProgressFragment", "Failed to retrieve profile", profileResult.error) + Profile.getDefaultInstance() + } + is AsyncResult.Pending -> Profile.getDefaultInstance() + is AsyncResult.Success -> profileResult.value } - return profileResult.getOrDefault(Profile.getDefaultInstance()) } private val promotedActivityListResultLiveData: @@ -113,16 +113,20 @@ class ProfileProgressViewModel @Inject constructor( } private fun processPromotedActivityListResult( - promotedActivityListtResult: AsyncResult + promotedActivityListResult: AsyncResult ): PromotedActivityList { - if (promotedActivityListtResult.isFailure()) { - oppiaLogger.e( - "ProfileProgressFragment", - "Failed to retrieve promoted story list: ", - promotedActivityListtResult.getErrorOrNull()!! - ) + return when (promotedActivityListResult) { + is AsyncResult.Failure -> { + oppiaLogger.e( + "ProfileProgressFragment", + "Failed to retrieve promoted story list: ", + promotedActivityListResult.error + ) + PromotedActivityList.getDefaultInstance() + } + is AsyncResult.Pending -> PromotedActivityList.getDefaultInstance() + is AsyncResult.Success -> promotedActivityListResult.value } - return promotedActivityListtResult.getOrDefault(PromotedActivityList.getDefaultInstance()) } private fun processPromotedActivityList( @@ -169,14 +173,18 @@ class ProfileProgressViewModel @Inject constructor( private fun processGetCompletedStoryListResult( completedStoryListResult: AsyncResult ): CompletedStoryList { - if (completedStoryListResult.isFailure()) { - oppiaLogger.e( - "ProfileProgressFragment", - "Failed to retrieve completed story list", - completedStoryListResult.getErrorOrNull()!! - ) + return when (completedStoryListResult) { + is AsyncResult.Failure -> { + oppiaLogger.e( + "ProfileProgressFragment", + "Failed to retrieve completed story list", + completedStoryListResult.error + ) + CompletedStoryList.getDefaultInstance() + } + is AsyncResult.Pending -> CompletedStoryList.getDefaultInstance() + is AsyncResult.Success -> completedStoryListResult.value } - return completedStoryListResult.getOrDefault(CompletedStoryList.getDefaultInstance()) } private fun subscribeToOngoingTopicListLiveData() { @@ -198,13 +206,17 @@ class ProfileProgressViewModel @Inject constructor( private fun processGetOngoingTopicListResult( ongoingTopicListResult: AsyncResult ): OngoingTopicList { - if (ongoingTopicListResult.isFailure()) { - oppiaLogger.e( - "ProfileProgressFragment", - "Failed to retrieve ongoing topic list", - ongoingTopicListResult.getErrorOrNull()!! - ) + return when (ongoingTopicListResult) { + is AsyncResult.Failure -> { + oppiaLogger.e( + "ProfileProgressFragment", + "Failed to retrieve ongoing topic list", + ongoingTopicListResult.error + ) + OngoingTopicList.getDefaultInstance() + } + is AsyncResult.Pending -> OngoingTopicList.getDefaultInstance() + is AsyncResult.Success -> ongoingTopicListResult.value } - return ongoingTopicListResult.getOrDefault(OngoingTopicList.getDefaultInstance()) } } diff --git a/app/src/main/java/org/oppia/android/app/resumelesson/ResumeLessonFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/resumelesson/ResumeLessonFragmentPresenter.kt index 16cc17775b9..be20d0102d5 100644 --- a/app/src/main/java/org/oppia/android/app/resumelesson/ResumeLessonFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/resumelesson/ResumeLessonFragmentPresenter.kt @@ -139,14 +139,18 @@ class ResumeLessonFragmentPresenter @Inject constructor( private fun processChapterSummaryResult( chapterSummaryResult: AsyncResult ): ChapterSummary { - if (chapterSummaryResult.isFailure()) { - oppiaLogger.e( - "ResumeLessonFragment", - "Failed to retrieve chapter summary for the explorationId $explorationId: ", - chapterSummaryResult.getErrorOrNull() - ) + return when (chapterSummaryResult) { + is AsyncResult.Failure -> { + oppiaLogger.e( + "ResumeLessonFragment", + "Failed to retrieve chapter summary for the explorationId $explorationId: ", + chapterSummaryResult.error + ) + ChapterSummary.getDefaultInstance() + } + is AsyncResult.Pending -> ChapterSummary.getDefaultInstance() + is AsyncResult.Success -> chapterSummaryResult.value } - return chapterSummaryResult.getOrDefault(ChapterSummary.getDefaultInstance()) } private fun playExploration( @@ -169,14 +173,11 @@ class ResumeLessonFragmentPresenter @Inject constructor( ).observe( fragment, Observer> { result -> - when { - result.isPending() -> oppiaLogger.d("ResumeLessonFragment", "Loading exploration") - result.isFailure() -> oppiaLogger.e( - "ResumeLessonFragment", - "Failed to load exploration", - result.getErrorOrNull()!! - ) - else -> { + when (result) { + is AsyncResult.Pending -> oppiaLogger.d("ResumeLessonFragment", "Loading exploration") + is AsyncResult.Failure -> + oppiaLogger.e("ResumeLessonFragment", "Failed to load exploration", result.error) + is AsyncResult.Success -> { oppiaLogger.d("ResumeLessonFragment", "Successfully loaded exploration") routeToExplorationListener.routeToExploration( internalProfileId, diff --git a/app/src/main/java/org/oppia/android/app/settings/profile/ProfileEditFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/settings/profile/ProfileEditFragmentPresenter.kt index 9839f15b8a8..ad057a019e2 100644 --- a/app/src/main/java/org/oppia/android/app/settings/profile/ProfileEditFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/settings/profile/ProfileEditFragmentPresenter.kt @@ -16,6 +16,7 @@ import org.oppia.android.domain.oppialogger.OppiaLogger import org.oppia.android.domain.profile.ProfileManagementController import org.oppia.android.util.data.DataProviders.Companion.toLiveData import javax.inject.Inject +import org.oppia.android.util.data.AsyncResult /** Argument key for profile deletion dialog in [ProfileEditFragment]. */ const val TAG_PROFILE_DELETION_DIALOG = "PROFILE_DELETION_DIALOG" @@ -96,11 +97,9 @@ class ProfileEditFragmentPresenter @Inject constructor( ).toLiveData().observe( activity, Observer { - if (it.isFailure()) { + if (it is AsyncResult.Failure) { oppiaLogger.e( - "ProfileEditActivityPresenter", - "Failed to updated allow download access", - it.getErrorOrNull()!! + "ProfileEditActivityPresenter", "Failed to updated allow download access", it.error ) } } @@ -126,7 +125,7 @@ class ProfileEditFragmentPresenter @Inject constructor( .observe( fragment, Observer { - if (it.isSuccess()) { + if (it is AsyncResult.Success) { if (fragment.requireContext().resources.getBoolean(R.bool.isTablet)) { val intent = Intent(fragment.requireContext(), AdministratorControlsActivity::class.java) diff --git a/app/src/main/java/org/oppia/android/app/settings/profile/ProfileEditViewModel.kt b/app/src/main/java/org/oppia/android/app/settings/profile/ProfileEditViewModel.kt index 549c3be7798..75a3a98b5dd 100644 --- a/app/src/main/java/org/oppia/android/app/settings/profile/ProfileEditViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/settings/profile/ProfileEditViewModel.kt @@ -44,14 +44,18 @@ class ProfileEditViewModel @Inject constructor( /** Fetches the profile of a user asynchronously. */ private fun processGetProfileResult(profileResult: AsyncResult): Profile { - if (profileResult.isFailure()) { - oppiaLogger.e( - "ProfileEditViewModel", - "Failed to retrieve the profile with ID: ${profileId.internalId}", - profileResult.getErrorOrNull()!! - ) + val profile = when (profileResult) { + is AsyncResult.Failure -> { + oppiaLogger.e( + "ProfileEditViewModel", + "Failed to retrieve the profile with ID: ${profileId.internalId}", + profileResult.error + ) + Profile.getDefaultInstance() + } + is AsyncResult.Pending -> Profile.getDefaultInstance() + is AsyncResult.Success -> profileResult.value } - val profile = profileResult.getOrDefault(Profile.getDefaultInstance()) isAllowedDownloadAccessMutableLiveData.value = profile.allowDownloadAccess isAdmin = profile.isAdmin return profile diff --git a/app/src/main/java/org/oppia/android/app/settings/profile/ProfileListViewModel.kt b/app/src/main/java/org/oppia/android/app/settings/profile/ProfileListViewModel.kt index a8b82176459..dfa4e50e6fe 100644 --- a/app/src/main/java/org/oppia/android/app/settings/profile/ProfileListViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/settings/profile/ProfileListViewModel.kt @@ -28,14 +28,16 @@ class ProfileListViewModel @Inject constructor( } private fun processGetProfilesResult(profilesResult: AsyncResult>): List { - if (profilesResult.isFailure()) { - oppiaLogger.e( - "ProfileListViewModel", - "Failed to retrieve the list of profiles", - profilesResult.getErrorOrNull()!! - ) + val profileList = when (profilesResult) { + is AsyncResult.Failure -> { + oppiaLogger.e( + "ProfileListViewModel", "Failed to retrieve the list of profiles", profilesResult.error + ) + emptyList() + } + is AsyncResult.Pending -> emptyList() + is AsyncResult.Success -> profilesResult.value } - val profileList = profilesResult.getOrDefault(emptyList()) val sortedProfileList = profileList.sortedBy { machineLocale.run { it.name.toMachineLowerCase() } diff --git a/app/src/main/java/org/oppia/android/app/settings/profile/ProfileRenameFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/settings/profile/ProfileRenameFragmentPresenter.kt index 7ab38ce0f16..e436385e7c6 100644 --- a/app/src/main/java/org/oppia/android/app/settings/profile/ProfileRenameFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/settings/profile/ProfileRenameFragmentPresenter.kt @@ -12,13 +12,13 @@ import org.oppia.android.R import org.oppia.android.app.fragment.FragmentScope import org.oppia.android.app.model.ProfileId import org.oppia.android.app.translation.AppLanguageResourceHandler -import org.oppia.android.app.utility.TextInputEditTextHelper.Companion.onTextChanged import org.oppia.android.app.viewmodel.ViewModelProvider import org.oppia.android.databinding.ProfileRenameFragmentBinding import org.oppia.android.domain.profile.ProfileManagementController import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.data.DataProviders.Companion.toLiveData import javax.inject.Inject +import org.oppia.android.app.utility.TextInputEditTextHelper.Companion.onTextChanged /** The presenter for [ProfileRenameFragment]. */ @FragmentScope @@ -101,12 +101,12 @@ class ProfileRenameFragmentPresenter @Inject constructor( } private fun handleAddProfileResult(result: AsyncResult, profileId: Int) { - if (result.isSuccess()) { + if (result is AsyncResult.Success) { val intent = ProfileEditActivity.createProfileEditActivity(activity, profileId) intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) activity.startActivity(intent) - } else if (result.isFailure()) { - when (result.getErrorOrNull()) { + } else if (result is AsyncResult.Failure) { + when (result.error) { is ProfileManagementController.ProfileNameNotUniqueException -> renameViewModel.nameErrorMsg.set( resourceHandler.getStringInLocale( diff --git a/app/src/main/java/org/oppia/android/app/settings/profile/ProfileResetPinFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/settings/profile/ProfileResetPinFragmentPresenter.kt index da948172c4c..a4b982dd063 100644 --- a/app/src/main/java/org/oppia/android/app/settings/profile/ProfileResetPinFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/settings/profile/ProfileResetPinFragmentPresenter.kt @@ -12,12 +12,13 @@ import androidx.lifecycle.Observer import org.oppia.android.R import org.oppia.android.app.model.ProfileId import org.oppia.android.app.translation.AppLanguageResourceHandler -import org.oppia.android.app.utility.TextInputEditTextHelper.Companion.onTextChanged import org.oppia.android.app.viewmodel.ViewModelProvider import org.oppia.android.databinding.ProfileResetPinFragmentBinding import org.oppia.android.domain.profile.ProfileManagementController import org.oppia.android.util.data.DataProviders.Companion.toLiveData import javax.inject.Inject +import org.oppia.android.util.data.AsyncResult +import org.oppia.android.app.utility.TextInputEditTextHelper.Companion.onTextChanged /** The presenter for [ProfileResetPinFragment]. */ class ProfileResetPinFragmentPresenter @Inject constructor( @@ -133,7 +134,7 @@ class ProfileResetPinFragmentPresenter @Inject constructor( .observe( activity, Observer { - if (it.isSuccess()) { + if (it is AsyncResult.Success) { val intent = ProfileEditActivity.createProfileEditActivity(activity, profileId) intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) activity.startActivity(intent) diff --git a/app/src/main/java/org/oppia/android/app/splash/SplashActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/splash/SplashActivityPresenter.kt index 8d779fb14fd..84fe06a1197 100644 --- a/app/src/main/java/org/oppia/android/app/splash/SplashActivityPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/splash/SplashActivityPresenter.kt @@ -115,16 +115,15 @@ class SplashActivityPresenter @Inject constructor( private fun processInitState( initStateResult: AsyncResult ): SplashInitState { - if (initStateResult.isFailure()) { - oppiaLogger.e( - "SplashActivity", - "Failed to compute initial state", - initStateResult.getErrorOrNull() - ) - } - // If there's an error loading the data, assume the default. - return initStateResult.getOrDefault(SplashInitState.computeDefault(localeController)) + return when (initStateResult) { + is AsyncResult.Failure -> { + oppiaLogger.e("SplashActivity", "Failed to compute initial state", initStateResult.error) + SplashInitState.computeDefault(localeController) + } + is AsyncResult.Pending -> SplashInitState.computeDefault(localeController) + is AsyncResult.Success -> initStateResult.value + } } private fun getDeprecationNoticeDialogFragment(): AutomaticAppDeprecationNoticeDialogFragment? { diff --git a/app/src/main/java/org/oppia/android/app/story/StoryFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/story/StoryFragmentPresenter.kt index a3b485fd7d8..247bd9cef3c 100644 --- a/app/src/main/java/org/oppia/android/app/story/StoryFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/story/StoryFragmentPresenter.kt @@ -278,14 +278,11 @@ class StoryFragmentPresenter @Inject constructor( ).observe( fragment, Observer> { result -> - when { - result.isPending() -> oppiaLogger.d("Story Fragment", "Loading exploration") - result.isFailure() -> oppiaLogger.e( - "Story Fragment", - "Failed to load exploration", - result.getErrorOrNull()!! - ) - else -> { + when (result) { + is AsyncResult.Pending -> oppiaLogger.d("Story Fragment", "Loading exploration") + is AsyncResult.Failure -> + oppiaLogger.e("Story Fragment", "Failed to load exploration", result.error) + is AsyncResult.Success -> { oppiaLogger.d("Story Fragment", "Successfully loaded exploration: $explorationId") routeToExplorationListener.routeToExploration( internalProfileId, diff --git a/app/src/main/java/org/oppia/android/app/story/StoryViewModel.kt b/app/src/main/java/org/oppia/android/app/story/StoryViewModel.kt index 6b7970a08cf..c127eae3707 100644 --- a/app/src/main/java/org/oppia/android/app/story/StoryViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/story/StoryViewModel.kt @@ -70,15 +70,14 @@ class StoryViewModel @Inject constructor( } private fun processStoryResult(storyResult: AsyncResult): StorySummary { - if (storyResult.isFailure()) { - oppiaLogger.e( - "StoryFragment", - "Failed to retrieve Story: ", - storyResult.getErrorOrNull()!! - ) + return when (storyResult) { + is AsyncResult.Failure -> { + oppiaLogger.e("StoryFragment", "Failed to retrieve Story: ", storyResult.error) + StorySummary.getDefaultInstance() + } + is AsyncResult.Pending -> StorySummary.getDefaultInstance() + is AsyncResult.Success -> storyResult.value } - - return storyResult.getOrDefault(StorySummary.getDefaultInstance()) } private fun processStoryChapterList(storySummary: StorySummary): List { diff --git a/app/src/main/java/org/oppia/android/app/story/storyitemviewmodel/StoryChapterSummaryViewModel.kt b/app/src/main/java/org/oppia/android/app/story/storyitemviewmodel/StoryChapterSummaryViewModel.kt index 10bee4d6ddc..db9186002dd 100644 --- a/app/src/main/java/org/oppia/android/app/story/storyitemviewmodel/StoryChapterSummaryViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/story/storyitemviewmodel/StoryChapterSummaryViewModel.kt @@ -56,7 +56,7 @@ class StoryChapterSummaryViewModel( fragment, object : Observer> { override fun onChanged(it: AsyncResult) { - if (it.isSuccess()) { + if (it is AsyncResult.Success) { explorationCheckpointLiveData.removeObserver(this) explorationSelectionListener.selectExploration( internalProfileId, @@ -66,9 +66,9 @@ class StoryChapterSummaryViewModel( canExplorationBeResumed = true, shouldSavePartialProgress, backflowId = 1, - explorationCheckpoint = it.getOrThrow() + explorationCheckpoint = it.value ) - } else if (it.isFailure()) { + } else if (it is AsyncResult.Failure) { explorationCheckpointLiveData.removeObserver(this) explorationSelectionListener.selectExploration( internalProfileId, diff --git a/app/src/main/java/org/oppia/android/app/testing/ExplorationTestActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/testing/ExplorationTestActivityPresenter.kt index 36ae33e2d6e..7542941bbb3 100644 --- a/app/src/main/java/org/oppia/android/app/testing/ExplorationTestActivityPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/testing/ExplorationTestActivityPresenter.kt @@ -50,21 +50,19 @@ class ExplorationTestActivityPresenter @Inject constructor( ).observe( activity, Observer> { result -> - when { - result.isPending() -> oppiaLogger.d(TAG_EXPLORATION_TEST_ACTIVITY, "Loading exploration") - result.isFailure() -> oppiaLogger.e( - TAG_EXPLORATION_TEST_ACTIVITY, - "Failed to load exploration", - result.getErrorOrNull()!! - ) - else -> { + when (result) { + is AsyncResult.Pending -> + oppiaLogger.d(TAG_EXPLORATION_TEST_ACTIVITY, "Loading exploration") + is AsyncResult.Failure -> + oppiaLogger.e(TAG_EXPLORATION_TEST_ACTIVITY, "Failed to load exploration", result.error) + is AsyncResult.Success -> { oppiaLogger.d(TAG_EXPLORATION_TEST_ACTIVITY, "Successfully loaded exploration") routeToExplorationListener.routeToExploration( INTERNAL_PROFILE_ID, TOPIC_ID, STORY_ID, EXPLORATION_ID, - /* backflowScreen= */ null, + backflowScreen = null, isCheckpointingEnabled = false ) } diff --git a/app/src/main/java/org/oppia/android/app/testing/SplashTestActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/testing/SplashTestActivityPresenter.kt index 4bceb84f036..483f745c470 100644 --- a/app/src/main/java/org/oppia/android/app/testing/SplashTestActivityPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/testing/SplashTestActivityPresenter.kt @@ -48,7 +48,7 @@ class SplashTestActivityPresenter @Inject constructor( } private fun processPlatformParameters(loadingStatus: AsyncResult): Boolean { - return loadingStatus.isSuccess() + return loadingStatus is AsyncResult.Success } private fun showToastIfAllowed() { diff --git a/app/src/main/java/org/oppia/android/app/topic/TopicViewModel.kt b/app/src/main/java/org/oppia/android/app/topic/TopicViewModel.kt index 1165eb155f2..46d591d347a 100644 --- a/app/src/main/java/org/oppia/android/app/topic/TopicViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/topic/TopicViewModel.kt @@ -52,14 +52,13 @@ class TopicViewModel @Inject constructor( } private fun processTopicResult(topicResult: AsyncResult): Topic { - if (topicResult.isFailure()) { - oppiaLogger.e( - "TopicFragment", - "Failed to retrieve Topic: ", - topicResult.getErrorOrNull()!! - ) + return when (topicResult) { + is AsyncResult.Failure -> { + oppiaLogger.e("TopicFragment", "Failed to retrieve Topic: ", topicResult.error) + Topic.getDefaultInstance() + } + is AsyncResult.Pending -> Topic.getDefaultInstance() + is AsyncResult.Success -> topicResult.value } - - return topicResult.getOrDefault(Topic.getDefaultInstance()) } } diff --git a/app/src/main/java/org/oppia/android/app/topic/conceptcard/ConceptCardViewModel.kt b/app/src/main/java/org/oppia/android/app/topic/conceptcard/ConceptCardViewModel.kt index f360fa5c359..70964188b58 100644 --- a/app/src/main/java/org/oppia/android/app/topic/conceptcard/ConceptCardViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/topic/conceptcard/ConceptCardViewModel.kt @@ -41,13 +41,15 @@ class ConceptCardViewModel @Inject constructor( private fun processConceptCardResult( conceptCardResult: AsyncResult ): EphemeralConceptCard { - if (conceptCardResult.isFailure()) { - oppiaLogger.e( - "ConceptCardFragment", - "Failed to retrieve Concept Card", - conceptCardResult.getErrorOrNull()!! - ) + return when (conceptCardResult) { + is AsyncResult.Failure -> { + oppiaLogger.e( + "ConceptCardFragment", "Failed to retrieve Concept Card", conceptCardResult.error + ) + EphemeralConceptCard.getDefaultInstance() + } + is AsyncResult.Pending -> EphemeralConceptCard.getDefaultInstance() + is AsyncResult.Success -> conceptCardResult.value } - return conceptCardResult.getOrDefault(EphemeralConceptCard.getDefaultInstance()) } } diff --git a/app/src/main/java/org/oppia/android/app/topic/info/TopicInfoFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/topic/info/TopicInfoFragmentPresenter.kt index c1ece29b555..9195ce8ebb5 100644 --- a/app/src/main/java/org/oppia/android/app/topic/info/TopicInfoFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/topic/info/TopicInfoFragmentPresenter.kt @@ -95,10 +95,14 @@ class TopicInfoFragmentPresenter @Inject constructor( } private fun processTopicResult(topic: AsyncResult): Topic { - if (topic.isFailure()) { - oppiaLogger.e("TopicInfoFragment", "Failed to retrieve topic", topic.getErrorOrNull()!!) + return when (topic) { + is AsyncResult.Failure -> { + oppiaLogger.e("TopicInfoFragment", "Failed to retrieve topic", topic.error) + Topic.getDefaultInstance() + } + is AsyncResult.Pending -> Topic.getDefaultInstance() + is AsyncResult.Success -> topic.value } - return topic.getOrDefault(Topic.getDefaultInstance()) } private fun controlSeeMoreTextVisibility() { diff --git a/app/src/main/java/org/oppia/android/app/topic/lessons/TopicLessonViewModel.kt b/app/src/main/java/org/oppia/android/app/topic/lessons/TopicLessonViewModel.kt index 930b512dd0d..4bf478275cb 100644 --- a/app/src/main/java/org/oppia/android/app/topic/lessons/TopicLessonViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/topic/lessons/TopicLessonViewModel.kt @@ -47,14 +47,14 @@ class TopicLessonViewModel @Inject constructor( } private fun processTopicResult(topic: AsyncResult): Topic { - if (topic.isFailure()) { - oppiaLogger.e( - "TopicLessonFragment", - "Failed to retrieve topic", - topic.getErrorOrNull()!! - ) + return when (topic) { + is AsyncResult.Failure -> { + oppiaLogger.e("TopicLessonFragment", "Failed to retrieve topic", topic.error) + Topic.getDefaultInstance() + } + is AsyncResult.Pending -> Topic.getDefaultInstance() + is AsyncResult.Success -> topic.value } - return topic.getOrDefault(Topic.getDefaultInstance()) } private fun processTopic(topic: Topic): List { diff --git a/app/src/main/java/org/oppia/android/app/topic/lessons/TopicLessonsFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/topic/lessons/TopicLessonsFragmentPresenter.kt index 2e437007ed7..4b22edfbd18 100644 --- a/app/src/main/java/org/oppia/android/app/topic/lessons/TopicLessonsFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/topic/lessons/TopicLessonsFragmentPresenter.kt @@ -243,7 +243,7 @@ class TopicLessonsFragmentPresenter @Inject constructor( fragment, object : Observer> { override fun onChanged(it: AsyncResult) { - if (it.isSuccess()) { + if (it is AsyncResult.Success) { explorationCheckpointLiveData.removeObserver(this) routeToResumeLessonListener.routeToResumeLesson( internalProfileId, @@ -251,9 +251,9 @@ class TopicLessonsFragmentPresenter @Inject constructor( storyId, explorationId, backflowScreen = 0, - explorationCheckpoint = it.getOrThrow() + explorationCheckpoint = it.value ) - } else if (it.isFailure()) { + } else if (it is AsyncResult.Failure) { explorationCheckpointLiveData.removeObserver(this) playExploration( internalProfileId, @@ -295,14 +295,11 @@ class TopicLessonsFragmentPresenter @Inject constructor( ).observe( fragment, Observer> { result -> - when { - result.isPending() -> oppiaLogger.d("TopicLessonsFragment", "Loading exploration") - result.isFailure() -> oppiaLogger.e( - "TopicLessonsFragment", - "Failed to load exploration", - result.getErrorOrNull()!! - ) - else -> { + when (result) { + is AsyncResult.Pending -> oppiaLogger.d("TopicLessonsFragment", "Loading exploration") + is AsyncResult.Failure -> + oppiaLogger.e("TopicLessonsFragment", "Failed to load exploration", result.error) + is AsyncResult.Success -> { oppiaLogger.d("TopicLessonsFragment", "Successfully loaded exploration") routeToExplorationListener.routeToExploration( internalProfileId, diff --git a/app/src/main/java/org/oppia/android/app/topic/practice/TopicPracticeViewModel.kt b/app/src/main/java/org/oppia/android/app/topic/practice/TopicPracticeViewModel.kt index 1cb085fcf87..16f7e7777ff 100644 --- a/app/src/main/java/org/oppia/android/app/topic/practice/TopicPracticeViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/topic/practice/TopicPracticeViewModel.kt @@ -52,14 +52,14 @@ class TopicPracticeViewModel @Inject constructor( } private fun processTopicResult(topic: AsyncResult): Topic { - if (topic.isFailure()) { - oppiaLogger.e( - "TopicPracticeFragment", - "Failed to retrieve topic", - topic.getErrorOrNull()!! - ) + return when (topic) { + is AsyncResult.Failure -> { + oppiaLogger.e("TopicPracticeFragment", "Failed to retrieve topic", topic.error) + Topic.getDefaultInstance() + } + is AsyncResult.Pending -> Topic.getDefaultInstance() + is AsyncResult.Success -> topic.value } - return topic.getOrDefault(Topic.getDefaultInstance()) } private fun processTopicPracticeSkillList(topic: Topic): List { diff --git a/app/src/main/java/org/oppia/android/app/topic/questionplayer/HintsAndSolutionQuestionManagerFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/topic/questionplayer/HintsAndSolutionQuestionManagerFragmentPresenter.kt index c6432134ffe..deeee084741 100644 --- a/app/src/main/java/org/oppia/android/app/topic/questionplayer/HintsAndSolutionQuestionManagerFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/topic/questionplayer/HintsAndSolutionQuestionManagerFragmentPresenter.kt @@ -40,25 +40,24 @@ class HintsAndSolutionQuestionManagerFragmentPresenter @Inject constructor( } private fun processEphemeralStateResult(result: AsyncResult) { - if (result.isFailure()) { - oppiaLogger.e( - "HintsAndSolutionQuestionManagerFragmentPresenter", - "Failed to retrieve ephemeral state", result.getErrorOrNull()!! - ) - return - } else if (result.isPending()) { - // Display nothing until a valid result is available. - return - } - - val ephemeralQuestionState = result.getOrThrow() - - // Check if hints are available for this state. - if (ephemeralQuestionState.ephemeralState.state.interaction.hintList.size != 0) { - (activity as HintsAndSolutionQuestionManagerListener).onQuestionStateLoaded( - ephemeralQuestionState.ephemeralState.state, - ephemeralQuestionState.ephemeralState.writtenTranslationContext - ) + when (result) { + is AsyncResult.Failure -> { + oppiaLogger.e( + "HintsAndSolutionQuestionManagerFragmentPresenter", + "Failed to retrieve ephemeral state", + result.error + ) + } + is AsyncResult.Pending -> {} // Display nothing until a valid result is available. + is AsyncResult.Success -> { + // Check if hints are available for this state. + if (result.value.ephemeralState.state.interaction.hintList.size != 0) { + (activity as HintsAndSolutionQuestionManagerListener).onQuestionStateLoaded( + result.value.ephemeralState.state, + result.value.ephemeralState.writtenTranslationContext + ) + } + } } } } diff --git a/app/src/main/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerActivityPresenter.kt index 4506674b907..082d67aec81 100644 --- a/app/src/main/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerActivityPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerActivityPresenter.kt @@ -11,6 +11,7 @@ import org.oppia.android.domain.oppialogger.OppiaLogger import org.oppia.android.domain.question.QuestionTrainingController import org.oppia.android.util.data.DataProviders.Companion.toLiveData import javax.inject.Inject +import org.oppia.android.util.data.AsyncResult const val TAG_QUESTION_PLAYER_FRAGMENT = "TAG_QUESTION_PLAYER_FRAGMENT" private const val TAG_HINTS_AND_SOLUTION_QUESTION_MANAGER = "HINTS_AND_SOLUTION_QUESTION_MANAGER" @@ -96,20 +97,14 @@ class QuestionPlayerActivityPresenter @Inject constructor( startDataProvider.toLiveData().observe( activity, { - when { - it.isPending() -> oppiaLogger.d( - "QuestionPlayerActivity", - "Starting training session" - ) - it.isFailure() -> { - oppiaLogger.e( - "QuestionPlayerActivity", - "Failed to start training session", - it.getErrorOrNull()!! - ) + when (it) { + is AsyncResult.Pending -> + oppiaLogger.d("QuestionPlayerActivity", "Starting training session") + is AsyncResult.Failure -> { + oppiaLogger.e("QuestionPlayerActivity", "Failed to start training session", it.error) activity.finish() // Can't recover from the session failing to start. } - else -> { + is AsyncResult.Success -> { oppiaLogger.d("QuestionPlayerActivity", "Successfully started training session") callback() } @@ -122,20 +117,14 @@ class QuestionPlayerActivityPresenter @Inject constructor( questionTrainingController.stopQuestionTrainingSession().toLiveData().observe( activity, { - when { - it.isPending() -> oppiaLogger.d( - "QuestionPlayerActivity", - "Stopping training session" - ) - it.isFailure() -> { - oppiaLogger.e( - "QuestionPlayerActivity", - "Failed to stop training session", - it.getErrorOrNull()!! - ) + when (it) { + is AsyncResult.Pending -> + oppiaLogger.d("QuestionPlayerActivity", "Stopping training session") + is AsyncResult.Failure -> { + oppiaLogger.e("QuestionPlayerActivity", "Failed to stop training session", it.error) activity.finish() // Can't recover from the session failing to stop. } - else -> { + is AsyncResult.Success -> { oppiaLogger.d("QuestionPlayerActivity", "Successfully stopped training session") callback() } diff --git a/app/src/main/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerFragmentPresenter.kt index a6db374baf8..e12366009cc 100644 --- a/app/src/main/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerFragmentPresenter.kt @@ -188,17 +188,18 @@ class QuestionPlayerFragmentPresenter @Inject constructor( } private fun processEphemeralQuestionResult(result: AsyncResult) { - if (result.isFailure()) { - oppiaLogger.e( - "QuestionPlayerFragment", - "Failed to retrieve ephemeral question", - result.getErrorOrNull()!! - ) - } else if (result.isPending()) { - // Display nothing until a valid result is available. - return + when (result) { + is AsyncResult.Failure -> { + oppiaLogger.e( + "QuestionPlayerFragment", "Failed to retrieve ephemeral question", result.error + ) + } + is AsyncResult.Pending -> {} // Display nothing until a valid result is available. + is AsyncResult.Success -> processEphemeralQuestion(result.value) } - val ephemeralQuestion = result.getOrThrow() + } + + private fun processEphemeralQuestion(ephemeralQuestion: EphemeralQuestion) { // TODO(#497): Update this to properly link to question assets. val skillId = ephemeralQuestion.question.linkedSkillIdsList.firstOrNull() ?: "" @@ -282,10 +283,8 @@ class QuestionPlayerFragmentPresenter @Inject constructor( resultLiveData.observe( fragment, { result -> - if (result.isFailure()) { - oppiaLogger.e( - "StateFragment", "Failed to retrieve hint/solution", result.getErrorOrNull()!! - ) + if (result is AsyncResult.Failure) { + oppiaLogger.e("StateFragment", "Failed to retrieve hint/solution", result.error) } else { // If the hint/solution, was revealed remove dot and radar. questionViewModel.setHintOpenedAndUnRevealedVisibility(false) @@ -298,14 +297,18 @@ class QuestionPlayerFragmentPresenter @Inject constructor( private fun processAnsweredQuestionOutcome( answeredQuestionOutcomeResult: AsyncResult ): AnsweredQuestionOutcome { - if (answeredQuestionOutcomeResult.isFailure()) { - oppiaLogger.e( - "QuestionPlayerFragment", - "Failed to retrieve answer outcome", - answeredQuestionOutcomeResult.getErrorOrNull()!! - ) + return when (answeredQuestionOutcomeResult) { + is AsyncResult.Failure -> { + oppiaLogger.e( + "QuestionPlayerFragment", + "Failed to retrieve answer outcome", + answeredQuestionOutcomeResult.error + ) + AnsweredQuestionOutcome.getDefaultInstance() + } + is AsyncResult.Pending -> AnsweredQuestionOutcome.getDefaultInstance() + is AsyncResult.Success -> answeredQuestionOutcomeResult.value } - return answeredQuestionOutcomeResult.getOrDefault(AnsweredQuestionOutcome.getDefaultInstance()) } private fun moveToNextState() { diff --git a/app/src/main/java/org/oppia/android/app/topic/revision/TopicRevisionViewModel.kt b/app/src/main/java/org/oppia/android/app/topic/revision/TopicRevisionViewModel.kt index 0849e5f4f7f..a1574b5ca5b 100755 --- a/app/src/main/java/org/oppia/android/app/topic/revision/TopicRevisionViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/topic/revision/TopicRevisionViewModel.kt @@ -54,14 +54,14 @@ class TopicRevisionViewModel @Inject constructor( } private fun processTopicResult(topic: AsyncResult): Topic { - if (topic.isFailure()) { - oppiaLogger.e( - "TopicRevisionFragment", - "Failed to retrieve topic", - topic.getErrorOrNull()!! - ) + return when (topic) { + is AsyncResult.Failure -> { + oppiaLogger.e("TopicRevisionFragment", "Failed to retrieve topic", topic.error) + Topic.getDefaultInstance() + } + is AsyncResult.Pending -> Topic.getDefaultInstance() + is AsyncResult.Success -> topic.value } - return topic.getOrDefault(Topic.getDefaultInstance()) } fun setTopicId(topicId: String) { diff --git a/app/src/main/java/org/oppia/android/app/topic/revisioncard/RevisionCardActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/topic/revisioncard/RevisionCardActivityPresenter.kt index f67bf0289fd..78ce25bfb86 100644 --- a/app/src/main/java/org/oppia/android/app/topic/revisioncard/RevisionCardActivityPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/topic/revisioncard/RevisionCardActivityPresenter.kt @@ -118,15 +118,17 @@ class RevisionCardActivityPresenter @Inject constructor( private fun processSubtopicTitleResult( revisionCardResult: AsyncResult ): String { - if (revisionCardResult.isFailure()) { - oppiaLogger.e( - "RevisionCardActivity", - "Failed to retrieve Revision Card", - revisionCardResult.getErrorOrNull()!! - ) - } val ephemeralRevisionCard = - revisionCardResult.getOrDefault(EphemeralRevisionCard.getDefaultInstance()) + when (revisionCardResult) { + is AsyncResult.Failure -> { + oppiaLogger.e( + "RevisionCardActivity", "Failed to retrieve Revision Card", revisionCardResult.error + ) + EphemeralRevisionCard.getDefaultInstance() + } + is AsyncResult.Pending -> EphemeralRevisionCard.getDefaultInstance() + is AsyncResult.Success -> revisionCardResult.value + } return ephemeralRevisionCard.revisionCard.subtopicTitle } diff --git a/app/src/main/java/org/oppia/android/app/topic/revisioncard/RevisionCardViewModel.kt b/app/src/main/java/org/oppia/android/app/topic/revisioncard/RevisionCardViewModel.kt index 8a2e1fa3cc1..01e859e2d1a 100755 --- a/app/src/main/java/org/oppia/android/app/topic/revisioncard/RevisionCardViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/topic/revisioncard/RevisionCardViewModel.kt @@ -54,13 +54,15 @@ class RevisionCardViewModel @Inject constructor( private fun processRevisionCard( revisionCardResult: AsyncResult ): EphemeralRevisionCard { - if (revisionCardResult.isFailure()) { - oppiaLogger.e( - "RevisionCardFragment", - "Failed to retrieve Revision Card", - revisionCardResult.getErrorOrNull()!! - ) + return when (revisionCardResult) { + is AsyncResult.Failure -> { + oppiaLogger.e( + "RevisionCardFragment", "Failed to retrieve Revision Card", revisionCardResult.error + ) + EphemeralRevisionCard.getDefaultInstance() + } + is AsyncResult.Pending -> EphemeralRevisionCard.getDefaultInstance() + is AsyncResult.Success -> revisionCardResult.value } - return revisionCardResult.getOrDefault(EphemeralRevisionCard.getDefaultInstance()) } } diff --git a/app/src/main/java/org/oppia/android/app/translation/AppLanguageWatcherMixin.kt b/app/src/main/java/org/oppia/android/app/translation/AppLanguageWatcherMixin.kt index e811c558b74..18b3f88b912 100644 --- a/app/src/main/java/org/oppia/android/app/translation/AppLanguageWatcherMixin.kt +++ b/app/src/main/java/org/oppia/android/app/translation/AppLanguageWatcherMixin.kt @@ -69,30 +69,34 @@ class AppLanguageWatcherMixin @Inject constructor( activity, object : Observer> { override fun onChanged(localeResult: AsyncResult) { - if (localeResult.isSuccess()) { - // Only recreate the activity if the locale actually changed (to avoid an infinite - // recreation loop). - if (appLanguageLocaleHandler.updateLocale(localeResult.getOrThrow())) { - // Recreate the activity to apply the latest locale state. Note that in some cases - // this may result in 2 recreations for the user: one to notify that there's a new - // system locale, and a second to actually apply that locale. This is due to a - // limitation in the infrastructure where the app can't know which system locale it - // can use without a LiveData trigger (this class). While this isn't an ideal user - // experience, the expectation is that the recreation should happen fairly quickly. - // If, in practice, that's not the case, the team will need to look into ways of - // synchronizing the UI-kept locale faster (maybe by short-circuiting some of the - // system locale selection code since the underlying I/O state is technically fixed - // and doesn't need a DataProvider past the splash screen). Finally, if the decision - // is made to recreate the activity then ensure it can never happen again in this - // activity by removing this observer. - liveData.removeObserver(this) - activityRecreator.recreate(activity) + when (localeResult) { + is AsyncResult.Success -> { + // Only recreate the activity if the locale actually changed (to avoid an infinite + // recreation loop). + if (appLanguageLocaleHandler.updateLocale(localeResult.value)) { + // Recreate the activity to apply the latest locale state. Note that in some cases + // this may result in 2 recreations for the user: one to notify that there's a new + // system locale, and a second to actually apply that locale. This is due to a + // limitation in the infrastructure where the app can't know which system locale it + // can use without a LiveData trigger (this class). While this isn't an ideal user + // experience, the expectation is that the recreation should happen fairly quickly. + // If, in practice, that's not the case, the team will need to look into ways of + // synchronizing the UI-kept locale faster (maybe by short-circuiting some of the + // system locale selection code since the underlying I/O state is technically fixed + // and doesn't need a DataProvider past the splash screen). Finally, if the decision + // is made to recreate the activity then ensure it can never happen again in this + // activity by removing this observer. + liveData.removeObserver(this) + activityRecreator.recreate(activity) + } } - } else if (localeResult.isFailure()) { - oppiaLogger.e( - "AppLanguageWatcherMixin", - "Failed to retrieve app string locale for activity: $activity" - ) + is AsyncResult.Failure -> { + oppiaLogger.e( + "AppLanguageWatcherMixin", + "Failed to retrieve app string locale for activity: $activity" + ) + } + is AsyncResult.Pending -> {} // Wait for an actual result. } } } diff --git a/app/src/main/java/org/oppia/android/app/walkthrough/end/WalkthroughFinalFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/walkthrough/end/WalkthroughFinalFragmentPresenter.kt index b39919ee66a..a49ae9378ea 100644 --- a/app/src/main/java/org/oppia/android/app/walkthrough/end/WalkthroughFinalFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/walkthrough/end/WalkthroughFinalFragmentPresenter.kt @@ -93,14 +93,14 @@ class WalkthroughFinalFragmentPresenter @Inject constructor( } private fun processTopicResult(topic: AsyncResult): Topic { - if (topic.isFailure()) { - oppiaLogger.e( - "WalkthroughFinalFragment", - "Failed to retrieve topic", - topic.getErrorOrNull()!! - ) + return when (topic) { + is AsyncResult.Failure -> { + oppiaLogger.e("WalkthroughFinalFragment", "Failed to retrieve topic", topic.error) + Topic.getDefaultInstance() + } + is AsyncResult.Pending -> Topic.getDefaultInstance() + is AsyncResult.Success -> topic.value } - return topic.getOrDefault(Topic.getDefaultInstance()) } override fun goBack() { diff --git a/app/src/main/java/org/oppia/android/app/walkthrough/topiclist/WalkthroughTopicViewModel.kt b/app/src/main/java/org/oppia/android/app/walkthrough/topiclist/WalkthroughTopicViewModel.kt index a02fbce4815..19eb3fbea61 100644 --- a/app/src/main/java/org/oppia/android/app/walkthrough/topiclist/WalkthroughTopicViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/walkthrough/topiclist/WalkthroughTopicViewModel.kt @@ -37,14 +37,18 @@ class WalkthroughTopicViewModel @Inject constructor( } private fun processTopicListResult(topicSummaryListResult: AsyncResult): TopicList { - if (topicSummaryListResult.isFailure()) { - oppiaLogger.e( - "WalkthroughTopicSummaryListFragment", - "Failed to retrieve TopicSummary list: ", - topicSummaryListResult.getErrorOrNull()!! - ) + return when (topicSummaryListResult) { + is AsyncResult.Failure -> { + oppiaLogger.e( + "WalkthroughTopicSummaryListFragment", + "Failed to retrieve TopicSummary list: ", + topicSummaryListResult.error + ) + TopicList.getDefaultInstance() + } + is AsyncResult.Pending -> TopicList.getDefaultInstance() + is AsyncResult.Success -> topicSummaryListResult.value } - return topicSummaryListResult.getOrDefault(TopicList.getDefaultInstance()) } private fun processCompletedTopicList(topicList: TopicList): List { diff --git a/app/src/main/java/org/oppia/android/app/walkthrough/welcome/WalkthroughWelcomeFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/walkthrough/welcome/WalkthroughWelcomeFragmentPresenter.kt index 04fc2404c9d..3c49e08cbc1 100644 --- a/app/src/main/java/org/oppia/android/app/walkthrough/welcome/WalkthroughWelcomeFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/walkthrough/welcome/WalkthroughWelcomeFragmentPresenter.kt @@ -87,14 +87,16 @@ class WalkthroughWelcomeFragmentPresenter @Inject constructor( } private fun processGetProfileResult(profileResult: AsyncResult): Profile { - if (profileResult.isFailure()) { - oppiaLogger.e( - "WalkthroughWelcomeFragment", - "Failed to retrieve profile", - profileResult.getErrorOrNull()!! - ) + return when (profileResult) { + is AsyncResult.Failure -> { + oppiaLogger.e( + "WalkthroughWelcomeFragment", "Failed to retrieve profile", profileResult.error + ) + Profile.getDefaultInstance() + } + is AsyncResult.Pending -> Profile.getDefaultInstance() + is AsyncResult.Success -> profileResult.value } - return profileResult.getOrDefault(Profile.getDefaultInstance()) } private fun setProfileName() { diff --git a/app/src/sharedTest/java/org/oppia/android/app/profile/ProfileChooserFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/profile/ProfileChooserFragmentTest.kt index e8c2691c584..b39f8d5ab5c 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/profile/ProfileChooserFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/profile/ProfileChooserFragmentTest.kt @@ -84,7 +84,6 @@ import org.oppia.android.testing.time.FakeOppiaClockModule import org.oppia.android.util.accessibility.AccessibilityTestModule import org.oppia.android.util.caching.AssetModule import org.oppia.android.util.caching.testing.CachingTestModule -import org.oppia.android.util.data.DataProviders.Companion.toLiveData import org.oppia.android.util.gcsresource.GcsResourceModule import org.oppia.android.util.locale.LocaleProdModule import org.oppia.android.util.logging.LoggerModule @@ -337,7 +336,7 @@ class ProfileChooserFragmentTest { allowDownloadAccess = true, colorRgb = -10710042, isAdmin = true - ).toLiveData() + ) launch(createProfileChooserActivityIntent()).use { testCoroutineDispatchers.runCurrent() onView( @@ -361,7 +360,7 @@ class ProfileChooserFragmentTest { allowDownloadAccess = true, colorRgb = -10710042, isAdmin = true - ).toLiveData() + ) launch(createProfileChooserActivityIntent()).use { testCoroutineDispatchers.runCurrent() onView(withId(R.id.administrator_controls_linear_layout)).perform(click()) diff --git a/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileEditActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileEditActivityTest.kt index 0fcc92fc225..719f475a09b 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileEditActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileEditActivityTest.kt @@ -82,7 +82,6 @@ import org.oppia.android.testing.time.FakeOppiaClockModule import org.oppia.android.util.accessibility.AccessibilityTestModule import org.oppia.android.util.caching.AssetModule import org.oppia.android.util.caching.testing.CachingTestModule -import org.oppia.android.util.data.DataProviders.Companion.toLiveData import org.oppia.android.util.gcsresource.GcsResourceModule import org.oppia.android.util.locale.LocaleProdModule import org.oppia.android.util.logging.LoggerModule @@ -410,7 +409,7 @@ class ProfileEditActivityTest { allowDownloadAccess = true, colorRgb = -10710042, isAdmin = false - ).toLiveData() + ) launch( ProfileEditActivity.createProfileEditActivity( context = context, @@ -431,7 +430,7 @@ class ProfileEditActivityTest { allowDownloadAccess = true, colorRgb = -10710042, isAdmin = false - ).toLiveData() + ) launch( ProfileEditActivity.createProfileEditActivity( context = context, @@ -453,7 +452,7 @@ class ProfileEditActivityTest { allowDownloadAccess = true, colorRgb = -10710042, isAdmin = false - ).toLiveData() + ) launch( ProfileEditActivity.createProfileEditActivity( context = context, @@ -476,7 +475,7 @@ class ProfileEditActivityTest { allowDownloadAccess = true, colorRgb = -10710042, isAdmin = false - ).toLiveData() + ) launch( ProfileEditActivity.createProfileEditActivity( context = context, @@ -497,7 +496,7 @@ class ProfileEditActivityTest { allowDownloadAccess = true, colorRgb = -10710042, isAdmin = false - ).toLiveData() + ) launch( ProfileEditActivity.createProfileEditActivity( context = context, @@ -518,7 +517,7 @@ class ProfileEditActivityTest { allowDownloadAccess = true, colorRgb = -10710042, isAdmin = false - ).toLiveData() + ) launch( ProfileEditActivity.createProfileEditActivity( context = context, diff --git a/data/BUILD.bazel b/data/BUILD.bazel index 3dc0dde487c..54900badcec 100644 --- a/data/BUILD.bazel +++ b/data/BUILD.bazel @@ -16,6 +16,8 @@ TEST_DEPS = [ "//data/src/main/java/org/oppia/android/data/persistence:cache_store", "//model:test_models", "//testing", + "//testing/src/main/java/org/oppia/android/testing/data:async_result_subject", + "//testing/src/main/java/org/oppia/android/testing/data:data_provider_test_monitor", "//testing/src/main/java/org/oppia/android/testing/network", "//testing/src/main/java/org/oppia/android/testing/network:test_module", "//testing/src/main/java/org/oppia/android/testing/platformparameter:test_constants", diff --git a/data/src/main/java/org/oppia/android/data/persistence/PersistentCacheStore.kt b/data/src/main/java/org/oppia/android/data/persistence/PersistentCacheStore.kt index 19608033e7b..c0305216ab8 100644 --- a/data/src/main/java/org/oppia/android/data/persistence/PersistentCacheStore.kt +++ b/data/src/main/java/org/oppia/android/data/persistence/PersistentCacheStore.kt @@ -64,7 +64,7 @@ class PersistentCacheStore private constructor( // First, determine whether the current cache has been attempted to be retrieved from disk. if (cachePayload.state == CacheState.UNLOADED) { deferLoadFile() - return AsyncResult.pending() + return AsyncResult.Pending() } // Second, check if a previous deferred read failed. The store stays in a failed state until @@ -74,13 +74,13 @@ class PersistentCacheStore private constructor( failureLock.withLock { deferredLoadCacheFailure?.let { // A previous read failed. - return AsyncResult.failed(it) + return AsyncResult.Failure(it) } } // Finally, check if there's an in-memory cached value that can be loaded now. // Otherwise, there should be a guaranteed in-memory value to use, instead. - return AsyncResult.success(cachePayload.value) + return AsyncResult.Success(cachePayload.value) } } diff --git a/data/src/test/java/org/oppia/android/data/persistence/PersistentCacheStoreTest.kt b/data/src/test/java/org/oppia/android/data/persistence/PersistentCacheStoreTest.kt index 45f77380839..1129b672df3 100644 --- a/data/src/test/java/org/oppia/android/data/persistence/PersistentCacheStoreTest.kt +++ b/data/src/test/java/org/oppia/android/data/persistence/PersistentCacheStoreTest.kt @@ -2,106 +2,74 @@ package org.oppia.android.data.persistence import android.app.Application import android.content.Context -import androidx.lifecycle.Observer import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat -import com.google.protobuf.MessageLite +import com.google.common.truth.extensions.proto.LiteProtoTruth.assertThat import dagger.BindsInstance import dagger.Component import dagger.Module import dagger.Provides +import java.io.File +import java.io.FileOutputStream +import java.io.IOException +import javax.inject.Inject +import javax.inject.Singleton import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.async import org.junit.Before -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentCaptor -import org.mockito.Captor -import org.mockito.Mock -import org.mockito.Mockito.atLeastOnce -import org.mockito.Mockito.reset -import org.mockito.Mockito.verify -import org.mockito.Mockito.verifyZeroInteractions -import org.mockito.junit.MockitoJUnit -import org.mockito.junit.MockitoRule import org.oppia.android.app.model.TestMessage import org.oppia.android.testing.TestLogReportingModule +import org.oppia.android.testing.data.AsyncResultSubject.Companion.assertThat +import org.oppia.android.testing.data.DataProviderTestMonitor import org.oppia.android.testing.robolectric.RobolectricModule import org.oppia.android.testing.threading.TestCoroutineDispatchers import org.oppia.android.testing.threading.TestDispatcherModule -import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.data.DataProviders -import org.oppia.android.util.data.DataProviders.Companion.toLiveData import org.oppia.android.util.data.DataProvidersInjector import org.oppia.android.util.data.DataProvidersInjectorProvider import org.oppia.android.util.threading.BackgroundDispatcher import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode -import java.io.File -import java.io.FileOutputStream -import java.io.IOException -import javax.inject.Inject -import javax.inject.Singleton private const val CACHE_NAME_1 = "test_cache_1" private const val CACHE_NAME_2 = "test_cache_2" /** Tests for [PersistentCacheStore]. */ +// Same parameter value: helpers reduce test context, even if they are used by 1 test. +// Function name: test names are conventionally named with underscores. +@Suppress("SameParameterValue", "FunctionName") @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) @Config(application = PersistentCacheStoreTest.TestApplication::class) class PersistentCacheStoreTest { private companion object { - private val TEST_MESSAGE_VERSION_1 = TestMessage.newBuilder().setIntValue(1).build() - private val TEST_MESSAGE_VERSION_2 = TestMessage.newBuilder().setIntValue(2).build() + private val TEST_MESSAGE_V1 = TestMessage.newBuilder().setIntValue(1).build() + private val TEST_MESSAGE_V2 = TestMessage.newBuilder().setIntValue(2).build() } - @Rule - @JvmField - val mockitoRule: MockitoRule = MockitoJUnit.rule() - - @Inject - lateinit var cacheFactory: PersistentCacheStore.Factory - - @Inject - lateinit var dataProviders: DataProviders - - @Inject - lateinit var testCoroutineDispatchers: TestCoroutineDispatchers - - @Inject - @field:BackgroundDispatcher - lateinit var backgroundDispatcher: CoroutineDispatcher - - @Mock - lateinit var mockUserAppHistoryObserver1: Observer> + @Inject lateinit var cacheFactory: PersistentCacheStore.Factory + @Inject lateinit var dataProviders: DataProviders + @Inject lateinit var testCoroutineDispatchers: TestCoroutineDispatchers + @field:[Inject BackgroundDispatcher] lateinit var backgroundDispatcher: CoroutineDispatcher + @Inject lateinit var monitorFactory: DataProviderTestMonitor.Factory - @Mock - lateinit var mockUserAppHistoryObserver2: Observer> - - @Captor - lateinit var userAppHistoryResultCaptor1: ArgumentCaptor> - - @Captor - lateinit var userAppHistoryResultCaptor2: ArgumentCaptor> - - private val backgroundDispatcherScope by lazy { - CoroutineScope(backgroundDispatcher) - } + private val backgroundDispatcherScope by lazy { CoroutineScope(backgroundDispatcher) } @Before fun setUp() { setUpTestApplicationComponent() } - // TODO(#59): Create a test-only proto for this test rather than needing to reuse a production-facing proto. + // TODO(#59): Create a test-only proto for this test rather than needing to reuse a + // production-facing proto. @Test @ExperimentalCoroutinesApi - fun testCache_toLiveData_initialState_isPending() { + fun testCache_initialState_isPending() { val cacheStore = cacheFactory.create(CACHE_NAME_1, TestMessage.getDefaultInstance()) // Directly call retrieveData() to get the very initial state of the provider. Relying on @@ -116,289 +84,221 @@ class PersistentCacheStoreTest { } testCoroutineDispatchers.advanceUntilIdle() - val result = deferredResult.getCompleted() - assertThat(result.isPending()).isTrue() + assertThat(deferredResult.getCompleted()).isPending() } @Test - fun testCache_toLiveData_loaded_providesInitialValue() { + fun testCache_loaded_providesInitialValue() { val cacheStore = cacheFactory.create(CACHE_NAME_1, TestMessage.getDefaultInstance()) - observeCache(cacheStore, mockUserAppHistoryObserver1) + val value = monitorFactory.waitForNextSuccessfulResult(cacheStore) // The initial cache state should be the default cache value. - verify( - mockUserAppHistoryObserver1, - atLeastOnce() - ).onChanged(userAppHistoryResultCaptor1.capture()) - assertThat(userAppHistoryResultCaptor1.value.isSuccess()).isTrue() - assertThat(userAppHistoryResultCaptor1.value.getOrThrow()).isEqualTo( - TestMessage.getDefaultInstance() - ) + assertThat(value).isEqualToDefaultInstance() } @Test - fun testCache_nonDefaultInitialState_toLiveData_loaded_providesCorrectInitialVal() { - val cacheStore = cacheFactory.create(CACHE_NAME_1, TEST_MESSAGE_VERSION_1) + fun testCache_nonDefaultInitialState_loaded_providesCorrectInitialVal() { + val cacheStore = cacheFactory.create(CACHE_NAME_1, TEST_MESSAGE_V1) - observeCache(cacheStore, mockUserAppHistoryObserver1) + val value = monitorFactory.waitForNextSuccessfulResult(cacheStore) // Caches can have non-default initial states. - verify( - mockUserAppHistoryObserver1, - atLeastOnce() - ).onChanged(userAppHistoryResultCaptor1.capture()) - assertThat(userAppHistoryResultCaptor1.value.isSuccess()).isTrue() - assertThat(userAppHistoryResultCaptor1.value.getOrThrow()).isEqualTo(TEST_MESSAGE_VERSION_1) + assertThat(value).isEqualTo(TEST_MESSAGE_V1) } @Test fun testCache_registerObserver_updateAfter_observerNotifiedOfNewValue() { val cacheStore = cacheFactory.create(CACHE_NAME_1, TestMessage.getDefaultInstance()) - observeCache(cacheStore, mockUserAppHistoryObserver1) - reset(mockUserAppHistoryObserver1) - val storeOp = cacheStore.storeDataAsync { TEST_MESSAGE_VERSION_1 } + monitorFactory.waitForNextSuccessfulResult(cacheStore) + val storeOp = cacheStore.storeDataAsync { TEST_MESSAGE_V1 } testCoroutineDispatchers.advanceUntilIdle() - // The store operation should be completed, and the observer should be notified of the changed value. + // The store operation should be completed, and the observer should be notified of the changed + // value. assertThat(storeOp.isCompleted).isTrue() - verify(mockUserAppHistoryObserver1).onChanged(userAppHistoryResultCaptor1.capture()) - assertThat(userAppHistoryResultCaptor1.value.isSuccess()).isTrue() - assertThat(userAppHistoryResultCaptor1.value.getOrThrow()).isEqualTo(TEST_MESSAGE_VERSION_1) + assertThat(monitorFactory.waitForNextSuccessfulResult(cacheStore)).isEqualTo(TEST_MESSAGE_V1) } @Test fun testCache_registerObserver_updateBefore_observesUpdatedStateInitially() { val cacheStore = cacheFactory.create(CACHE_NAME_1, TestMessage.getDefaultInstance()) - val storeOp = cacheStore.storeDataAsync { TEST_MESSAGE_VERSION_1 } + val storeOp = cacheStore.storeDataAsync { TEST_MESSAGE_V1 } testCoroutineDispatchers.advanceUntilIdle() - observeCache(cacheStore, mockUserAppHistoryObserver1) - // The store operation should be completed, and the observer's only call should be the updated state. + // The store operation should be completed, and the observer's only call should be the updated + // state. + val value = monitorFactory.waitForNextSuccessfulResult(cacheStore) assertThat(storeOp.isCompleted).isTrue() - verify(mockUserAppHistoryObserver1).onChanged(userAppHistoryResultCaptor1.capture()) - assertThat(userAppHistoryResultCaptor1.value.isSuccess()).isTrue() - assertThat(userAppHistoryResultCaptor1.value.getOrThrow()).isEqualTo(TEST_MESSAGE_VERSION_1) + assertThat(value).isEqualTo(TEST_MESSAGE_V1) } @Test fun testCache_noMemoryCacheUpdate_updateAfterReg_observerNotNotified() { val cacheStore = cacheFactory.create(CACHE_NAME_1, TestMessage.getDefaultInstance()) + val monitor = monitorFactory.createMonitor(cacheStore) - observeCache(cacheStore, mockUserAppHistoryObserver1) - reset(mockUserAppHistoryObserver1) - val storeOp = cacheStore.storeDataAsync(updateInMemoryCache = false) { TEST_MESSAGE_VERSION_1 } + monitor.waitForNextSuccessResult() + val storeOp = cacheStore.storeDataAsync(updateInMemoryCache = false) { TEST_MESSAGE_V1 } testCoroutineDispatchers.advanceUntilIdle() - // The store operation should be completed, but the observe will not be notified of changes since the in-memory - // cache was not changed. + // The store operation should be completed, but the observe will not be notified of changes + // since the in-memory cache was not changed. assertThat(storeOp.isCompleted).isTrue() - verifyZeroInteractions(mockUserAppHistoryObserver1) + monitor.verifyProviderIsNotUpdated() } @Test fun testCache_noMemoryCacheUpdate_updateBeforeReg_observesUpdatedState() { val cacheStore = cacheFactory.create(CACHE_NAME_1, TestMessage.getDefaultInstance()) - val storeOp = cacheStore.storeDataAsync(updateInMemoryCache = false) { TEST_MESSAGE_VERSION_1 } + val storeOp = cacheStore.storeDataAsync(updateInMemoryCache = false) { TEST_MESSAGE_V1 } testCoroutineDispatchers.advanceUntilIdle() - observeCache(cacheStore, mockUserAppHistoryObserver1) - // The store operation should be completed, but the observer will receive the updated state since the cache wasn't - // primed and no previous observers initialized it. - // NB: This may not be ideal behavior long-term; the store may need to be updated to be more resilient to these - // types of scenarios. + // The store operation should be completed, but the observer will receive the updated state + // since the cache wasn't primed and no previous observers initialized it. + // NB: This may not be ideal behavior long-term; the store may need to be updated to be more + // resilient to these types of scenarios. assertThat(storeOp.isCompleted).isTrue() - verify( - mockUserAppHistoryObserver1, - atLeastOnce() - ).onChanged(userAppHistoryResultCaptor1.capture()) - assertThat(userAppHistoryResultCaptor1.value.isSuccess()).isTrue() - assertThat(userAppHistoryResultCaptor1.value.getOrThrow()).isEqualTo(TEST_MESSAGE_VERSION_1) + assertThat(monitorFactory.waitForNextSuccessfulResult(cacheStore)).isEqualTo(TEST_MESSAGE_V1) } @Test fun testCache_updated_newCache_newObserver_observesNewValue() { val cacheStore1 = cacheFactory.create(CACHE_NAME_1, TestMessage.getDefaultInstance()) - val storeOp = cacheStore1.storeDataAsync { TEST_MESSAGE_VERSION_1 } + val storeOp = cacheStore1.storeDataAsync { TEST_MESSAGE_V1 } testCoroutineDispatchers.advanceUntilIdle() // Create a new cache with the same name. val cacheStore2 = cacheFactory.create(CACHE_NAME_1, TestMessage.getDefaultInstance()) - observeCache(cacheStore2, mockUserAppHistoryObserver1) - // The new cache should have the updated value since it points to the same file as the first cache. This is - // simulating something closer to an app restart or non-UI Dagger component refresh since UI components should share - // the same cache instance via an application-bound controller object. + // The new cache should have the updated value since it points to the same file as the first + // cache. This is simulating something closer to an app restart or non-UI Dagger component + // refresh since UI components should share the same cache instance via an application-bound + // controller object. assertThat(storeOp.isCompleted).isTrue() - verify( - mockUserAppHistoryObserver1, - atLeastOnce() - ).onChanged(userAppHistoryResultCaptor1.capture()) - assertThat(userAppHistoryResultCaptor1.value.isSuccess()).isTrue() - assertThat(userAppHistoryResultCaptor1.value.getOrThrow()).isEqualTo(TEST_MESSAGE_VERSION_1) + assertThat(monitorFactory.waitForNextSuccessfulResult(cacheStore2)).isEqualTo(TEST_MESSAGE_V1) } @Test fun testCache_updated_noInMemoryCacheUpdate_newCache_newObserver_observesNewVal() { val cacheStore1 = cacheFactory.create(CACHE_NAME_1, TestMessage.getDefaultInstance()) - val storeOp = cacheStore1.storeDataAsync(updateInMemoryCache = false) { TEST_MESSAGE_VERSION_1 } + val storeOp = cacheStore1.storeDataAsync(updateInMemoryCache = false) { TEST_MESSAGE_V1 } testCoroutineDispatchers.advanceUntilIdle() // Create a new cache with the same name. val cacheStore2 = cacheFactory.create(CACHE_NAME_1, TestMessage.getDefaultInstance()) - observeCache(cacheStore2, mockUserAppHistoryObserver1) - // The new cache should have the updated value since it points to the same file as the first cache, even though the - // update operation did not update the in-memory cache (the new cache has a separate in-memory cache). This is - // simulating something closer to an app restart or non-UI Dagger component refresh since UI components should share - // the same cache instance via an application-bound controller object. + // The new cache should have the updated value since it points to the same file as the first + // cache, even though the update operation did not update the in-memory cache (the new cache has + // a separate in-memory cache). This is simulating something closer to an app restart or non-UI + // Dagger component refresh since UI components should share the same cache instance via an + // application-bound controller object. assertThat(storeOp.isCompleted).isTrue() - verify( - mockUserAppHistoryObserver1, - atLeastOnce() - ).onChanged(userAppHistoryResultCaptor1.capture()) - assertThat(userAppHistoryResultCaptor1.value.isSuccess()).isTrue() - assertThat(userAppHistoryResultCaptor1.value.getOrThrow()).isEqualTo(TEST_MESSAGE_VERSION_1) + assertThat(monitorFactory.waitForNextSuccessfulResult(cacheStore2)).isEqualTo(TEST_MESSAGE_V1) } @Test fun testExistingDiskCache_newCacheObject_updateNoMemThenRead_receivesNewValue() { val cacheStore1 = cacheFactory.create(CACHE_NAME_1, TestMessage.getDefaultInstance()) val storeOp1 = - cacheStore1.storeDataAsync(updateInMemoryCache = false) { TEST_MESSAGE_VERSION_1 } + cacheStore1.storeDataAsync(updateInMemoryCache = false) { TEST_MESSAGE_V1 } testCoroutineDispatchers.advanceUntilIdle() // Create a new cache with the same name and update it, then observe it. val cacheStore2 = cacheFactory.create(CACHE_NAME_1, TestMessage.getDefaultInstance()) - val storeOp2 = - cacheStore2.storeDataAsync(updateInMemoryCache = false) { TEST_MESSAGE_VERSION_2 } + val storeOp2 = cacheStore2.storeDataAsync(updateInMemoryCache = false) { TEST_MESSAGE_V2 } testCoroutineDispatchers.advanceUntilIdle() - observeCache(cacheStore2, mockUserAppHistoryObserver1) - // Both operations should be complete, and the observer will receive the latest value since the update was posted - // before the read occurred. + // Both operations should be complete, and the observer will receive the latest value since the + // update was posted before the read occurred. assertThat(storeOp1.isCompleted).isTrue() assertThat(storeOp2.isCompleted).isTrue() - verify( - mockUserAppHistoryObserver1, - atLeastOnce() - ).onChanged(userAppHistoryResultCaptor1.capture()) - assertThat(userAppHistoryResultCaptor1.value.isSuccess()).isTrue() - assertThat(userAppHistoryResultCaptor1.value.getOrThrow()).isEqualTo(TEST_MESSAGE_VERSION_2) + assertThat(monitorFactory.waitForNextSuccessfulResult(cacheStore2)).isEqualTo(TEST_MESSAGE_V2) } @Test fun testExistingDiskCache_newObject_updateNoMemThenRead_primed_receivesPrevVal() { val cacheStore1 = cacheFactory.create(CACHE_NAME_1, TestMessage.getDefaultInstance()) val storeOp1 = - cacheStore1.storeDataAsync(updateInMemoryCache = false) { TEST_MESSAGE_VERSION_1 } + cacheStore1.storeDataAsync(updateInMemoryCache = false) { TEST_MESSAGE_V1 } testCoroutineDispatchers.advanceUntilIdle() - // Create a new cache with the same name and update it, then observe it. However, first prime it. + // Create a new cache with the same name and update it, then observe it. However, first prime + // it. val cacheStore2 = cacheFactory.create(CACHE_NAME_1, TestMessage.getDefaultInstance()) val primeOp = cacheStore2.primeCacheAsync() testCoroutineDispatchers.advanceUntilIdle() - val storeOp2 = - cacheStore2.storeDataAsync(updateInMemoryCache = false) { TEST_MESSAGE_VERSION_2 } + val storeOp2 = cacheStore2.storeDataAsync(updateInMemoryCache = false) { TEST_MESSAGE_V2 } testCoroutineDispatchers.advanceUntilIdle() - observeCache(cacheStore2, mockUserAppHistoryObserver1) - // All operations should be complete, but the observer will receive the previous update rather than th elatest since - // it wasn't updated in memory and the cache was pre-primed. + // All operations should be complete, but the observer will receive the previous update rather + // than the latest since it wasn't updated in memory and the cache was pre-primed. assertThat(storeOp1.isCompleted).isTrue() assertThat(storeOp2.isCompleted).isTrue() assertThat(primeOp.isCompleted).isTrue() - verify( - mockUserAppHistoryObserver1, - atLeastOnce() - ).onChanged(userAppHistoryResultCaptor1.capture()) - assertThat(userAppHistoryResultCaptor1.value.isSuccess()).isTrue() - assertThat(userAppHistoryResultCaptor1.value.getOrThrow()).isEqualTo(TEST_MESSAGE_VERSION_1) + assertThat(monitorFactory.waitForNextSuccessfulResult(cacheStore2)).isEqualTo(TEST_MESSAGE_V1) } @Test fun testExistingDiskCache_newObject_updateMemThenRead_primed_receivesNewVal() { val cacheStore1 = cacheFactory.create(CACHE_NAME_1, TestMessage.getDefaultInstance()) val storeOp1 = - cacheStore1.storeDataAsync(updateInMemoryCache = false) { TEST_MESSAGE_VERSION_1 } + cacheStore1.storeDataAsync(updateInMemoryCache = false) { TEST_MESSAGE_V1 } testCoroutineDispatchers.advanceUntilIdle() - // Create a new cache with the same name and update it, then observe it. However, first prime it. + // Create a new cache with the same name and update it, then observe it. However, first prime + // it. val cacheStore2 = cacheFactory.create(CACHE_NAME_1, TestMessage.getDefaultInstance()) val primeOp = cacheStore2.primeCacheAsync() testCoroutineDispatchers.advanceUntilIdle() - val storeOp2 = cacheStore2.storeDataAsync { TEST_MESSAGE_VERSION_2 } + val storeOp2 = cacheStore2.storeDataAsync { TEST_MESSAGE_V2 } testCoroutineDispatchers.advanceUntilIdle() - observeCache(cacheStore2, mockUserAppHistoryObserver1) - // Similar to the previous test, except due to the in-memory update the observer will receive the latest result - // regardless of the cache priming. + // Similar to the previous test, except due to the in-memory update the observer will receive + // the latest result regardless of the cache priming. assertThat(storeOp1.isCompleted).isTrue() assertThat(storeOp2.isCompleted).isTrue() assertThat(primeOp.isCompleted).isTrue() - verify( - mockUserAppHistoryObserver1, - atLeastOnce() - ).onChanged(userAppHistoryResultCaptor1.capture()) - assertThat(userAppHistoryResultCaptor1.value.isSuccess()).isTrue() - assertThat(userAppHistoryResultCaptor1.value.getOrThrow()).isEqualTo(TEST_MESSAGE_VERSION_2) + assertThat(monitorFactory.waitForNextSuccessfulResult(cacheStore2)).isEqualTo(TEST_MESSAGE_V2) } @Test fun testCache_primed_afterStoreUpdateWithoutMemUpdate_notForced_observesOldValue() { val cacheStore = cacheFactory.create(CACHE_NAME_1, TestMessage.getDefaultInstance()) - observeCache( - cacheStore, - mockUserAppHistoryObserver1 - ) // Force initializing the store's in-memory cache + // Force initializing the store's in-memory cache + monitorFactory.waitForNextSuccessfulResult(cacheStore) - val storeOp = cacheStore.storeDataAsync(updateInMemoryCache = false) { TEST_MESSAGE_VERSION_1 } + val storeOp = cacheStore.storeDataAsync(updateInMemoryCache = false) { TEST_MESSAGE_V1 } testCoroutineDispatchers.advanceUntilIdle() val primeOp = cacheStore.primeCacheAsync(forceUpdate = false) testCoroutineDispatchers.advanceUntilIdle() - observeCache(cacheStore, mockUserAppHistoryObserver2) - // Both ops will succeed, and the observer will receive the old value due to the update not changing the in-memory - // cache, and the prime no-oping due to the cache already being initialized. + // Both ops will succeed, and the observer will receive the old value due to the update not + // changing the in-memory cache, and the prime no-oping due to the cache already being + // initialized. assertThat(storeOp.isCompleted).isTrue() assertThat(primeOp.isCompleted).isTrue() - verify( - mockUserAppHistoryObserver2, - atLeastOnce() - ).onChanged(userAppHistoryResultCaptor1.capture()) - assertThat(userAppHistoryResultCaptor1.value.isSuccess()).isTrue() - assertThat(userAppHistoryResultCaptor1.value.getOrThrow()).isEqualTo( - TestMessage.getDefaultInstance() - ) + assertThat(monitorFactory.waitForNextSuccessfulResult(cacheStore)).isEqualToDefaultInstance() } @Test fun testCache_primed_afterStoreUpdateWithoutMemoryUpdate_forced_observesNewValue() { val cacheStore = cacheFactory.create(CACHE_NAME_1, TestMessage.getDefaultInstance()) - observeCache( - cacheStore, - mockUserAppHistoryObserver1 - ) // Force initializing the store's in-memory cache + monitorFactory.waitForNextSuccessfulResult(cacheStore) - val storeOp = cacheStore.storeDataAsync(updateInMemoryCache = false) { TEST_MESSAGE_VERSION_1 } + val storeOp = cacheStore.storeDataAsync(updateInMemoryCache = false) { TEST_MESSAGE_V1 } testCoroutineDispatchers.advanceUntilIdle() val primeOp = cacheStore.primeCacheAsync(forceUpdate = true) testCoroutineDispatchers.advanceUntilIdle() - observeCache(cacheStore, mockUserAppHistoryObserver2) - // The observer will receive the new value because the prime was forced. This ensures the store's in-memory cache is - // now up-to-date with the on-disk representation. + // The observer will receive the new value because the prime was forced. This ensures the + // store's in-memory cache is now up-to-date with the on-disk representation. assertThat(storeOp.isCompleted).isTrue() assertThat(primeOp.isCompleted).isTrue() - verify( - mockUserAppHistoryObserver2, - atLeastOnce() - ).onChanged(userAppHistoryResultCaptor1.capture()) - assertThat(userAppHistoryResultCaptor1.value.isSuccess()).isTrue() - assertThat(userAppHistoryResultCaptor1.value.getOrThrow()).isEqualTo(TEST_MESSAGE_VERSION_1) + assertThat(monitorFactory.waitForNextSuccessfulResult(cacheStore)).isEqualTo(TEST_MESSAGE_V1) } @Test @@ -407,110 +307,76 @@ class PersistentCacheStoreTest { val clearOp = cacheStore.clearCacheAsync() testCoroutineDispatchers.advanceUntilIdle() - observeCache(cacheStore, mockUserAppHistoryObserver1) - // The new observer should observe the store with its default state since nothing needed to be cleared. + // The new observer should observe the store with its default state since nothing needed to be + // cleared. assertThat(clearOp.isCompleted).isTrue() - verify( - mockUserAppHistoryObserver1, - atLeastOnce() - ).onChanged(userAppHistoryResultCaptor1.capture()) - assertThat(userAppHistoryResultCaptor1.value.isSuccess()).isTrue() - assertThat(userAppHistoryResultCaptor1.value.getOrThrow()).isEqualTo( - TestMessage.getDefaultInstance() - ) + assertThat(monitorFactory.waitForNextSuccessfulResult(cacheStore)).isEqualToDefaultInstance() } @Test fun testCache_update_clear_resetsCacheToInitialState() { val cacheStore = cacheFactory.create(CACHE_NAME_1, TestMessage.getDefaultInstance()) - val storeOp = cacheStore.storeDataAsync { TEST_MESSAGE_VERSION_1 } + val storeOp = cacheStore.storeDataAsync { TEST_MESSAGE_V1 } testCoroutineDispatchers.advanceUntilIdle() val clearOp = cacheStore.clearCacheAsync() testCoroutineDispatchers.advanceUntilIdle() - observeCache(cacheStore, mockUserAppHistoryObserver1) // The new observer should observe that the store is cleared. assertThat(storeOp.isCompleted).isTrue() assertThat(clearOp.isCompleted).isTrue() - verify( - mockUserAppHistoryObserver1, - atLeastOnce() - ).onChanged(userAppHistoryResultCaptor1.capture()) - assertThat(userAppHistoryResultCaptor1.value.isSuccess()).isTrue() - assertThat(userAppHistoryResultCaptor1.value.getOrThrow()).isEqualTo( - TestMessage.getDefaultInstance() - ) + assertThat(monitorFactory.waitForNextSuccessfulResult(cacheStore)).isEqualToDefaultInstance() } @Test fun testCache_update_existingObserver_clear_isNotifiedOfClear() { val cacheStore = cacheFactory.create(CACHE_NAME_1, TestMessage.getDefaultInstance()) - val storeOp = cacheStore.storeDataAsync { TEST_MESSAGE_VERSION_1 } + val storeOp = cacheStore.storeDataAsync { TEST_MESSAGE_V1 } testCoroutineDispatchers.advanceUntilIdle() - observeCache(cacheStore, mockUserAppHistoryObserver1) - reset(mockUserAppHistoryObserver1) + val monitor = monitorFactory.createMonitor(cacheStore) + monitor.waitForNextSuccessResult() val clearOp = cacheStore.clearCacheAsync() testCoroutineDispatchers.advanceUntilIdle() // The observer should not be notified the cache was cleared. assertThat(storeOp.isCompleted).isTrue() assertThat(clearOp.isCompleted).isTrue() - verify( - mockUserAppHistoryObserver1, - atLeastOnce() - ).onChanged(userAppHistoryResultCaptor1.capture()) - assertThat(userAppHistoryResultCaptor1.value.isSuccess()).isTrue() - assertThat(userAppHistoryResultCaptor1.value.getOrThrow()).isEqualTo( - TestMessage.getDefaultInstance() - ) + monitor.verifyProviderIsNotUpdated() } @Test fun testCache_update_newCache_observesInitialState() { val cacheStore1 = cacheFactory.create(CACHE_NAME_1, TestMessage.getDefaultInstance()) - val storeOp = cacheStore1.storeDataAsync { TEST_MESSAGE_VERSION_1 } + val storeOp = cacheStore1.storeDataAsync { TEST_MESSAGE_V1 } testCoroutineDispatchers.advanceUntilIdle() val clearOp = cacheStore1.clearCacheAsync() testCoroutineDispatchers.advanceUntilIdle() - val cacheStore2 = cacheFactory.create(CACHE_NAME_1, TEST_MESSAGE_VERSION_2) - observeCache(cacheStore2, mockUserAppHistoryObserver1) + val cacheStore2 = cacheFactory.create(CACHE_NAME_1, TEST_MESSAGE_V2) - // The new observer should observe that there's no persisted on-disk store since it has a different default value - // that would only be used if there wasn't already on-disk storage. + // The new observer should observe that there's no persisted on-disk store since it has a + // different default value that would only be used if there wasn't already on-disk storage. assertThat(storeOp.isCompleted).isTrue() assertThat(clearOp.isCompleted).isTrue() - verify( - mockUserAppHistoryObserver1, - atLeastOnce() - ).onChanged(userAppHistoryResultCaptor1.capture()) - assertThat(userAppHistoryResultCaptor1.value.isSuccess()).isTrue() - assertThat(userAppHistoryResultCaptor1.value.getOrThrow()).isEqualTo(TEST_MESSAGE_VERSION_2) + assertThat(monitorFactory.waitForNextSuccessfulResult(cacheStore2)).isEqualTo(TEST_MESSAGE_V2) } @Test fun testMultipleCaches_oneUpdates_newCacheSameNameDiffInit_observesUpdatedValue() { val cacheStore1 = cacheFactory.create(CACHE_NAME_1, TestMessage.getDefaultInstance()) - val storeOp = cacheStore1.storeDataAsync { TEST_MESSAGE_VERSION_1 } + val storeOp = cacheStore1.storeDataAsync { TEST_MESSAGE_V1 } testCoroutineDispatchers.advanceUntilIdle() - val cacheStore2 = cacheFactory.create(CACHE_NAME_1, TEST_MESSAGE_VERSION_2) - observeCache(cacheStore2, mockUserAppHistoryObserver1) + val cacheStore2 = cacheFactory.create(CACHE_NAME_1, TEST_MESSAGE_V2) - // The new cache should observe the updated on-disk value rather than its new default since an on-disk value exists. - // This isn't a very realistic test since all caches should use default proto instances for initialization, but it's - // a possible edge case that should at least have established behavior that can be adjusted later if it isn't - // desirable in some circumstances. + // The new cache should observe the updated on-disk value rather than its new default since an + // on-disk value exists. This isn't a very realistic test since all caches should use default + // proto instances for initialization, but it's a possible edge case that should at least have + // established behavior that can be adjusted later if it isn't desirable in some circumstances. assertThat(storeOp.isCompleted).isTrue() - verify( - mockUserAppHistoryObserver1, - atLeastOnce() - ).onChanged(userAppHistoryResultCaptor1.capture()) - assertThat(userAppHistoryResultCaptor1.value.isSuccess()).isTrue() - assertThat(userAppHistoryResultCaptor1.value.getOrThrow()).isEqualTo(TEST_MESSAGE_VERSION_1) + assertThat(monitorFactory.waitForNextSuccessfulResult(cacheStore2)).isEqualTo(TEST_MESSAGE_V1) } @Test @@ -518,85 +384,58 @@ class PersistentCacheStoreTest { val cacheStore1 = cacheFactory.create(CACHE_NAME_1, TestMessage.getDefaultInstance()) val cacheStore2 = cacheFactory.create(CACHE_NAME_2, TestMessage.getDefaultInstance()) - observeCache(cacheStore1, mockUserAppHistoryObserver1) - observeCache(cacheStore2, mockUserAppHistoryObserver2) - reset(mockUserAppHistoryObserver2) - val storeOp = cacheStore1.storeDataAsync { TEST_MESSAGE_VERSION_1 } + val monitor1 = monitorFactory.createMonitor(cacheStore1) + val monitor2 = monitorFactory.createMonitor(cacheStore2) + val storeOp = cacheStore1.storeDataAsync { TEST_MESSAGE_V1 } testCoroutineDispatchers.advanceUntilIdle() - // The observer of the second store will be not notified of the change to the first store since they have different - // names. + // The observer of the second store will be not notified of the change to the first store since + // they have different names. assertThat(storeOp.isCompleted).isTrue() - verifyZeroInteractions(mockUserAppHistoryObserver2) + monitor1.verifyProviderIsNotUpdated() + monitor2.verifyProviderIsNotUpdated() } @Test fun testMultipleCaches_diffNames_oneUpdates_cachesRecreated_onlyOneObservesVal() { val cacheStore1a = cacheFactory.create(CACHE_NAME_1, TestMessage.getDefaultInstance()) cacheFactory.create(CACHE_NAME_2, TestMessage.getDefaultInstance()) - val storeOp = cacheStore1a.storeDataAsync { TEST_MESSAGE_VERSION_1 } + val storeOp = cacheStore1a.storeDataAsync { TEST_MESSAGE_V1 } testCoroutineDispatchers.advanceUntilIdle() // Recreate the stores and observe them. val cacheStore1b = cacheFactory.create(CACHE_NAME_1, TestMessage.getDefaultInstance()) val cacheStore2b = cacheFactory.create(CACHE_NAME_2, TestMessage.getDefaultInstance()) - observeCache(cacheStore1b, mockUserAppHistoryObserver1) - observeCache(cacheStore2b, mockUserAppHistoryObserver2) - // Only the observer of the first cache should notice the update since they are different caches. + // Only the observer of the first cache should notice the update since they are different + // caches. assertThat(storeOp.isCompleted).isTrue() - verify( - mockUserAppHistoryObserver1, - atLeastOnce() - ).onChanged(userAppHistoryResultCaptor1.capture()) - verify( - mockUserAppHistoryObserver2, - atLeastOnce() - ).onChanged(userAppHistoryResultCaptor2.capture()) - assertThat(userAppHistoryResultCaptor1.value.isSuccess()).isTrue() - assertThat(userAppHistoryResultCaptor1.value.getOrThrow()).isEqualTo(TEST_MESSAGE_VERSION_1) - assertThat(userAppHistoryResultCaptor2.value.isSuccess()).isTrue() - assertThat(userAppHistoryResultCaptor2.value.getOrThrow()).isEqualTo( - TestMessage.getDefaultInstance() - ) + assertThat(monitorFactory.waitForNextSuccessfulResult(cacheStore1b)).isEqualTo(TEST_MESSAGE_V1) + assertThat(monitorFactory.waitForNextSuccessfulResult(cacheStore2b)).isEqualToDefaultInstance() } @Test fun testNewCache_fileCorrupted_providesError() { val cacheStore1 = cacheFactory.create(CACHE_NAME_1, TestMessage.getDefaultInstance()) - val storeOp = cacheStore1.storeDataAsync { TEST_MESSAGE_VERSION_1 } + val storeOp = cacheStore1.storeDataAsync { TEST_MESSAGE_V1 } testCoroutineDispatchers.advanceUntilIdle() // Simulate the file being corrupted & reopen the file in a new store. corruptFileCache(CACHE_NAME_1) val cacheStore2 = cacheFactory.create(CACHE_NAME_1, TestMessage.getDefaultInstance()) - observeCache(cacheStore2, mockUserAppHistoryObserver1) // The new observer should receive an IOException error when trying to read the file. assertThat(storeOp.isCompleted).isTrue() - verify( - mockUserAppHistoryObserver1, - atLeastOnce() - ).onChanged(userAppHistoryResultCaptor1.capture()) - assertThat(userAppHistoryResultCaptor1.value.isFailure()).isTrue() - assertThat(userAppHistoryResultCaptor1.value.getErrorOrNull()).isInstanceOf( - IOException::class.java - ) - } - - private fun observeCache( - cacheStore: PersistentCacheStore, - observer: Observer> - ) { - cacheStore.toLiveData().observeForever(observer) - testCoroutineDispatchers.advanceUntilIdle() + val error = monitorFactory.waitForNextFailureResult(cacheStore2) + assertThat(error).isInstanceOf(IOException::class.java) } private fun corruptFileCache(cacheName: String) { - // NB: This is unfortunately tied to the implementation details of PersistentCacheStore. If this ends up being an - // issue, the store should be updated to call into a file path provider that can also be used in this test to - // retrieve the file cache. This may also be needed for downstream profile work if per-profile data stores are done - // via subdirectories or altered filenames. + // NB: This is unfortunately tied to the implementation details of PersistentCacheStore. If this + // ends up being an issue, the store should be updated to call into a file path provider that + // can also be used in this test to retrieve the file cache. This may also be needed for + // downstream profile work if per-profile data stores are done via subdirectories or altered + // filenames. val cacheFileName = "$cacheName.cache" val cacheFile = File( ApplicationProvider.getApplicationContext().filesDir, cacheFileName diff --git a/domain/BUILD.bazel b/domain/BUILD.bazel index f60e901cf58..1becc4af6f6 100755 --- a/domain/BUILD.bazel +++ b/domain/BUILD.bazel @@ -190,6 +190,7 @@ TEST_DEPS = [ "//domain/src/main/java/org/oppia/android/domain/oppialogger/loguploader:worker_module", "//domain/src/main/java/org/oppia/android/domain/testing/oppialogger/loguploader:fake_log_uploader", "//testing", + "//testing/src/main/java/org/oppia/android/testing/data:async_result_subject", "//testing/src/main/java/org/oppia/android/testing/data:data_provider_test_monitor", "//testing/src/main/java/org/oppia/android/testing/network:network", "//testing/src/main/java/org/oppia/android/testing/network:test_module", diff --git a/domain/src/main/java/org/oppia/android/domain/audio/AudioPlayerController.kt b/domain/src/main/java/org/oppia/android/domain/audio/AudioPlayerController.kt index 8e4e8c14f67..de43ecd963c 100644 --- a/domain/src/main/java/org/oppia/android/domain/audio/AudioPlayerController.kt +++ b/domain/src/main/java/org/oppia/android/domain/audio/AudioPlayerController.kt @@ -40,7 +40,7 @@ class AudioPlayerController @Inject constructor( ) { inner class AudioMutableLiveData : - MutableLiveData>(AsyncResult.pending()) { + MutableLiveData>(AsyncResult.Pending()) { override fun onActive() { super.onActive() audioLock.withLock { @@ -128,18 +128,17 @@ class AudioPlayerController @Inject constructor( completed = true stopUpdatingSeekBar() playProgress?.value = - AsyncResult.success(PlayProgress(PlayStatus.COMPLETED, 0, duration)) + AsyncResult.Success(PlayProgress(PlayStatus.COMPLETED, 0, duration)) } mediaPlayer.setOnPreparedListener { prepared = true duration = it.duration playProgress?.value = - AsyncResult.success(PlayProgress(PlayStatus.PREPARED, 0, duration)) + AsyncResult.Success(PlayProgress(PlayStatus.PREPARED, 0, duration)) } mediaPlayer.setOnErrorListener { _, what, extra -> playProgress?.value = - AsyncResult.failed( - AudioPlayerException("Audio Player put in error state with what: $what and extra: $extra") + AsyncResult.Failure(AudioPlayerException("Audio Player put in error state with what: $what and extra: $extra") ) releaseMediaPlayer() initializeMediaPlayer() @@ -186,7 +185,7 @@ class AudioPlayerController @Inject constructor( exceptionsController.logNonFatalException(e) oppiaLogger.e("AudioPlayerController", "Failed to set data source for media player", e) } - playProgress?.value = AsyncResult.pending() + playProgress?.value = AsyncResult.Pending() } /** @@ -212,8 +211,7 @@ class AudioPlayerController @Inject constructor( check(prepared) { "Media Player not in a prepared state" } if (mediaPlayer.isPlaying) { playProgress?.value = - AsyncResult.success( - PlayProgress(PlayStatus.PAUSED, mediaPlayer.currentPosition, duration) + AsyncResult.Success(PlayProgress(PlayStatus.PAUSED, mediaPlayer.currentPosition, duration) ) mediaPlayer.pause() stopUpdatingSeekBar() @@ -239,8 +237,7 @@ class AudioPlayerController @Inject constructor( val position = if (completed) 0 else mediaPlayer.currentPosition completed = false playProgress?.postValue( - AsyncResult.success( - PlayProgress(PlayStatus.PLAYING, position, mediaPlayer.duration) + AsyncResult.Success(PlayProgress(PlayStatus.PLAYING, position, mediaPlayer.duration) ) ) } diff --git a/domain/src/main/java/org/oppia/android/domain/devoptions/ModifyLessonProgressController.kt b/domain/src/main/java/org/oppia/android/domain/devoptions/ModifyLessonProgressController.kt index c9f5aad125d..47cec594855 100644 --- a/domain/src/main/java/org/oppia/android/domain/devoptions/ModifyLessonProgressController.kt +++ b/domain/src/main/java/org/oppia/android/domain/devoptions/ModifyLessonProgressController.kt @@ -8,13 +8,13 @@ import org.oppia.android.app.model.TopicProgress import org.oppia.android.domain.topic.StoryProgressController import org.oppia.android.domain.topic.TopicController import org.oppia.android.domain.topic.TopicListController -import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.data.DataProvider import org.oppia.android.util.data.DataProviders.Companion.combineWith import org.oppia.android.util.data.DataProviders.Companion.transformAsync import org.oppia.android.util.system.OppiaClock import javax.inject.Inject import javax.inject.Singleton +import org.oppia.android.util.data.AsyncResult private const val GET_ALL_TOPICS_PROVIDER_ID = "get_all_topics_provider_id" private const val GET_ALL_TOPICS_COMBINED_PROVIDER_ID = "get_all_topics_combined_provider_id" @@ -44,7 +44,7 @@ class ModifyLessonProgressController @Inject constructor( val topicId = topicSummary.topicId listOfTopics.add(topicController.retrieveTopic(topicId)) } - AsyncResult.success(listOfTopics.toList()) + AsyncResult.Success(listOfTopics.toList()) } val topicProgressListDataProvider = storyProgressController.retrieveTopicProgressListDataProvider(profileId) @@ -71,7 +71,7 @@ class ModifyLessonProgressController @Inject constructor( listOfTopics.forEach { topic -> storyMap[topic.topicId] = topic.storyList } - AsyncResult.success(storyMap.toMap()) + AsyncResult.Success(storyMap.toMap()) } } diff --git a/domain/src/main/java/org/oppia/android/domain/exploration/ExplorationDataController.kt b/domain/src/main/java/org/oppia/android/domain/exploration/ExplorationDataController.kt index 62657e1387d..e4780f88f30 100644 --- a/domain/src/main/java/org/oppia/android/domain/exploration/ExplorationDataController.kt +++ b/domain/src/main/java/org/oppia/android/domain/exploration/ExplorationDataController.kt @@ -1,7 +1,5 @@ package org.oppia.android.domain.exploration -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData import org.oppia.android.app.model.Exploration import org.oppia.android.app.model.ExplorationCheckpoint import org.oppia.android.app.model.ProfileId @@ -125,10 +123,10 @@ class ExplorationDataController @Inject constructor( @Suppress("RedundantSuspendModifier") private suspend fun retrieveExplorationById(explorationId: String): AsyncResult { return try { - AsyncResult.success(explorationRetriever.loadExploration(explorationId)) + AsyncResult.Success(explorationRetriever.loadExploration(explorationId)) } catch (e: Exception) { exceptionsController.logNonFatalException(e) - AsyncResult.failed(e) + AsyncResult.Failure(e) } } } diff --git a/domain/src/main/java/org/oppia/android/domain/exploration/ExplorationProgressController.kt b/domain/src/main/java/org/oppia/android/domain/exploration/ExplorationProgressController.kt index 19ec0d29f29..7935afb9155 100644 --- a/domain/src/main/java/org/oppia/android/domain/exploration/ExplorationProgressController.kt +++ b/domain/src/main/java/org/oppia/android/domain/exploration/ExplorationProgressController.kt @@ -214,11 +214,11 @@ class ExplorationProgressController @Inject constructor( asyncDataSubscriptionManager.notifyChangeAsync(CURRENT_STATE_DATA_PROVIDER_ID) - return MutableLiveData(AsyncResult.success(answerOutcome)) + return MutableLiveData(AsyncResult.Success(answerOutcome)) } } catch (e: Exception) { exceptionsController.logNonFatalException(e) - return MutableLiveData(AsyncResult.failed(e)) + return MutableLiveData(AsyncResult.Failure(e)) } } @@ -260,11 +260,11 @@ class ExplorationProgressController @Inject constructor( explorationProgress.advancePlayStageTo(ExplorationProgress.PlayStage.VIEWING_STATE) } asyncDataSubscriptionManager.notifyChangeAsync(CURRENT_STATE_DATA_PROVIDER_ID) - return MutableLiveData(AsyncResult.success(null)) + return MutableLiveData(AsyncResult.Success(null)) } } catch (e: Exception) { exceptionsController.logNonFatalException(e) - return MutableLiveData(AsyncResult.failed(e)) + return MutableLiveData(AsyncResult.Failure(e)) } } @@ -305,11 +305,11 @@ class ExplorationProgressController @Inject constructor( } asyncDataSubscriptionManager.notifyChangeAsync(CURRENT_STATE_DATA_PROVIDER_ID) - return MutableLiveData(AsyncResult.success(null)) + return MutableLiveData(AsyncResult.Success(null)) } } catch (e: Exception) { exceptionsController.logNonFatalException(e) - return MutableLiveData(AsyncResult.failed(e)) + return MutableLiveData(AsyncResult.Failure(e)) } } @@ -349,10 +349,10 @@ class ExplorationProgressController @Inject constructor( explorationProgress.stateDeck.navigateToPreviousState() asyncDataSubscriptionManager.notifyChangeAsync(CURRENT_STATE_DATA_PROVIDER_ID) } - return MutableLiveData(AsyncResult.success(null)) + return MutableLiveData(AsyncResult.Success(null)) } catch (e: Exception) { exceptionsController.logNonFatalException(e) - return MutableLiveData(AsyncResult.failed(e)) + return MutableLiveData(AsyncResult.Failure(e)) } } @@ -403,10 +403,10 @@ class ExplorationProgressController @Inject constructor( } asyncDataSubscriptionManager.notifyChangeAsync(CURRENT_STATE_DATA_PROVIDER_ID) } - return MutableLiveData(AsyncResult.success(null)) + return MutableLiveData(AsyncResult.Success(null)) } catch (e: Exception) { exceptionsController.logNonFatalException(e) - return MutableLiveData(AsyncResult.failed(e)) + return MutableLiveData(AsyncResult.Failure(e)) } } @@ -446,7 +446,7 @@ class ExplorationProgressController @Inject constructor( retrieveCurrentStateWithinCacheAsync(writtenTranslationContentLocale) } catch (e: Exception) { exceptionsController.logNonFatalException(e) - AsyncResult.failed(e) + AsyncResult.Failure(e) } } @@ -476,20 +476,20 @@ class ExplorationProgressController @Inject constructor( " to ${explorationProgress.currentExplorationId}" } return when (explorationProgress.playStage) { - ExplorationProgress.PlayStage.NOT_PLAYING -> AsyncResult.pending() + ExplorationProgress.PlayStage.NOT_PLAYING -> AsyncResult.Pending() ExplorationProgress.PlayStage.LOADING_EXPLORATION -> { try { // The exploration must be available for this stage since it was loaded above. finishLoadExploration(exploration!!, explorationProgress) - AsyncResult.success(computeCurrentEphemeralState(writtenTranslationContentLocale)) + AsyncResult.Success(computeCurrentEphemeralState(writtenTranslationContentLocale)) } catch (e: Exception) { exceptionsController.logNonFatalException(e) - AsyncResult.failed(e) + AsyncResult.Failure(e) } } ExplorationProgress.PlayStage.VIEWING_STATE -> - AsyncResult.success(computeCurrentEphemeralState(writtenTranslationContentLocale)) - ExplorationProgress.PlayStage.SUBMITTING_ANSWER -> AsyncResult.pending() + AsyncResult.Success(computeCurrentEphemeralState(writtenTranslationContentLocale)) + ExplorationProgress.PlayStage.SUBMITTING_ANSWER -> AsyncResult.Pending() } } } diff --git a/domain/src/main/java/org/oppia/android/domain/exploration/lightweightcheckpointing/ExplorationCheckpointController.kt b/domain/src/main/java/org/oppia/android/domain/exploration/lightweightcheckpointing/ExplorationCheckpointController.kt index f86d1402d55..3dd8ed69756 100644 --- a/domain/src/main/java/org/oppia/android/domain/exploration/lightweightcheckpointing/ExplorationCheckpointController.kt +++ b/domain/src/main/java/org/oppia/android/domain/exploration/lightweightcheckpointing/ExplorationCheckpointController.kt @@ -134,7 +134,7 @@ class ExplorationCheckpointController @Inject constructor( return dataProviders.createInMemoryDataProviderAsync( RECORD_EXPLORATION_CHECKPOINT_DATA_PROVIDER_ID ) { - return@createInMemoryDataProviderAsync AsyncResult.success(deferred.await()) + return@createInMemoryDataProviderAsync AsyncResult.Success(deferred.await()) } } @@ -153,22 +153,20 @@ class ExplorationCheckpointController @Inject constructor( when { checkpoint != null && exploration.version == checkpoint.explorationVersion -> { - AsyncResult.success(checkpoint) + AsyncResult.Success(checkpoint) } checkpoint != null && exploration.version != checkpoint.explorationVersion -> { - AsyncResult.failed( - OutdatedExplorationCheckpointException( - "checkpoint with version: ${checkpoint.explorationVersion} cannot be used to " + - "resume exploration $explorationId with version: ${exploration.version}" - ) + AsyncResult.Failure(OutdatedExplorationCheckpointException( + "checkpoint with version: ${checkpoint.explorationVersion} cannot be used to " + + "resume exploration $explorationId with version: ${exploration.version}" + ) ) } else -> { - AsyncResult.failed( - ExplorationCheckpointNotFoundException( - "Checkpoint with the explorationId $explorationId was not found " + - "for profileId ${profileId.internalId}." - ) + AsyncResult.Failure(ExplorationCheckpointNotFoundException( + "Checkpoint with the explorationId $explorationId was not found " + + "for profileId ${profileId.internalId}." + ) ) } } @@ -201,12 +199,11 @@ class ExplorationCheckpointController @Inject constructor( .setExplorationTitle(oldestCheckpoint.value.explorationTitle) .setExplorationVersion(oldestCheckpoint.value.explorationVersion) .build() - AsyncResult.success(explorationCheckpointDetails) + AsyncResult.Success(explorationCheckpointDetails) } else { - AsyncResult.failed( - ExplorationCheckpointNotFoundException( - "No saved checkpoints in $CACHE_NAME for profileId ${profileId.internalId}." - ) + AsyncResult.Failure(ExplorationCheckpointNotFoundException( + "No saved checkpoints in $CACHE_NAME for profileId ${profileId.internalId}." + ) ) } } @@ -256,14 +253,12 @@ class ExplorationCheckpointController @Inject constructor( ): AsyncResult { return when (deferred.await()) { ExplorationCheckpointActionStatus.CHECKPOINT_NOT_FOUND -> - AsyncResult.failed( - ExplorationCheckpointNotFoundException( - "No saved checkpoint with explorationId ${explorationId!!} found for " + - "the profileId ${profileId!!.internalId}." - ) + AsyncResult.Failure(ExplorationCheckpointNotFoundException( + "No saved checkpoint with explorationId ${explorationId!!} found for " + + "the profileId ${profileId!!.internalId}." + ) ) - ExplorationCheckpointActionStatus.SUCCESS -> - AsyncResult.success(null) + ExplorationCheckpointActionStatus.SUCCESS -> AsyncResult.Success(null) } } diff --git a/domain/src/main/java/org/oppia/android/domain/locale/LocaleController.kt b/domain/src/main/java/org/oppia/android/domain/locale/LocaleController.kt index ec90dae8f02..f565d68c1d3 100644 --- a/domain/src/main/java/org/oppia/android/domain/locale/LocaleController.kt +++ b/domain/src/main/java/org/oppia/android/domain/locale/LocaleController.kt @@ -215,9 +215,8 @@ class LocaleController @Inject constructor( fun retrieveSystemLanguage(): DataProvider { val providerId = SYSTEM_LANGUAGE_DATA_PROVIDER_ID return getSystemLocaleProfile().transformAsync(providerId) { systemLocaleProfile -> - AsyncResult.success( - retrieveLanguageDefinitionFromSystemCode(systemLocaleProfile)?.language - ?: OppiaLanguage.LANGUAGE_UNSPECIFIED + AsyncResult.Success(retrieveLanguageDefinitionFromSystemCode(systemLocaleProfile)?.language + ?: OppiaLanguage.LANGUAGE_UNSPECIFIED ) } } @@ -303,11 +302,10 @@ class LocaleController @Inject constructor( @Suppress("UNCHECKED_CAST") // as? should always be a safe cast, even if unchecked. val locale = computeLocale(language, systemLocaleProfile, usageMode) as? T return locale?.let { - AsyncResult.success(it) - } ?: AsyncResult.failed( - IllegalStateException( - "Language $language for usage $usageMode doesn't match supported language definitions" - ) + AsyncResult.Success(it) + } ?: AsyncResult.Failure(IllegalStateException( + "Language $language for usage $usageMode doesn't match supported language definitions" + ) ) } diff --git a/domain/src/main/java/org/oppia/android/domain/platformparameter/PlatformParameterController.kt b/domain/src/main/java/org/oppia/android/domain/platformparameter/PlatformParameterController.kt index 7fe6d744842..7298fe27e61 100644 --- a/domain/src/main/java/org/oppia/android/domain/platformparameter/PlatformParameterController.kt +++ b/domain/src/main/java/org/oppia/android/domain/platformparameter/PlatformParameterController.kt @@ -84,7 +84,7 @@ class PlatformParameterController @Inject constructor( deferred: Deferred ): AsyncResult { return when (deferred.await()) { - PlatformParameterCachingStatus.SUCCESS -> AsyncResult.success(null) + PlatformParameterCachingStatus.SUCCESS -> AsyncResult.Success(null) } } diff --git a/domain/src/main/java/org/oppia/android/domain/platformparameter/syncup/PlatformParameterSyncUpWorker.kt b/domain/src/main/java/org/oppia/android/domain/platformparameter/syncup/PlatformParameterSyncUpWorker.kt index 83bb3d4b274..4b21480d08b 100644 --- a/domain/src/main/java/org/oppia/android/domain/platformparameter/syncup/PlatformParameterSyncUpWorker.kt +++ b/domain/src/main/java/org/oppia/android/domain/platformparameter/syncup/PlatformParameterSyncUpWorker.kt @@ -22,6 +22,7 @@ import retrofit2.Response import java.lang.IllegalArgumentException import java.lang.IllegalStateException import javax.inject.Inject +import org.oppia.android.util.data.AsyncResult /** Worker class that fetches and caches the latest platform parameters from the remote service. */ class PlatformParameterSyncUpWorker private constructor( @@ -114,8 +115,8 @@ class PlatformParameterSyncUpWorker private constructor( val cachingResult = platformParameterController .updatePlatformParameterDatabase(platformParameterList) .retrieveData() - if (cachingResult.isFailure()) { - throw IllegalStateException(cachingResult.getErrorOrNull()) + if (cachingResult is AsyncResult.Failure) { + throw IllegalStateException(cachingResult.error) } Result.success() } else { diff --git a/domain/src/main/java/org/oppia/android/domain/profile/ProfileManagementController.kt b/domain/src/main/java/org/oppia/android/domain/profile/ProfileManagementController.kt index 85de12651d3..f9a9e1dfb09 100644 --- a/domain/src/main/java/org/oppia/android/domain/profile/ProfileManagementController.kt +++ b/domain/src/main/java/org/oppia/android/domain/profile/ProfileManagementController.kt @@ -144,13 +144,12 @@ class ProfileManagementController @Inject constructor( return profileDataStore.transformAsync(GET_PROFILE_PROVIDER_ID) { val profile = it.profilesMap[profileId.internalId] if (profile != null) { - AsyncResult.success(profile) + AsyncResult.Success(profile) } else { - AsyncResult.failed( - ProfileNotFoundException( - "ProfileId ${profileId.internalId} does" + - " not match an existing Profile" - ) + AsyncResult.Failure(ProfileNotFoundException( + "ProfileId ${profileId.internalId} does" + + " not match an existing Profile" + ) ) } } @@ -160,7 +159,7 @@ class ProfileManagementController @Inject constructor( fun getWasProfileEverAdded(): DataProvider { return profileDataStore.transformAsync(GET_WAS_PROFILE_EVER_ADDED_PROVIDER_ID) { val wasProfileEverAdded = it.wasProfileEverAdded - AsyncResult.success(wasProfileEverAdded) + AsyncResult.Success(wasProfileEverAdded) } } @@ -169,9 +168,9 @@ class ProfileManagementController @Inject constructor( return profileDataStore.transformAsync(GET_DEVICE_SETTINGS_PROVIDER_ID) { val deviceSettings = it.deviceSettings if (deviceSettings != null) { - AsyncResult.success(deviceSettings) + AsyncResult.Success(deviceSettings) } else { - AsyncResult.failed(DeviceSettingsNotFoundException("Device Settings not found.")) + AsyncResult.Failure(DeviceSettingsNotFoundException("Device Settings not found.")) } } } @@ -578,13 +577,12 @@ class ProfileManagementController @Inject constructor( val profileDatabase = profileDataStore.readDataAsync().await() if (profileDatabase.profilesMap.containsKey(profileId.internalId)) { currentProfileId = profileId.internalId - return@createInMemoryDataProviderAsync AsyncResult.success(0) + return@createInMemoryDataProviderAsync AsyncResult.Success(0) } - AsyncResult.failed( - ProfileNotFoundException( - "ProfileId ${profileId.internalId} is" + - " not associated with an existing profile" - ) + AsyncResult.Failure(ProfileNotFoundException( + "ProfileId ${profileId.internalId} is" + + " not associated with an existing profile" + ) ) } } @@ -643,40 +641,32 @@ class ProfileManagementController @Inject constructor( deferred: Deferred ): AsyncResult { return when (deferred.await()) { - ProfileActionStatus.SUCCESS -> AsyncResult.success(null) - ProfileActionStatus.INVALID_PROFILE_NAME -> AsyncResult.failed( - ProfileNameOnlyLettersException("$name does not contain only letters") + ProfileActionStatus.SUCCESS -> AsyncResult.Success(null) + ProfileActionStatus.INVALID_PROFILE_NAME -> AsyncResult.Failure(ProfileNameOnlyLettersException("$name does not contain only letters") ) - ProfileActionStatus.PROFILE_NAME_NOT_UNIQUE -> AsyncResult.failed( - ProfileNameNotUniqueException("$name is not unique to other profiles") + ProfileActionStatus.PROFILE_NAME_NOT_UNIQUE -> AsyncResult.Failure(ProfileNameNotUniqueException("$name is not unique to other profiles") ) - ProfileActionStatus.FAILED_TO_STORE_IMAGE -> AsyncResult.failed( - FailedToStoreImageException( - "Failed to store user's selected avatar image" - ) + ProfileActionStatus.FAILED_TO_STORE_IMAGE -> AsyncResult.Failure(FailedToStoreImageException( + "Failed to store user's selected avatar image" ) - ProfileActionStatus.FAILED_TO_GENERATE_GRAVATAR -> AsyncResult.failed( - FailedToGenerateGravatarException("Failed to generate a gravatar url") ) - ProfileActionStatus.FAILED_TO_DELETE_DIR -> AsyncResult.failed( - FailedToDeleteDirException( - "Failed to delete directory with ${profileId?.internalId}" - ) + ProfileActionStatus.FAILED_TO_GENERATE_GRAVATAR -> AsyncResult.Failure(FailedToGenerateGravatarException("Failed to generate a gravatar url") ) - ProfileActionStatus.PROFILE_NOT_FOUND -> AsyncResult.failed( - ProfileNotFoundException( - "ProfileId ${profileId?.internalId} does not match an existing Profile" - ) + ProfileActionStatus.FAILED_TO_DELETE_DIR -> AsyncResult.Failure(FailedToDeleteDirException( + "Failed to delete directory with ${profileId?.internalId}" ) - ProfileActionStatus.PROFILE_NOT_ADMIN -> AsyncResult.failed( - ProfileNotAdminException( - "ProfileId ${profileId?.internalId} does not match an existing admin" - ) ) - ProfileActionStatus.PROFILE_ALREADY_HAS_ADMIN -> AsyncResult.failed( - ProfileAlreadyHasAdminException( - "Profile cannot be an admin" - ) + ProfileActionStatus.PROFILE_NOT_FOUND -> AsyncResult.Failure(ProfileNotFoundException( + "ProfileId ${profileId?.internalId} does not match an existing Profile" + ) + ) + ProfileActionStatus.PROFILE_NOT_ADMIN -> AsyncResult.Failure(ProfileNotAdminException( + "ProfileId ${profileId?.internalId} does not match an existing admin" + ) + ) + ProfileActionStatus.PROFILE_ALREADY_HAS_ADMIN -> AsyncResult.Failure(ProfileAlreadyHasAdminException( + "Profile cannot be an admin" + ) ) } } diff --git a/domain/src/main/java/org/oppia/android/domain/question/QuestionAssessmentProgressController.kt b/domain/src/main/java/org/oppia/android/domain/question/QuestionAssessmentProgressController.kt index 476b1c76b88..e90fa5286e0 100644 --- a/domain/src/main/java/org/oppia/android/domain/question/QuestionAssessmentProgressController.kt +++ b/domain/src/main/java/org/oppia/android/domain/question/QuestionAssessmentProgressController.kt @@ -200,11 +200,11 @@ class QuestionAssessmentProgressController @Inject constructor( asyncDataSubscriptionManager.notifyChangeAsync(CREATE_CURRENT_QUESTION_DATA_PROVIDER_ID) - return MutableLiveData(AsyncResult.success(answeredQuestionOutcome)) + return MutableLiveData(AsyncResult.Success(answeredQuestionOutcome)) } } catch (e: Exception) { exceptionsController.logNonFatalException(e) - return MutableLiveData(AsyncResult.failed(e)) + return MutableLiveData(AsyncResult.Failure(e)) } } @@ -238,11 +238,11 @@ class QuestionAssessmentProgressController @Inject constructor( progress.advancePlayStageTo(TrainStage.VIEWING_STATE) } asyncDataSubscriptionManager.notifyChangeAsync(CREATE_CURRENT_QUESTION_DATA_PROVIDER_ID) - return MutableLiveData(AsyncResult.success(null)) + return MutableLiveData(AsyncResult.Success(null)) } } catch (e: Exception) { exceptionsController.logNonFatalException(e) - return MutableLiveData(AsyncResult.failed(e)) + return MutableLiveData(AsyncResult.Failure(e)) } } @@ -275,11 +275,11 @@ class QuestionAssessmentProgressController @Inject constructor( } asyncDataSubscriptionManager.notifyChangeAsync(CREATE_CURRENT_QUESTION_DATA_PROVIDER_ID) - return MutableLiveData(AsyncResult.success(null)) + return MutableLiveData(AsyncResult.Success(null)) } } catch (e: Exception) { exceptionsController.logNonFatalException(e) - return MutableLiveData(AsyncResult.failed(e)) + return MutableLiveData(AsyncResult.Failure(e)) } } @@ -316,10 +316,10 @@ class QuestionAssessmentProgressController @Inject constructor( } asyncDataSubscriptionManager.notifyChangeAsync(CREATE_CURRENT_QUESTION_DATA_PROVIDER_ID) } - return MutableLiveData(AsyncResult.success(null)) + return MutableLiveData(AsyncResult.Success(null)) } catch (e: Exception) { exceptionsController.logNonFatalException(e) - return MutableLiveData(AsyncResult.failed(e)) + return MutableLiveData(AsyncResult.Failure(e)) } } @@ -380,7 +380,7 @@ class QuestionAssessmentProgressController @Inject constructor( progressLock.withLock { val scoreCalculator = scoreCalculatorFactory.create(skillIdList, progress.questionSessionMetrics) - return AsyncResult.success(scoreCalculator.computeAll()) + return AsyncResult.Success(scoreCalculator.computeAll()) } } @@ -400,24 +400,24 @@ class QuestionAssessmentProgressController @Inject constructor( progressLock.withLock { return try { when (progress.trainStage) { - TrainStage.NOT_IN_TRAINING_SESSION -> AsyncResult.pending() + TrainStage.NOT_IN_TRAINING_SESSION -> AsyncResult.Pending() TrainStage.LOADING_TRAINING_SESSION -> { // If the assessment hasn't yet been initialized, initialize it // now that a list of questions is available. initializeAssessment(questionsList) progress.advancePlayStageTo(TrainStage.VIEWING_STATE) - AsyncResult.success( + AsyncResult.Success( retrieveEphemeralQuestionState(questionsList) ) } - TrainStage.VIEWING_STATE -> AsyncResult.success( + TrainStage.VIEWING_STATE -> AsyncResult.Success( retrieveEphemeralQuestionState(questionsList) ) - TrainStage.SUBMITTING_ANSWER -> AsyncResult.pending() + TrainStage.SUBMITTING_ANSWER -> AsyncResult.Pending() } } catch (e: Exception) { exceptionsController.logNonFatalException(e) - AsyncResult.failed(e) + AsyncResult.Failure(e) } } } diff --git a/domain/src/main/java/org/oppia/android/domain/question/QuestionTrainingController.kt b/domain/src/main/java/org/oppia/android/domain/question/QuestionTrainingController.kt index 935e17cca38..5fcab8a4455 100644 --- a/domain/src/main/java/org/oppia/android/domain/question/QuestionTrainingController.kt +++ b/domain/src/main/java/org/oppia/android/domain/question/QuestionTrainingController.kt @@ -4,13 +4,13 @@ import org.oppia.android.app.model.ProfileId import org.oppia.android.app.model.Question import org.oppia.android.domain.oppialogger.exceptions.ExceptionsController import org.oppia.android.domain.topic.TopicController -import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.data.DataProvider import org.oppia.android.util.data.DataProviders import org.oppia.android.util.data.DataProviders.Companion.transform import javax.inject.Inject import javax.inject.Singleton import kotlin.random.Random +import org.oppia.android.util.data.AsyncResult private const val RETRIEVE_QUESTION_FOR_SKILLS_ID_PROVIDER_ID = "retrieve_question_for_skills_id_provider_id" @@ -58,7 +58,7 @@ class QuestionTrainingController @Inject constructor( } catch (e: Exception) { exceptionsController.logNonFatalException(e) dataProviders.createInMemoryDataProviderAsync(START_QUESTION_TRAINING_SESSION_PROVIDER_ID) { - AsyncResult.failed(e) + AsyncResult.Failure(e) } } } diff --git a/domain/src/main/java/org/oppia/android/domain/topic/StoryProgressController.kt b/domain/src/main/java/org/oppia/android/domain/topic/StoryProgressController.kt index e2830055195..53852b66137 100644 --- a/domain/src/main/java/org/oppia/android/domain/topic/StoryProgressController.kt +++ b/domain/src/main/java/org/oppia/android/domain/topic/StoryProgressController.kt @@ -304,9 +304,9 @@ class StoryProgressController @Inject constructor( .transformAsync(RETRIEVE_CHAPTER_PLAY_STATE_DATA_PROVIDER_ID) { val chapterProgress = it.chapterProgressMap[explorationId] if (chapterProgress != null) { - AsyncResult.success(chapterProgress.chapterPlayState) + AsyncResult.Success(chapterProgress.chapterPlayState) } else { - AsyncResult.success(ChapterPlayState.NOT_STARTED) + AsyncResult.Success(ChapterPlayState.NOT_STARTED) } } } @@ -319,7 +319,7 @@ class StoryProgressController @Inject constructor( .transformAsync(RETRIEVE_TOPIC_PROGRESS_LIST_DATA_PROVIDER_ID) { topicProgressDatabase -> val topicProgressList = mutableListOf() topicProgressList.addAll(topicProgressDatabase.topicProgressMap.values) - AsyncResult.success(topicProgressList.toList()) + AsyncResult.Success(topicProgressList.toList()) } } @@ -330,7 +330,7 @@ class StoryProgressController @Inject constructor( ): DataProvider { return retrieveCacheStore(profileId) .transformAsync(RETRIEVE_TOPIC_PROGRESS_DATA_PROVIDER_ID) { - AsyncResult.success(it.topicProgressMap[topicId] ?: TopicProgress.getDefaultInstance()) + AsyncResult.Success(it.topicProgressMap[topicId] ?: TopicProgress.getDefaultInstance()) } } @@ -342,7 +342,7 @@ class StoryProgressController @Inject constructor( ): DataProvider { return retrieveTopicProgressDataProvider(profileId, topicId) .transformAsync(RETRIEVE_STORY_PROGRESS_DATA_PROVIDER_ID) { - AsyncResult.success(it.storyProgressMap[storyId] ?: StoryProgress.getDefaultInstance()) + AsyncResult.Success(it.storyProgressMap[storyId] ?: StoryProgress.getDefaultInstance()) } } @@ -350,7 +350,7 @@ class StoryProgressController @Inject constructor( deferred: Deferred ): AsyncResult { return when (deferred.await()) { - StoryProgressActionStatus.SUCCESS -> AsyncResult.success(null) + StoryProgressActionStatus.SUCCESS -> AsyncResult.Success(null) } } diff --git a/domain/src/main/java/org/oppia/android/domain/topic/TopicController.kt b/domain/src/main/java/org/oppia/android/domain/topic/TopicController.kt index 2e8c0485fbe..ab831717cc5 100755 --- a/domain/src/main/java/org/oppia/android/domain/topic/TopicController.kt +++ b/domain/src/main/java/org/oppia/android/domain/topic/TopicController.kt @@ -115,7 +115,7 @@ class TopicController @Inject constructor( fun getTopic(profileId: ProfileId, topicId: String): DataProvider { val topicDataProvider = dataProviders.createInMemoryDataProviderAsync(GET_TOPIC_PROVIDER_ID) { - return@createInMemoryDataProviderAsync AsyncResult.success(retrieveTopic(topicId)) + return@createInMemoryDataProviderAsync AsyncResult.Success(retrieveTopic(topicId)) } val topicProgressDataProvider = storyProgressController.retrieveTopicProgressDataProvider(profileId, topicId) @@ -142,7 +142,7 @@ class TopicController @Inject constructor( ): DataProvider { val storyDataProvider = dataProviders.createInMemoryDataProviderAsync(GET_STORY_PROVIDER_ID) { - return@createInMemoryDataProviderAsync AsyncResult.success(retrieveStory(topicId, storyId)) + return@createInMemoryDataProviderAsync AsyncResult.Success(retrieveStory(topicId, storyId)) } val storyProgressDataProvider = storyProgressController.retrieveStoryProgressDataProvider(profileId, topicId, storyId) @@ -168,16 +168,15 @@ class TopicController @Inject constructor( explorationId: String ): DataProvider { return dataProviders.createInMemoryDataProviderAsync(GET_STORY_PROVIDER_ID) { - return@createInMemoryDataProviderAsync AsyncResult.success(retrieveStory(topicId, storyId)) + return@createInMemoryDataProviderAsync AsyncResult.Success(retrieveStory(topicId, storyId)) }.transformAsync(GET_CHAPTER_PROVIDER_ID) { storySummary -> val chapterSummary = fetchChapter(storySummary, explorationId) if (chapterSummary != null) { - AsyncResult.success(chapterSummary) + AsyncResult.Success(chapterSummary) } else { - AsyncResult.failed( - ChapterNotFoundException( - "Chapter for exploration $explorationId not found in story $storyId and topic $topicId" - ) + AsyncResult.Failure(ChapterNotFoundException( + "Chapter for exploration $explorationId not found in story $storyId and topic $topicId" + ) ) } } @@ -246,7 +245,7 @@ class TopicController @Inject constructor( ) ) } - AsyncResult.success(completedStoryListBuilder.build()) + AsyncResult.Success(completedStoryListBuilder.build()) } } @@ -258,7 +257,7 @@ class TopicController @Inject constructor( profileId ).transformAsync(GET_ONGOING_TOPIC_LIST_PROVIDER_ID) { val ongoingTopicList = createOngoingTopicListFromProgress(it) - AsyncResult.success(ongoingTopicList) + AsyncResult.Success(ongoingTopicList) } } diff --git a/domain/src/main/java/org/oppia/android/domain/topic/TopicListController.kt b/domain/src/main/java/org/oppia/android/domain/topic/TopicListController.kt index 6d37567193f..9d8cda5ded5 100644 --- a/domain/src/main/java/org/oppia/android/domain/topic/TopicListController.kt +++ b/domain/src/main/java/org/oppia/android/domain/topic/TopicListController.kt @@ -29,7 +29,6 @@ import org.oppia.android.domain.util.JsonAssetRetriever import org.oppia.android.domain.util.getStringFromObject import org.oppia.android.util.caching.AssetRepository import org.oppia.android.util.caching.LoadLessonProtosFromAssets -import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.data.DataProvider import org.oppia.android.util.data.DataProviders import org.oppia.android.util.data.DataProviders.Companion.transformAsync @@ -37,6 +36,7 @@ import org.oppia.android.util.system.OppiaClock import java.util.concurrent.TimeUnit import javax.inject.Inject import javax.inject.Singleton +import org.oppia.android.util.data.AsyncResult private const val ONE_WEEK_IN_DAYS = 7 @@ -121,7 +121,7 @@ class TopicListController @Inject constructor( return storyProgressController.retrieveTopicProgressListDataProvider(profileId) .transformAsync(GET_PROMOTED_ACTIVITY_LIST_PROVIDER_ID) { val promotedActivityList = computePromotedActivityList(it) - AsyncResult.success(promotedActivityList) + AsyncResult.Success(promotedActivityList) } } diff --git a/domain/src/main/java/org/oppia/android/domain/translation/TranslationController.kt b/domain/src/main/java/org/oppia/android/domain/translation/TranslationController.kt index 6cf23c39d04..007233a2d7a 100644 --- a/domain/src/main/java/org/oppia/android/domain/translation/TranslationController.kt +++ b/domain/src/main/java/org/oppia/android/domain/translation/TranslationController.kt @@ -17,7 +17,6 @@ import org.oppia.android.app.model.WrittenTranslationContext import org.oppia.android.app.model.WrittenTranslationLanguageSelection import org.oppia.android.domain.locale.LocaleController import org.oppia.android.util.data.AsyncDataSubscriptionManager -import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.data.DataProvider import org.oppia.android.util.data.DataProviders import org.oppia.android.util.data.DataProviders.Companion.transform @@ -27,6 +26,7 @@ import java.util.concurrent.locks.ReentrantLock import javax.inject.Inject import javax.inject.Singleton import kotlin.concurrent.withLock +import org.oppia.android.util.data.AsyncResult private const val SYSTEM_LANGUAGE_LOCALE_DATA_PROVIDER_ID = "system_language_locale" private const val APP_LANGUAGE_DATA_PROVIDER_ID = "app_language" @@ -122,7 +122,7 @@ class TranslationController @Inject constructor( fun updateAppLanguage(profileId: ProfileId, selection: AppLanguageSelection): DataProvider { return dataProviders.createInMemoryDataProviderAsync(UPDATE_APP_LANGUAGE_DATA_PROVIDER_ID) { updateAppLanguageSelection(profileId, selection) - return@createInMemoryDataProviderAsync AsyncResult.success(Unit) + return@createInMemoryDataProviderAsync AsyncResult.Success(Unit) } } @@ -173,7 +173,7 @@ class TranslationController @Inject constructor( val providerId = UPDATE_WRITTEN_TRANSLATION_CONTENT_DATA_PROVIDER_ID return dataProviders.createInMemoryDataProviderAsync(providerId) { updateWrittenTranslationContentLanguageSelection(profileId, selection) - return@createInMemoryDataProviderAsync AsyncResult.success(Unit) + return@createInMemoryDataProviderAsync AsyncResult.Success(Unit) } } @@ -224,7 +224,7 @@ class TranslationController @Inject constructor( val providerId = UPDATE_AUDIO_TRANSLATION_CONTENT_DATA_PROVIDER_ID return dataProviders.createInMemoryDataProviderAsync(providerId) { updateAudioTranslationContentLanguageSelection(profileId, selection) - return@createInMemoryDataProviderAsync AsyncResult.success(Unit) + return@createInMemoryDataProviderAsync AsyncResult.Success(Unit) } } diff --git a/domain/src/test/java/org/oppia/android/domain/audio/AudioPlayerControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/audio/AudioPlayerControllerTest.kt index 972bd2ea1ba..9add96bd2c0 100644 --- a/domain/src/test/java/org/oppia/android/domain/audio/AudioPlayerControllerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/audio/AudioPlayerControllerTest.kt @@ -49,8 +49,12 @@ import org.robolectric.shadows.util.DataSource import java.io.IOException import javax.inject.Inject import javax.inject.Singleton +import org.oppia.android.testing.data.AsyncResultSubject +import org.oppia.android.testing.data.AsyncResultSubject.Companion.assertThat /** Tests for [AudioPlayerControllerTest]. */ +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) @Config(manifest = Config.NONE) @@ -146,8 +150,9 @@ class AudioPlayerControllerTest { arrangeMediaPlayer() verify(mockAudioPlayerObserver, atLeastOnce()).onChanged(audioPlayerResultCaptor.capture()) - assertThat(audioPlayerResultCaptor.value.isSuccess()).isTrue() - assertThat(audioPlayerResultCaptor.value.getOrThrow().type).isEqualTo(PlayStatus.PREPARED) + assertThat(audioPlayerResultCaptor.value).hasSuccessValueWhere { + assertThat(type).isEqualTo(PlayStatus.PREPARED) + } } @Test @@ -159,7 +164,7 @@ class AudioPlayerControllerTest { audioPlayerController.changeDataSource(TEST_URL) verify(mockAudioPlayerObserver, atLeastOnce()).onChanged(audioPlayerResultCaptor.capture()) - assertThat(audioPlayerResultCaptor.value.isPending()).isTrue() + assertThat(audioPlayerResultCaptor.value).isPending() } @Test @@ -169,9 +174,10 @@ class AudioPlayerControllerTest { shadowMediaPlayer.invokeCompletionListener() verify(mockAudioPlayerObserver, atLeastOnce()).onChanged(audioPlayerResultCaptor.capture()) - assertThat(audioPlayerResultCaptor.value.isSuccess()).isTrue() - assertThat(audioPlayerResultCaptor.value.getOrThrow().type).isEqualTo(PlayStatus.COMPLETED) - assertThat(audioPlayerResultCaptor.value.getOrThrow().position).isEqualTo(0) + assertThat(audioPlayerResultCaptor.value).hasSuccessValueWhere { + assertThat(type).isEqualTo(PlayStatus.COMPLETED) + assertThat(position).isEqualTo(0) + } } @Test @@ -181,7 +187,7 @@ class AudioPlayerControllerTest { audioPlayerController.changeDataSource(TEST_URL2) verify(mockAudioPlayerObserver, atLeastOnce()).onChanged(audioPlayerResultCaptor.capture()) - assertThat(audioPlayerResultCaptor.value.isPending()).isTrue() + assertThat(audioPlayerResultCaptor.value).isPending() } @Test @@ -192,7 +198,7 @@ class AudioPlayerControllerTest { audioPlayerController.changeDataSource(TEST_URL2) verify(mockAudioPlayerObserver, atLeastOnce()).onChanged(audioPlayerResultCaptor.capture()) - assertThat(audioPlayerResultCaptor.value.isPending()).isTrue() + assertThat(audioPlayerResultCaptor.value).isPending() } @Test @@ -203,8 +209,9 @@ class AudioPlayerControllerTest { testCoroutineDispatchers.runCurrent() verify(mockAudioPlayerObserver, atLeastOnce()).onChanged(audioPlayerResultCaptor.capture()) - assertThat(audioPlayerResultCaptor.value.isSuccess()).isTrue() - assertThat(audioPlayerResultCaptor.value.getOrThrow().type).isEqualTo(PlayStatus.PLAYING) + assertThat(audioPlayerResultCaptor.value).hasSuccessValueWhere { + assertThat(type).isEqualTo(PlayStatus.PLAYING) + } } @Test @@ -218,7 +225,7 @@ class AudioPlayerControllerTest { verify(mockAudioPlayerObserver, atLeastOnce()).onChanged(audioPlayerResultCaptor.capture()) val results = audioPlayerResultCaptor.allValues - val pendingIndex = results.indexOfLast { it.isPending() } + val pendingIndex = results.indexOfLast { it is AsyncResult.Pending } val preparedIndex = results.indexOfLast { it.hasStatus(PlayStatus.PREPARED) } val playingIndex = results.indexOfLast { it.hasStatus(PlayStatus.PLAYING) } val completedIndex = results.indexOfLast { it.hasStatus(PlayStatus.COMPLETED) } @@ -238,8 +245,9 @@ class AudioPlayerControllerTest { audioPlayerController.pause() verify(mockAudioPlayerObserver, atLeastOnce()).onChanged(audioPlayerResultCaptor.capture()) - assertThat(audioPlayerResultCaptor.value.isSuccess()).isTrue() - assertThat(audioPlayerResultCaptor.value.getOrThrow().type).isEqualTo(PlayStatus.PAUSED) + assertThat(audioPlayerResultCaptor.value).hasSuccessValueWhere { + assertThat(type).isEqualTo(PlayStatus.PAUSED) + } } @Test @@ -247,7 +255,9 @@ class AudioPlayerControllerTest { arrangeMediaPlayer() verify(mockAudioPlayerObserver, atLeastOnce()).onChanged(audioPlayerResultCaptor.capture()) - assertThat(audioPlayerResultCaptor.value.getOrThrow().position).isEqualTo(0) + assertThat(audioPlayerResultCaptor.value).hasSuccessValueWhere { + assertThat(position).isEqualTo(0) + } } @Test @@ -260,7 +270,9 @@ class AudioPlayerControllerTest { testCoroutineDispatchers.runCurrent() verify(mockAudioPlayerObserver, atLeastOnce()).onChanged(audioPlayerResultCaptor.capture()) - assertThat(audioPlayerResultCaptor.value.getOrThrow().position).isEqualTo(500) + assertThat(audioPlayerResultCaptor.value).hasSuccessValueWhere { + assertThat(position).isEqualTo(500) + } } @Test @@ -270,7 +282,9 @@ class AudioPlayerControllerTest { audioPlayerController.play() verify(mockAudioPlayerObserver, atLeastOnce()).onChanged(audioPlayerResultCaptor.capture()) - assertThat(audioPlayerResultCaptor.value.getOrThrow().duration).isEqualTo(2000) + assertThat(audioPlayerResultCaptor.value).hasSuccessValueWhere { + assertThat(duration).isEqualTo(2000) + } } @Test @@ -283,7 +297,9 @@ class AudioPlayerControllerTest { audioPlayerController.play() verify(mockAudioPlayerObserver, atLeastOnce()).onChanged(audioPlayerResultCaptor.capture()) - assertThat(audioPlayerResultCaptor.value.getOrThrow().position).isEqualTo(0) + assertThat(audioPlayerResultCaptor.value).hasSuccessValueWhere { + assertThat(position).isEqualTo(0) + } } @Test @@ -295,8 +311,9 @@ class AudioPlayerControllerTest { verify(mockAudioPlayerObserver, atLeastOnce()).onChanged(audioPlayerResultCaptor.capture()) // If the observer was still getting updates, the result would be pending - assertThat(audioPlayerResultCaptor.value.isSuccess()).isTrue() - assertThat(audioPlayerResultCaptor.value.getOrThrow().type).isEqualTo(PlayStatus.PREPARED) + assertThat(audioPlayerResultCaptor.value).hasSuccessValueWhere { + assertThat(type).isEqualTo(PlayStatus.PREPARED) + } } @Test @@ -309,7 +326,9 @@ class AudioPlayerControllerTest { testCoroutineDispatchers.advanceTimeBy(2000) verify(mockAudioPlayerObserver, atLeastOnce()).onChanged(audioPlayerResultCaptor.capture()) - assertThat(audioPlayerResultCaptor.value.getOrThrow().type).isEqualTo(PlayStatus.PAUSED) + assertThat(audioPlayerResultCaptor.value).hasSuccessValueWhere { + assertThat(type).isEqualTo(PlayStatus.PAUSED) + } // Verify: If the test does not hang, the behavior is correct. } @@ -323,7 +342,9 @@ class AudioPlayerControllerTest { testCoroutineDispatchers.advanceTimeBy(2000) verify(mockAudioPlayerObserver, atLeastOnce()).onChanged(audioPlayerResultCaptor.capture()) - assertThat(audioPlayerResultCaptor.value.getOrThrow().type).isEqualTo(PlayStatus.COMPLETED) + assertThat(audioPlayerResultCaptor.value).hasSuccessValueWhere { + assertThat(type).isEqualTo(PlayStatus.COMPLETED) + } // Verify: If the test does not hang, the behavior is correct. } @@ -367,7 +388,7 @@ class AudioPlayerControllerTest { shadowMediaPlayer.invokePreparedListener() verify(mockAudioPlayerObserver, atLeastOnce()).onChanged(audioPlayerResultCaptor.capture()) - assertThat(audioPlayerResultCaptor.value.isFailure()).isTrue() + assertThat(audioPlayerResultCaptor.value).isFailure() } @Test @@ -440,7 +461,7 @@ class AudioPlayerControllerTest { } private fun AsyncResult.hasStatus(playStatus: PlayStatus): Boolean { - return isCompleted() && getOrThrow().type == playStatus + return (this is AsyncResult.Success) && value.type == playStatus } private fun setUpTestApplicationComponent() { diff --git a/domain/src/test/java/org/oppia/android/domain/audio/CellularAudioDialogControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/audio/CellularAudioDialogControllerTest.kt index 4aaafe0af54..6e21f7aaded 100644 --- a/domain/src/test/java/org/oppia/android/domain/audio/CellularAudioDialogControllerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/audio/CellularAudioDialogControllerTest.kt @@ -2,7 +2,6 @@ package org.oppia.android.domain.audio import android.app.Application import android.content.Context -import androidx.lifecycle.Observer import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat @@ -10,26 +9,18 @@ import dagger.BindsInstance import dagger.Component import dagger.Module import dagger.Provides +import javax.inject.Inject +import javax.inject.Singleton import org.junit.Before -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentCaptor -import org.mockito.Captor -import org.mockito.Mock -import org.mockito.Mockito.atLeastOnce -import org.mockito.Mockito.verify -import org.mockito.junit.MockitoJUnit -import org.mockito.junit.MockitoRule -import org.oppia.android.app.model.CellularDataPreference import org.oppia.android.domain.oppialogger.LogStorageModule import org.oppia.android.testing.TestLogReportingModule +import org.oppia.android.testing.data.DataProviderTestMonitor import org.oppia.android.testing.robolectric.RobolectricModule import org.oppia.android.testing.threading.TestCoroutineDispatchers import org.oppia.android.testing.threading.TestDispatcherModule import org.oppia.android.testing.time.FakeOppiaClockModule -import org.oppia.android.util.data.AsyncResult -import org.oppia.android.util.data.DataProviders.Companion.toLiveData import org.oppia.android.util.data.DataProvidersInjector import org.oppia.android.util.data.DataProvidersInjectorProvider import org.oppia.android.util.locale.LocaleProdModule @@ -40,28 +31,16 @@ import org.oppia.android.util.logging.LogLevel import org.oppia.android.util.networking.NetworkConnectionUtilDebugModule import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode -import javax.inject.Inject -import javax.inject.Singleton +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) @Config(application = CellularAudioDialogControllerTest.TestApplication::class) class CellularAudioDialogControllerTest { - @Rule - @JvmField - val mockitoRule: MockitoRule = MockitoJUnit.rule() - - @Inject - lateinit var cellularAudioDialogController: CellularAudioDialogController - - @Inject - lateinit var testCoroutineDispatchers: TestCoroutineDispatchers - - @Mock - lateinit var mockCellularDataObserver: Observer> - - @Captor - lateinit var cellularDataResultCaptor: ArgumentCaptor> + @Inject lateinit var cellularAudioDialogController: CellularAudioDialogController + @Inject lateinit var testCoroutineDispatchers: TestCoroutineDispatchers + @Inject lateinit var monitorFactory: DataProviderTestMonitor.Factory @Before fun setUp() { @@ -73,80 +52,64 @@ class CellularAudioDialogControllerTest { } @Test - fun testController_providesInitialLiveData_indicatesToNotHideDialogAndNotUseCellularData() { - val cellularDataPreference = - cellularAudioDialogController.getCellularDataPreference().toLiveData() - cellularDataPreference.observeForever(mockCellularDataObserver) - testCoroutineDispatchers.advanceUntilIdle() + fun testController_providesInitialState_indicatesToNotHideDialogAndNotUseCellularData() { + val cellularDataPreference = cellularAudioDialogController.getCellularDataPreference() - verify(mockCellularDataObserver, atLeastOnce()).onChanged(cellularDataResultCaptor.capture()) - assertThat(cellularDataResultCaptor.value.isSuccess()).isTrue() - assertThat(cellularDataResultCaptor.value.getOrThrow().hideDialog).isFalse() - assertThat(cellularDataResultCaptor.value.getOrThrow().useCellularData).isFalse() + val value = monitorFactory.waitForNextSuccessfulResult(cellularDataPreference) + assertThat(value.hideDialog).isFalse() + assertThat(value.useCellularData).isFalse() } @Test - fun testController_setNeverUseCellularDataPref_providesLiveData_indicatesToHideDialogAndNotUseCellularData() { // ktlint-disable max-line-length - val appHistory = - cellularAudioDialogController.getCellularDataPreference().toLiveData() + fun testController_setNeverUseCellularDataPref_indicatesToHideDialogAndNotUseCellularData() { + val cellularDataPreference = cellularAudioDialogController.getCellularDataPreference() - appHistory.observeForever(mockCellularDataObserver) cellularAudioDialogController.setNeverUseCellularDataPreference() testCoroutineDispatchers.advanceUntilIdle() - verify(mockCellularDataObserver, atLeastOnce()).onChanged(cellularDataResultCaptor.capture()) - assertThat(cellularDataResultCaptor.value.isSuccess()).isTrue() - assertThat(cellularDataResultCaptor.value.getOrThrow().hideDialog).isTrue() - assertThat(cellularDataResultCaptor.value.getOrThrow().useCellularData).isFalse() + val value = monitorFactory.waitForNextSuccessfulResult(cellularDataPreference) + assertThat(value.hideDialog).isTrue() + assertThat(value.useCellularData).isFalse() } @Test - fun testController_setAlwaysUseCellularDataPref_providesLiveData_indicatesToHideDialogAndUseCellularData() { // ktlint-disable max-line-length - val appHistory = - cellularAudioDialogController.getCellularDataPreference().toLiveData() + fun testController_setAlwaysUseCellularDataPref_indicatesToHideDialogAndUseCellularData() { + val cellularDataPreference = cellularAudioDialogController.getCellularDataPreference() - appHistory.observeForever(mockCellularDataObserver) cellularAudioDialogController.setAlwaysUseCellularDataPreference() testCoroutineDispatchers.advanceUntilIdle() - verify(mockCellularDataObserver, atLeastOnce()).onChanged(cellularDataResultCaptor.capture()) - assertThat(cellularDataResultCaptor.value.getOrThrow().hideDialog).isTrue() - assertThat(cellularDataResultCaptor.value.getOrThrow().useCellularData).isTrue() + val value = monitorFactory.waitForNextSuccessfulResult(cellularDataPreference) + assertThat(value.hideDialog).isTrue() + assertThat(value.useCellularData).isTrue() } @Test - fun testController_setNeverUseCellularDataPref_observedNewController_indicatesToHideDialogAndNotUseCellularData() { // ktlint-disable max-line-length + fun testController_setNeverUseCellDataPref_observeNewController_indicatesHideDialogNotUseCell() { // Pause immediate dispatching to avoid an infinite loop within the provider pipeline. cellularAudioDialogController.setNeverUseCellularDataPreference() testCoroutineDispatchers.advanceUntilIdle() setUpTestApplicationComponent() - val appHistory = - cellularAudioDialogController.getCellularDataPreference().toLiveData() - appHistory.observeForever(mockCellularDataObserver) testCoroutineDispatchers.advanceUntilIdle() + val cellularDataPreference = cellularAudioDialogController.getCellularDataPreference() - verify(mockCellularDataObserver, atLeastOnce()).onChanged(cellularDataResultCaptor.capture()) - assertThat(cellularDataResultCaptor.value.isSuccess()).isTrue() - assertThat(cellularDataResultCaptor.value.getOrThrow().hideDialog).isTrue() - assertThat(cellularDataResultCaptor.value.getOrThrow().useCellularData).isFalse() + val value = monitorFactory.waitForNextSuccessfulResult(cellularDataPreference) + assertThat(value.hideDialog).isTrue() + assertThat(value.useCellularData).isFalse() } @Test - fun testController_setAlwaysUseCellularDataPref_observedNewController_indicatesToHideDialogAndUseCellularData() { // ktlint-disable max-line-length + fun testController_setAlwaysUseCellDataPref_observeNewController_indicatesHideDialogAndUseCell() { cellularAudioDialogController.setAlwaysUseCellularDataPreference() testCoroutineDispatchers.advanceUntilIdle() setUpTestApplicationComponent() - val appHistory = - cellularAudioDialogController.getCellularDataPreference().toLiveData() - appHistory.observeForever(mockCellularDataObserver) - testCoroutineDispatchers.advanceUntilIdle() + val cellularDataPreference = cellularAudioDialogController.getCellularDataPreference() - verify(mockCellularDataObserver, atLeastOnce()).onChanged(cellularDataResultCaptor.capture()) - assertThat(cellularDataResultCaptor.value.isSuccess()).isTrue() - assertThat(cellularDataResultCaptor.value.getOrThrow().hideDialog).isTrue() - assertThat(cellularDataResultCaptor.value.getOrThrow().useCellularData).isTrue() + val value = monitorFactory.waitForNextSuccessfulResult(cellularDataPreference) + assertThat(value.hideDialog).isTrue() + assertThat(value.useCellularData).isTrue() } // TODO(#89): Move this to a common test application component. diff --git a/domain/src/test/java/org/oppia/android/domain/devoptions/ModifyLessonProgressControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/devoptions/ModifyLessonProgressControllerTest.kt index 4bd222ebe06..7d787abfa56 100644 --- a/domain/src/test/java/org/oppia/android/domain/devoptions/ModifyLessonProgressControllerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/devoptions/ModifyLessonProgressControllerTest.kt @@ -2,7 +2,6 @@ package org.oppia.android.domain.devoptions import android.app.Application import android.content.Context -import androidx.lifecycle.Observer import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat @@ -11,15 +10,8 @@ import dagger.Component import dagger.Module import dagger.Provides import org.junit.Before -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentCaptor -import org.mockito.Captor -import org.mockito.Mock -import org.mockito.Mockito.verify -import org.mockito.junit.MockitoJUnit -import org.mockito.junit.MockitoRule import org.oppia.android.app.model.ChapterPlayState import org.oppia.android.app.model.ProfileId import org.oppia.android.app.model.StorySummary @@ -36,8 +28,6 @@ import org.oppia.android.testing.time.FakeOppiaClockModule import org.oppia.android.util.caching.AssetModule import org.oppia.android.util.caching.CacheAssetsLocally import org.oppia.android.util.caching.LoadLessonProtosFromAssets -import org.oppia.android.util.data.AsyncResult -import org.oppia.android.util.data.DataProviders.Companion.toLiveData import org.oppia.android.util.data.DataProvidersInjector import org.oppia.android.util.data.DataProvidersInjectorProvider import org.oppia.android.util.locale.LocaleProdModule @@ -50,8 +40,11 @@ import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode import javax.inject.Inject import javax.inject.Singleton +import org.oppia.android.testing.data.DataProviderTestMonitor /** Tests for [ModifyLessonProgressController]. */ +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) @Config(application = ModifyLessonProgressControllerTest.TestApplication::class) @@ -71,43 +64,15 @@ class ModifyLessonProgressControllerTest { private const val RATIOS_STORY_ID_1 = "xBSdg4oOClga" private const val TEST_EXPLORATION_ID_2 = "test_exp_id_2" - private const val TEST_EXPLORATION_ID_4 = "test_exp_id_4" - private const val TEST_EXPLORATION_ID_5 = "13" private const val FRACTIONS_EXPLORATION_ID_0 = "umPkwp0L1M0-" private const val FRACTIONS_EXPLORATION_ID_1 = "MjZzEVOG47_1" - private const val RATIOS_EXPLORATION_ID_0 = "2mzzFVDLuAj8" - private const val RATIOS_EXPLORATION_ID_1 = "5NWuolNcwH6e" - private const val RATIOS_EXPLORATION_ID_2 = "k2bQ7z5XHNbK" - private const val RATIOS_EXPLORATION_ID_3 = "tIoSb3HZFN6e" } - @Rule - @JvmField - val mockitoRule: MockitoRule = MockitoJUnit.rule() - - @Inject - lateinit var storyProgressTestHelper: StoryProgressTestHelper - - @Inject - lateinit var modifyLessonProgressController: ModifyLessonProgressController - - @Inject - lateinit var testCoroutineDispatchers: TestCoroutineDispatchers - - @Inject - lateinit var fakeOppiaClock: FakeOppiaClock - - @Mock - lateinit var mockAllTopicsObserver: Observer>> - - @Captor - lateinit var allTopicsResultCaptor: ArgumentCaptor>> - - @Mock - lateinit var mockAllStoriesObserver: Observer>>> - - @Captor - lateinit var allStoriesResultCaptor: ArgumentCaptor>>> + @Inject lateinit var storyProgressTestHelper: StoryProgressTestHelper + @Inject lateinit var modifyLessonProgressController: ModifyLessonProgressController + @Inject lateinit var testCoroutineDispatchers: TestCoroutineDispatchers + @Inject lateinit var fakeOppiaClock: FakeOppiaClock + @Inject lateinit var monitorFactory: DataProviderTestMonitor.Factory private lateinit var profileId: ProfileId @@ -120,24 +85,22 @@ class ModifyLessonProgressControllerTest { @Test fun testRetrieveAllTopics_isSuccessful() { - val allTopicsLiveData = - modifyLessonProgressController.getAllTopicsWithProgress(profileId).toLiveData() - allTopicsLiveData.observeForever(mockAllTopicsObserver) - testCoroutineDispatchers.runCurrent() - verify(mockAllTopicsObserver).onChanged(allTopicsResultCaptor.capture()) - val allTopicsResult = allTopicsResultCaptor.value - assertThat(allTopicsResult!!.isSuccess()).isTrue() + val topicsProvider = modifyLessonProgressController.getAllTopicsWithProgress(profileId) + + monitorFactory.waitForNextSuccessfulResult(topicsProvider) } @Test fun testRetrieveAllTopics_providesListOfMultipleTopics() { val allTopics = retrieveAllTopics() + assertThat(allTopics.size).isGreaterThan(1) } @Test fun testRetrieveAllTopics_firstTopic_hasCorrectTopicInfo() { val allTopics = retrieveAllTopics() + val firstTopic = allTopics[0] assertThat(firstTopic.topicId).isEqualTo(TEST_TOPIC_ID_0) assertThat(firstTopic.name).isEqualTo("First Test Topic") @@ -146,6 +109,7 @@ class ModifyLessonProgressControllerTest { @Test fun testRetrieveAllTopics_secondTopic_hasCorrectTopicInfo() { val allTopics = retrieveAllTopics() + val secondTopic = allTopics[1] assertThat(secondTopic.topicId).isEqualTo(TEST_TOPIC_ID_1) assertThat(secondTopic.name).isEqualTo("Second Test Topic") @@ -154,6 +118,7 @@ class ModifyLessonProgressControllerTest { @Test fun testRetrieveAllTopics_fractionsTopic_hasCorrectTopicInfo() { val allTopics = retrieveAllTopics() + val fractionsTopic = allTopics[2] assertThat(fractionsTopic.topicId).isEqualTo(FRACTIONS_TOPIC_ID) assertThat(fractionsTopic.name).isEqualTo("Fractions") @@ -162,6 +127,7 @@ class ModifyLessonProgressControllerTest { @Test fun testRetrieveAllTopics_ratiosTopic_hasCorrectTopicInfo() { val allTopics = retrieveAllTopics() + val ratiosTopic = allTopics[3] assertThat(ratiosTopic.topicId).isEqualTo(RATIOS_TOPIC_ID) assertThat(ratiosTopic.name).isEqualTo("Ratios and Proportional Reasoning") @@ -177,6 +143,7 @@ class ModifyLessonProgressControllerTest { @Test fun testRetrieveAllTopics_firstTopic_withoutAnyProgress_correctProgressFound() { val allTopics = retrieveAllTopics() + val firstTopic = allTopics[0] assertThat(firstTopic.topicId).isEqualTo(TEST_TOPIC_ID_0) assertThat(firstTopic.storyList[0].chapterList[0].chapterPlayState) @@ -190,7 +157,9 @@ class ModifyLessonProgressControllerTest { @Test fun testRetrieveAllTopics_firstTopic_withTopicCompleted_correctProgressFound() { markFirstTestTopicCompleted() + val allTopics = retrieveAllTopics() + val firstTopic = allTopics[0] assertThat(firstTopic.topicId).isEqualTo(TEST_TOPIC_ID_0) assertThat(firstTopic.storyList[0].chapterList[0].chapterPlayState) @@ -202,43 +171,47 @@ class ModifyLessonProgressControllerTest { @Test fun testRetrieveAllTopics_withoutAnyProgress_noTopicIsCompleted() { val allTopics = retrieveAllTopics() - allTopics.forEach { topic -> - val isCompleted = modifyLessonProgressController.checkIfTopicIsCompleted(topic) - assertThat(isCompleted).isFalse() - } + + val topicsProgress = allTopics.map(modifyLessonProgressController::checkIfTopicIsCompleted) + + // None of the topics have progress. + assertThat(topicsProgress.all { !it }).isTrue() } @Test fun markFirstTestTopicCompleted_testRetrieveAllTopics_onlyFirstTestTopicIsCompleted() { markFirstTestTopicCompleted() val allTopics = retrieveAllTopics() - allTopics.forEach { topic -> - val isCompleted = modifyLessonProgressController.checkIfTopicIsCompleted(topic) - if (topic.topicId.equals(TEST_TOPIC_ID_0)) assertThat(isCompleted).isTrue() - else assertThat(isCompleted).isFalse() - } + + val topicsProgress = + allTopics.associateBy(Topic::getTopicId).mapValues { (_, topic) -> + modifyLessonProgressController.checkIfTopicIsCompleted(topic) + } + + // All topics except the test topic 0 should not have progress. + val nonTestTopics = topicsProgress.filterNot { (id, _) -> id == TEST_TOPIC_ID_0 } + assertThat(nonTestTopics.values.count { !it }).isEqualTo(allTopics.size - 1) + assertThat(topicsProgress[TEST_TOPIC_ID_0]).isTrue() } @Test fun testRetrieveAllStories_isSuccessful() { - val allStoriesLiveData = - modifyLessonProgressController.getStoryMapWithProgress(profileId).toLiveData() - allStoriesLiveData.observeForever(mockAllStoriesObserver) - testCoroutineDispatchers.runCurrent() - verify(mockAllStoriesObserver).onChanged(allStoriesResultCaptor.capture()) - val allStoriesResult = allStoriesResultCaptor.value - assertThat(allStoriesResult!!.isSuccess()).isTrue() + val storyProgressProvider = modifyLessonProgressController.getStoryMapWithProgress(profileId) + + monitorFactory.waitForNextSuccessfulResult(storyProgressProvider) } @Test fun testRetrieveAllStories_providesListOfMultipleStories() { val allStories = retrieveAllStories() + assertThat(allStories.size).isGreaterThan(1) } @Test fun testRetrieveAllStories_firstStory_hasCorrectStoryInfo() { val allStories = retrieveAllStories() + val firstStory = allStories[0] assertThat(firstStory.storyId).isEqualTo(TEST_STORY_ID_0) assertThat(firstStory.storyName).isEqualTo("First Story") @@ -247,6 +220,7 @@ class ModifyLessonProgressControllerTest { @Test fun testRetrieveAllStories_otherStory_hasCorrectStoryInfo() { val allStories = retrieveAllStories() + val secondStory = allStories[1] assertThat(secondStory.storyId).isEqualTo(TEST_STORY_ID_2) assertThat(secondStory.storyName).isEqualTo("Other Interesting Story") @@ -255,6 +229,7 @@ class ModifyLessonProgressControllerTest { @Test fun testRetrieveAllStories_fractionsStory_hasCorrectStoryInfo() { val allStories = retrieveAllStories() + val fractionsStory = allStories[2] assertThat(fractionsStory.storyId).isEqualTo(FRACTIONS_STORY_ID_0) assertThat(fractionsStory.storyName).isEqualTo("Matthew Goes to the Bakery") @@ -263,6 +238,7 @@ class ModifyLessonProgressControllerTest { @Test fun testRetrieveAllStories_ratiosStory1_hasCorrectStoryInfo() { val allStories = retrieveAllStories() + val ratiosStory1 = allStories[3] assertThat(ratiosStory1.storyId).isEqualTo(RATIOS_STORY_ID_0) assertThat(ratiosStory1.storyName).isEqualTo("Ratios: Part 1") @@ -271,6 +247,7 @@ class ModifyLessonProgressControllerTest { @Test fun testRetrieveAllStories_ratiosStory2_hasCorrectStoryInfo() { val allStories = retrieveAllStories() + val ratiosStory2 = allStories[4] assertThat(ratiosStory2.storyId).isEqualTo(RATIOS_STORY_ID_1) assertThat(ratiosStory2.storyName).isEqualTo("Ratios: Part 2") @@ -279,6 +256,7 @@ class ModifyLessonProgressControllerTest { @Test fun testRetrieveAllStories_firstStory_withoutAnyProgress_correctProgressFound() { val allStories = retrieveAllStories() + val firstStory = allStories[0] assertThat(firstStory.storyId).isEqualTo(TEST_STORY_ID_0) assertThat(firstStory.chapterList[0].chapterPlayState).isEqualTo(ChapterPlayState.NOT_STARTED) @@ -291,7 +269,9 @@ class ModifyLessonProgressControllerTest { @Test fun testRetrieveAllStories_firstStory_withStoryCompleted_correctProgressFound() { markFirstStoryCompleted() + val allStories = retrieveAllStories() + val firstStory = allStories[0] assertThat(firstStory.storyId).isEqualTo(TEST_STORY_ID_0) assertThat(firstStory.chapterList[0].chapterPlayState).isEqualTo(ChapterPlayState.COMPLETED) @@ -301,21 +281,27 @@ class ModifyLessonProgressControllerTest { @Test fun testRetrieveAllStories_withoutAnyProgress_noStoryIsCompleted() { val allStories = retrieveAllStories() - allStories.forEach { storySummary -> - val isCompleted = modifyLessonProgressController.checkIfStoryIsCompleted(storySummary) - assertThat(isCompleted).isFalse() - } + + val storiesProgress = allStories.map(modifyLessonProgressController::checkIfStoryIsCompleted) + + // None of the stories have progress. + assertThat(storiesProgress.all { !it }).isTrue() } @Test fun markFirstStoryCompleted_testRetrieveAllStories_onlyFirstStoryIsCompleted() { markFirstStoryCompleted() val allStories = retrieveAllStories() - allStories.forEach { storySummary -> - val isCompleted = modifyLessonProgressController.checkIfStoryIsCompleted(storySummary) - if (storySummary.storyId.equals(TEST_STORY_ID_0)) assertThat(isCompleted).isTrue() - else assertThat(isCompleted).isFalse() - } + + val storiesProgress = + allStories.associateBy(StorySummary::getStoryId).mapValues { (_, storySummary) -> + modifyLessonProgressController.checkIfStoryIsCompleted(storySummary) + } + + // All stories except the test story 0 should not have progress. + val nonTestStories = storiesProgress.filterNot { (id, _) -> id == TEST_STORY_ID_0 } + assertThat(nonTestStories.values.count { !it }).isEqualTo(allStories.size - 1) + assertThat(storiesProgress[TEST_STORY_ID_0]).isTrue() } @Test @@ -324,6 +310,7 @@ class ModifyLessonProgressControllerTest { profileId, listOf(TEST_TOPIC_ID_0, FRACTIONS_TOPIC_ID) ) + val allTopics = retrieveAllTopics() val firstTopic = allTopics[0] val fractionsTopic = allTopics[2] @@ -345,6 +332,7 @@ class ModifyLessonProgressControllerTest { profileId, mapOf(TEST_STORY_ID_0 to TEST_TOPIC_ID_0, RATIOS_STORY_ID_1 to RATIOS_TOPIC_ID) ) + val allStories = retrieveAllStories() val firstStory = allStories[0] val ratios2Story = allStories[4] @@ -366,6 +354,7 @@ class ModifyLessonProgressControllerTest { FRACTIONS_EXPLORATION_ID_1 to Pair(FRACTIONS_STORY_ID_0, FRACTIONS_TOPIC_ID) ) ) + val allStories = retrieveAllStories() val firstStory = allStories[0] val fractionsStory = allStories[2] @@ -396,21 +385,13 @@ class ModifyLessonProgressControllerTest { } private fun retrieveAllTopics(): List { - val allTopicsLiveData = - modifyLessonProgressController.getAllTopicsWithProgress(profileId).toLiveData() - allTopicsLiveData.observeForever(mockAllTopicsObserver) - testCoroutineDispatchers.runCurrent() - verify(mockAllTopicsObserver).onChanged(allTopicsResultCaptor.capture()) - return allTopicsResultCaptor.value.getOrThrow() + val topicsProvider = modifyLessonProgressController.getAllTopicsWithProgress(profileId) + return monitorFactory.waitForNextSuccessfulResult(topicsProvider) } private fun retrieveAllStories(): List { - val allStoriesLiveData = - modifyLessonProgressController.getStoryMapWithProgress(profileId).toLiveData() - allStoriesLiveData.observeForever(mockAllStoriesObserver) - testCoroutineDispatchers.runCurrent() - verify(mockAllStoriesObserver).onChanged(allStoriesResultCaptor.capture()) - return allStoriesResultCaptor.value.getOrThrow().values.flatten() + val storyProgressProvider = modifyLessonProgressController.getStoryMapWithProgress(profileId) + return monitorFactory.waitForNextSuccessfulResult(storyProgressProvider).values.flatten() } // TODO(#89): Move this to a common test application component. diff --git a/domain/src/test/java/org/oppia/android/domain/exploration/lightweightcheckpointing/BUILD.bazel b/domain/src/test/java/org/oppia/android/domain/exploration/lightweightcheckpointing/BUILD.bazel new file mode 100644 index 00000000000..5b8b31df22c --- /dev/null +++ b/domain/src/test/java/org/oppia/android/domain/exploration/lightweightcheckpointing/BUILD.bazel @@ -0,0 +1,57 @@ +""" +Tests for lightweight checkpointing domain components. +""" + +load("@dagger//:workspace_defs.bzl", "dagger_rules") +load("//:oppia_android_test.bzl", "oppia_android_test") + +oppia_android_test( + name = "ExplorationCheckpointControllerTest", + srcs = ["ExplorationCheckpointControllerTest.kt"], + custom_package = "org.oppia.android.domain.exploration.lightweightcheckpointing", + test_class = "org.oppia.android.domain.exploration.lightweightcheckpointing.ExplorationCheckpointControllerTest", + test_manifest = "//domain:test_manifest", + deps = [ + ":dagger", + "//domain/src/main/java/org/oppia/android/domain/oppialogger:storage_module", + "//testing", + "//testing/src/main/java/org/oppia/android/testing/data:data_provider_test_monitor", + "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module", + "//testing/src/main/java/org/oppia/android/testing/threading:test_module", + "//testing/src/main/java/org/oppia/android/testing/time:test_module", + "//third_party:androidx_test_ext_junit", + "//third_party:com_google_truth_truth", + "//third_party:junit_junit", + "//third_party:org_robolectric_robolectric", + "//third_party:robolectric_android-all", + "//utility/src/main/java/org/oppia/android/util/caching:asset_prod_module", + "//utility/src/main/java/org/oppia/android/util/locale:prod_module", + "//utility/src/main/java/org/oppia/android/util/networking:debug_module", + ], +) + +oppia_android_test( + name = "ExplorationStorageModuleTest", + srcs = ["ExplorationStorageModuleTest.kt"], + custom_package = "org.oppia.android.domain.exploration.lightweightcheckpointing", + test_class = "org.oppia.android.domain.exploration.lightweightcheckpointing.ExplorationStorageModuleTest", + test_manifest = "//domain:test_manifest", + deps = [ + ":dagger", + "//domain/src/main/java/org/oppia/android/domain/oppialogger:storage_module", + "//testing", + "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module", + "//testing/src/main/java/org/oppia/android/testing/threading:test_module", + "//testing/src/main/java/org/oppia/android/testing/time:test_module", + "//third_party:androidx_test_ext_junit", + "//third_party:com_google_truth_truth", + "//third_party:junit_junit", + "//third_party:org_robolectric_robolectric", + "//third_party:robolectric_android-all", + "//utility/src/main/java/org/oppia/android/util/caching:asset_prod_module", + "//utility/src/main/java/org/oppia/android/util/locale:prod_module", + "//utility/src/main/java/org/oppia/android/util/networking:debug_module", + ], +) + +dagger_rules() diff --git a/domain/src/test/java/org/oppia/android/domain/exploration/lightweightcheckpointing/ExplorationCheckpointControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/exploration/lightweightcheckpointing/ExplorationCheckpointControllerTest.kt index 58abada089d..0f6906d6c19 100644 --- a/domain/src/test/java/org/oppia/android/domain/exploration/lightweightcheckpointing/ExplorationCheckpointControllerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/exploration/lightweightcheckpointing/ExplorationCheckpointControllerTest.kt @@ -2,7 +2,6 @@ package org.oppia.android.domain.exploration.lightweightcheckpointing import android.app.Application import android.content.Context -import androidx.lifecycle.Observer import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat @@ -10,26 +9,21 @@ import dagger.BindsInstance import dagger.Component import dagger.Module import dagger.Provides +import javax.inject.Inject +import javax.inject.Singleton import org.junit.Before -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentCaptor -import org.mockito.Captor -import org.mockito.Mock -import org.mockito.Mockito.atLeastOnce -import org.mockito.Mockito.reset -import org.mockito.Mockito.verify -import org.mockito.junit.MockitoJUnit -import org.mockito.junit.MockitoRule import org.oppia.android.app.model.CheckpointState import org.oppia.android.app.model.ExplorationCheckpoint -import org.oppia.android.app.model.ExplorationCheckpointDetails import org.oppia.android.app.model.ProfileId +import org.oppia.android.domain.exploration.lightweightcheckpointing.ExplorationCheckpointController.ExplorationCheckpointNotFoundException +import org.oppia.android.domain.exploration.lightweightcheckpointing.ExplorationCheckpointController.OutdatedExplorationCheckpointException import org.oppia.android.domain.oppialogger.LogStorageModule import org.oppia.android.domain.topic.FRACTIONS_EXPLORATION_ID_0 import org.oppia.android.domain.topic.FRACTIONS_EXPLORATION_ID_1 import org.oppia.android.testing.TestLogReportingModule +import org.oppia.android.testing.data.DataProviderTestMonitor import org.oppia.android.testing.environment.TestEnvironmentConfig import org.oppia.android.testing.lightweightcheckpointing.ExplorationCheckpointTestHelper import org.oppia.android.testing.lightweightcheckpointing.FRACTIONS_EXPLORATION_0_TITLE @@ -46,8 +40,6 @@ import org.oppia.android.util.caching.AssetModule import org.oppia.android.util.caching.CacheAssetsLocally import org.oppia.android.util.caching.LoadLessonProtosFromAssets import org.oppia.android.util.caching.TopicListToCache -import org.oppia.android.util.data.AsyncResult -import org.oppia.android.util.data.DataProviders.Companion.toLiveData import org.oppia.android.util.data.DataProvidersInjector import org.oppia.android.util.data.DataProvidersInjectorProvider import org.oppia.android.util.locale.LocaleProdModule @@ -58,8 +50,6 @@ import org.oppia.android.util.logging.LogLevel import org.oppia.android.util.networking.NetworkConnectionUtilDebugModule import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode -import javax.inject.Inject -import javax.inject.Singleton /** * The base exploration id for every exploration used for testing [ExplorationCheckpointController]. @@ -81,47 +71,18 @@ private const val BASE_TEST_EXPLORATION_TITLE = "Test Exploration " * For testing this controller, checkpoints of hypothetical explorations are saved, updated, * retrieved and deleted. These hypothetical explorations are referred to as "test explorations". */ +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) @Config(application = ExplorationCheckpointControllerTest.TestApplication::class) class ExplorationCheckpointControllerTest { - - @Rule - @JvmField - val mockitoRule: MockitoRule = MockitoJUnit.rule() - - @Inject - lateinit var testCoroutineDispatchers: TestCoroutineDispatchers - - @Inject - lateinit var context: Context - - @Inject - lateinit var fakeOppiaClock: FakeOppiaClock - - @Inject - lateinit var explorationCheckpointController: ExplorationCheckpointController - - @Inject - lateinit var explorationCheckpointTestHelper: ExplorationCheckpointTestHelper - - @Mock - lateinit var mockResultObserver: Observer> - - @Captor - lateinit var resultCaptor: ArgumentCaptor> - - @Mock - lateinit var mockExplorationCheckpointObserver: Observer> - - @Captor - lateinit var explorationCheckpointCaptor: ArgumentCaptor> - - @Mock - lateinit var mockCheckpointDetailsObserver: Observer> - - @Captor - lateinit var checkpointDetailsCaptor: ArgumentCaptor> + @Inject lateinit var testCoroutineDispatchers: TestCoroutineDispatchers + @Inject lateinit var context: Context + @Inject lateinit var fakeOppiaClock: FakeOppiaClock + @Inject lateinit var explorationCheckpointController: ExplorationCheckpointController + @Inject lateinit var explorationCheckpointTestHelper: ExplorationCheckpointTestHelper + @Inject lateinit var monitorFactory: DataProviderTestMonitor.Factory private val firstTestProfile = ProfileId.newBuilder().setInternalId(0).build() private val secondTestProfile = ProfileId.newBuilder().setInternalId(1).build() @@ -134,28 +95,27 @@ class ExplorationCheckpointControllerTest { @Test fun testController_saveCheckpoint_databaseNotFull_isSuccessfulWithDatabaseInCorrectState() { - saveCheckpoint(firstTestProfile, index = 0) - assertThat(resultCaptor.value.getOrThrow()).isEqualTo( - CheckpointState.CHECKPOINT_SAVED_DATABASE_NOT_EXCEEDED_LIMIT - ) + val result = saveCheckpoint(firstTestProfile, index = 0) + + assertThat(result).isEqualTo(CheckpointState.CHECKPOINT_SAVED_DATABASE_NOT_EXCEEDED_LIMIT) } @Test fun testController_saveCheckpoint_databaseFull_isSuccessfulWithDatabaseInCorrectState() { saveMultipleCheckpoints(firstTestProfile, numberOfCheckpoints = 2) - saveCheckpoint(firstTestProfile, index = 3) - assertThat(resultCaptor.value.getOrThrow()).isEqualTo( - CheckpointState.CHECKPOINT_SAVED_DATABASE_EXCEEDED_LIMIT - ) + + val result = saveCheckpoint(firstTestProfile, index = 3) + + assertThat(result).isEqualTo(CheckpointState.CHECKPOINT_SAVED_DATABASE_EXCEEDED_LIMIT) } @Test fun testController_databaseFullForFirstTestProfile_checkDatabaseNotFullForSecondTestProfile() { saveMultipleCheckpoints(firstTestProfile, numberOfCheckpoints = 3) - saveCheckpoint(secondTestProfile, index = 0) - assertThat(resultCaptor.value.getOrThrow()).isEqualTo( - CheckpointState.CHECKPOINT_SAVED_DATABASE_NOT_EXCEEDED_LIMIT - ) + + val result = saveCheckpoint(secondTestProfile, index = 0) + + assertThat(result).isEqualTo(CheckpointState.CHECKPOINT_SAVED_DATABASE_NOT_EXCEEDED_LIMIT) } @Test @@ -164,12 +124,14 @@ class ExplorationCheckpointControllerTest { profileId = firstTestProfile, version = FRACTIONS_STORY_0_EXPLORATION_0_CURRENT_VERSION ) - val retrieveCheckpointLiveData = explorationCheckpointController.retrieveExplorationCheckpoint( - firstTestProfile, - FRACTIONS_EXPLORATION_ID_0 - ).toLiveData() - retrieveCheckpointLiveData.observeForever(mockExplorationCheckpointObserver) - verifyMockObserverIsSuccessful(mockExplorationCheckpointObserver, explorationCheckpointCaptor) + + val retrieveCheckpointProvider = + explorationCheckpointController.retrieveExplorationCheckpoint( + firstTestProfile, + FRACTIONS_EXPLORATION_ID_0 + ) + + monitorFactory.waitForNextSuccessfulResult(retrieveCheckpointProvider) } @Test @@ -178,16 +140,15 @@ class ExplorationCheckpointControllerTest { profileId = firstTestProfile, version = FRACTIONS_STORY_0_EXPLORATION_0_CURRENT_VERSION ) - val retrieveCheckpointLiveData = explorationCheckpointController.retrieveExplorationCheckpoint( - firstTestProfile, - FRACTIONS_EXPLORATION_ID_1 - ).toLiveData() - retrieveCheckpointLiveData.observeForever(mockExplorationCheckpointObserver) - verifyMockObserverIsFailure(mockExplorationCheckpointObserver, explorationCheckpointCaptor) - - assertThat(explorationCheckpointCaptor.value.getErrorOrNull()).isInstanceOf( - ExplorationCheckpointController.ExplorationCheckpointNotFoundException::class.java - ) + + val retrieveCheckpointProvider = + explorationCheckpointController.retrieveExplorationCheckpoint( + firstTestProfile, + FRACTIONS_EXPLORATION_ID_1 + ) + + val error = monitorFactory.waitForNextFailureResult(retrieveCheckpointProvider) + assertThat(error).isInstanceOf(ExplorationCheckpointNotFoundException::class.java) } @Test @@ -197,16 +158,14 @@ class ExplorationCheckpointControllerTest { version = FRACTIONS_STORY_0_EXPLORATION_0_CURRENT_VERSION ) - val retrieveCheckpointLiveData = explorationCheckpointController.retrieveExplorationCheckpoint( - secondTestProfile, - FRACTIONS_EXPLORATION_ID_0 - ).toLiveData() - retrieveCheckpointLiveData.observeForever(mockExplorationCheckpointObserver) - verifyMockObserverIsFailure(mockExplorationCheckpointObserver, explorationCheckpointCaptor) + val retrieveCheckpointProvider = + explorationCheckpointController.retrieveExplorationCheckpoint( + secondTestProfile, + FRACTIONS_EXPLORATION_ID_0 + ) - assertThat(explorationCheckpointCaptor.value.getErrorOrNull()).isInstanceOf( - ExplorationCheckpointController.ExplorationCheckpointNotFoundException::class.java - ) + val error = monitorFactory.waitForNextFailureResult(retrieveCheckpointProvider) + assertThat(error).isInstanceOf(ExplorationCheckpointNotFoundException::class.java) } @Test @@ -220,17 +179,15 @@ class ExplorationCheckpointControllerTest { version = FRACTIONS_STORY_0_EXPLORATION_0_CURRENT_VERSION ) - val retrieveCheckpointLiveData = explorationCheckpointController.retrieveExplorationCheckpoint( - firstTestProfile, - FRACTIONS_EXPLORATION_ID_0 - ).toLiveData() - retrieveCheckpointLiveData.observeForever(mockExplorationCheckpointObserver) - verifyMockObserverIsSuccessful(mockExplorationCheckpointObserver, explorationCheckpointCaptor) + val retrieveCheckpointProvider = + explorationCheckpointController.retrieveExplorationCheckpoint( + firstTestProfile, + FRACTIONS_EXPLORATION_ID_0 + ) - val updatedCheckpoint = - explorationCheckpointCaptor.value.getOrDefault(ExplorationCheckpoint.getDefaultInstance()) + val updatedCheckpoint =monitorFactory.waitForNextSuccessfulResult(retrieveCheckpointProvider) assertThat(updatedCheckpoint.pendingStateName) - .matches(FRACTIONS_STORY_0_EXPLORATION_0_SECOND_STATE_NAME) + .isEqualTo(FRACTIONS_STORY_0_EXPLORATION_0_SECOND_STATE_NAME) } @Test @@ -239,34 +196,30 @@ class ExplorationCheckpointControllerTest { profileId = firstTestProfile, version = FRACTIONS_STORY_0_EXPLORATION_0_CURRENT_VERSION ) - explorationCheckpointTestHelper.saveCheckpointForFractionsStory0Exploration1( profileId = firstTestProfile, version = FRACTIONS_STORY_0_EXPLORATION_1_CURRENT_VERSION ) - val oldestCheckpointDetailsLiveData = - explorationCheckpointController - .retrieveOldestSavedExplorationCheckpointDetails(firstTestProfile).toLiveData() - oldestCheckpointDetailsLiveData.observeForever(mockCheckpointDetailsObserver) - verifyMockObserverIsSuccessful(mockCheckpointDetailsObserver, checkpointDetailsCaptor) + val checkpointProvider = + explorationCheckpointController.retrieveOldestSavedExplorationCheckpointDetails( + firstTestProfile + ) - val oldestCheckpointDetails = checkpointDetailsCaptor.value.getOrThrow() + val oldestCheckpointDetails = monitorFactory.waitForNextSuccessfulResult(checkpointProvider) assertThat(oldestCheckpointDetails.explorationId).isEqualTo(FRACTIONS_EXPLORATION_ID_0) assertThat(oldestCheckpointDetails.explorationTitle).isEqualTo(FRACTIONS_EXPLORATION_0_TITLE) } @Test fun testCheckpointController_databaseEmpty_retrieveOldestCheckpointDetails_isFailure() { - val oldestCheckpointDetailsLiveData = - explorationCheckpointController - .retrieveOldestSavedExplorationCheckpointDetails(firstTestProfile).toLiveData() - oldestCheckpointDetailsLiveData.observeForever(mockCheckpointDetailsObserver) - verifyMockObserverIsFailure(mockCheckpointDetailsObserver, checkpointDetailsCaptor) - - assertThat(checkpointDetailsCaptor.value.getErrorOrNull()).isInstanceOf( - ExplorationCheckpointController.ExplorationCheckpointNotFoundException::class.java - ) + val checkpointProvider = + explorationCheckpointController.retrieveOldestSavedExplorationCheckpointDetails( + firstTestProfile + ) + + val error = monitorFactory.waitForNextFailureResult(checkpointProvider) + assertThat(error).isInstanceOf(ExplorationCheckpointNotFoundException::class.java) } @Test @@ -276,62 +229,68 @@ class ExplorationCheckpointControllerTest { version = FRACTIONS_STORY_0_EXPLORATION_0_CURRENT_VERSION ) - val deleteCheckpointLiveData = + val deleteCheckpointProvider = explorationCheckpointController.deleteSavedExplorationCheckpoint( firstTestProfile, FRACTIONS_EXPLORATION_ID_0 - ).toLiveData() - deleteCheckpointLiveData.observeForever(mockResultObserver) - verifyMockObserverIsSuccessful(mockResultObserver, resultCaptor) + ) + + monitorFactory.waitForNextSuccessfulResult(deleteCheckpointProvider) + } + + @Test + fun testCheckpointController_saveCheckpoint_deleteSavedCheckpoint_checkpointWasDeleted() { + explorationCheckpointTestHelper.saveCheckpointForFractionsStory0Exploration0( + profileId = firstTestProfile, + version = FRACTIONS_STORY_0_EXPLORATION_0_CURRENT_VERSION + ) + val deleteCheckpointProvider = + explorationCheckpointController.deleteSavedExplorationCheckpoint( + firstTestProfile, + FRACTIONS_EXPLORATION_ID_0 + ) + monitorFactory.ensureDataProviderExecutes(deleteCheckpointProvider) // Verify that the checkpoint was deleted. - val retrieveCheckpointLiveData = + val retrieveCheckpointProvider = explorationCheckpointController.retrieveExplorationCheckpoint( firstTestProfile, FRACTIONS_EXPLORATION_ID_0 - ).toLiveData() - retrieveCheckpointLiveData.observeForever(mockExplorationCheckpointObserver) - verifyMockObserverIsFailure(mockExplorationCheckpointObserver, explorationCheckpointCaptor) + ) - assertThat(explorationCheckpointCaptor.value.getErrorOrNull()).isInstanceOf( - ExplorationCheckpointController.ExplorationCheckpointNotFoundException::class.java - ) + val error = monitorFactory.waitForNextFailureResult(retrieveCheckpointProvider) + assertThat(error).isInstanceOf(ExplorationCheckpointNotFoundException::class.java) } @Test fun testController_saveCheckpoint_deleteUnsavedCheckpoint_isFailure() { saveCheckpoint(firstTestProfile, index = 0) - val deleteCheckpointLiveData = explorationCheckpointController.deleteSavedExplorationCheckpoint( - firstTestProfile, - FRACTIONS_EXPLORATION_ID_0 - ).toLiveData() - deleteCheckpointLiveData.observeForever(mockResultObserver) - verifyMockObserverIsFailure(mockResultObserver, resultCaptor) + val deleteCheckpointProvider = + explorationCheckpointController.deleteSavedExplorationCheckpoint( + firstTestProfile, + FRACTIONS_EXPLORATION_ID_0 + ) - assertThat(resultCaptor.value.getErrorOrNull()).isInstanceOf( - ExplorationCheckpointController.ExplorationCheckpointNotFoundException::class.java - ) - assertThat(resultCaptor.value.getErrorOrNull()).hasMessageThat() - .contains("No saved checkpoint with explorationId") + val error = monitorFactory.waitForNextFailureResult(deleteCheckpointProvider) + assertThat(error).isInstanceOf(ExplorationCheckpointNotFoundException::class.java) + assertThat(error).hasMessageThat().contains("No saved checkpoint with explorationId") } @Test fun testController_saveCheckpoint_deleteSavedCheckpointFromDifferentProfile_isFailure() { saveCheckpoint(firstTestProfile, index = 0) - val deleteCheckpointLiveData = explorationCheckpointController.deleteSavedExplorationCheckpoint( - secondTestProfile, - createExplorationIdForIndex(0) - ).toLiveData() - deleteCheckpointLiveData.observeForever(mockResultObserver) - verifyMockObserverIsFailure(mockResultObserver, resultCaptor) + val deleteCheckpointProvider = + explorationCheckpointController.deleteSavedExplorationCheckpoint( + secondTestProfile, + createExplorationIdForIndex(0) + ) - assertThat(resultCaptor.value.getErrorOrNull()).isInstanceOf( - ExplorationCheckpointController.ExplorationCheckpointNotFoundException::class.java - ) - assertThat(resultCaptor.value.getErrorOrNull()).hasMessageThat() - .contains("No saved checkpoint with explorationId") + val error = monitorFactory.waitForNextFailureResult(deleteCheckpointProvider) + + assertThat(error).isInstanceOf(ExplorationCheckpointNotFoundException::class.java) + assertThat(error).hasMessageThat().contains("No saved checkpoint with explorationId") } @Test @@ -340,11 +299,14 @@ class ExplorationCheckpointControllerTest { profileId = firstTestProfile, version = FRACTIONS_STORY_0_EXPLORATION_0_CURRENT_VERSION ) - explorationCheckpointController.retrieveExplorationCheckpoint( - firstTestProfile, - FRACTIONS_EXPLORATION_ID_0 - ).toLiveData().observeForever(mockExplorationCheckpointObserver) - verifyMockObserverIsSuccessful(mockExplorationCheckpointObserver, explorationCheckpointCaptor) + + val checkpointProvider = + explorationCheckpointController.retrieveExplorationCheckpoint( + firstTestProfile, + FRACTIONS_EXPLORATION_ID_0 + ) + + monitorFactory.waitForNextSuccessfulResult(checkpointProvider) } @Test @@ -353,67 +315,24 @@ class ExplorationCheckpointControllerTest { profileId = firstTestProfile, version = FRACTIONS_STORY_0_EXPLORATION_0_OLD_VERSION ) - explorationCheckpointController.retrieveExplorationCheckpoint( - firstTestProfile, - FRACTIONS_EXPLORATION_ID_0 - ).toLiveData().observeForever(mockExplorationCheckpointObserver) - verifyMockObserverIsFailure(mockExplorationCheckpointObserver, explorationCheckpointCaptor) - - assertThat(explorationCheckpointCaptor.value.getErrorOrNull()).isInstanceOf( - ExplorationCheckpointController.OutdatedExplorationCheckpointException::class.java - ) - } - private fun verifyMockObserverIsSuccessful( - mockObserver: Observer>, - captor: ArgumentCaptor> - ) { - testCoroutineDispatchers.runCurrent() - verify(mockObserver, atLeastOnce()).onChanged(captor.capture()) - assertThat(captor.value.isSuccess()).isTrue() - } + val checkpointProvider = + explorationCheckpointController.retrieveExplorationCheckpoint( + firstTestProfile, + FRACTIONS_EXPLORATION_ID_0 + ) - private fun verifyMockObserverIsFailure( - mockObserver: Observer>, - captor: ArgumentCaptor> - ) { - testCoroutineDispatchers.runCurrent() - verify(mockObserver, atLeastOnce()).onChanged(captor.capture()) - assertThat(captor.value.isFailure()).isTrue() + val error = monitorFactory.waitForNextFailureResult(checkpointProvider) + assertThat(error).isInstanceOf(OutdatedExplorationCheckpointException::class.java) } - private fun saveCheckpoint( - profileId: ProfileId, - index: Int - ) { - reset(mockResultObserver) - explorationCheckpointController.recordExplorationCheckpoint( + private fun saveCheckpoint(profileId: ProfileId, index: Int): Any? { + val recordProvider = explorationCheckpointController.recordExplorationCheckpoint( profileId = profileId, explorationId = createExplorationIdForIndex(index), explorationCheckpoint = createCheckpoint(index) - ).toLiveData().observeForever(mockResultObserver) - verifyMockObserverIsSuccessful(mockResultObserver, resultCaptor) - } - - /** - * Updates the saved checkpoint for the test exploration specified by the [index] supplied. - * - * For this function to work as intended, it has to be made sure that a checkpoint for the test - * exploration specified by the index already exists in the checkpoint database of that profile. - * - * This function can update the checkpoint of a particular test exploration only once. - */ - private fun saveUpdatedCheckpoint( - profileId: ProfileId, - index: Int - ) { - reset(mockResultObserver) - explorationCheckpointController.recordExplorationCheckpoint( - profileId = profileId, - explorationId = createExplorationIdForIndex(index), - explorationCheckpoint = createUpdatedCheckpoint(index) - ).toLiveData().observeForever(mockResultObserver) - verifyMockObserverIsSuccessful(mockResultObserver, resultCaptor) + ) + return monitorFactory.waitForNextSuccessfulResult(recordProvider) } private fun saveMultipleCheckpoints(profileId: ProfileId, numberOfCheckpoints: Int) { @@ -459,13 +378,6 @@ class ExplorationCheckpointControllerTest { .setStateIndex(0) .build() - private fun createUpdatedCheckpoint(index: Int): ExplorationCheckpoint = - ExplorationCheckpoint.newBuilder() - .setExplorationTitle(createExplorationTitleForIndex(index)) - .setPendingStateName("second_state") - .setStateIndex(1) - .build() - private fun setUpTestApplicationComponent() { ApplicationProvider.getApplicationContext() .inject(this) diff --git a/domain/src/test/java/org/oppia/android/domain/onboarding/AppStartupStateControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/onboarding/AppStartupStateControllerTest.kt index c283cd69adc..5c4fbb96d73 100644 --- a/domain/src/test/java/org/oppia/android/domain/onboarding/AppStartupStateControllerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/onboarding/AppStartupStateControllerTest.kt @@ -3,8 +3,6 @@ package org.oppia.android.domain.onboarding import android.app.Application import android.content.Context import android.os.Bundle -import androidx.arch.core.executor.testing.InstantTaskExecutorRule -import androidx.lifecycle.Observer import androidx.test.core.app.ApplicationProvider import androidx.test.core.content.pm.ApplicationInfoBuilder import androidx.test.core.content.pm.PackageInfoBuilder @@ -14,18 +12,15 @@ import dagger.BindsInstance import dagger.Component import dagger.Module import dagger.Provides -import org.junit.Rule +import java.text.SimpleDateFormat +import java.time.Duration +import java.time.Instant +import java.util.Date +import java.util.Locale +import javax.inject.Inject +import javax.inject.Singleton import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentCaptor -import org.mockito.Captor -import org.mockito.Mock -import org.mockito.Mockito.atLeastOnce -import org.mockito.Mockito.verify -import org.mockito.junit.MockitoJUnit -import org.mockito.junit.MockitoRule -import org.oppia.android.app.model.AppStartupState -import org.oppia.android.app.model.AppStartupState.StartupMode import org.oppia.android.app.model.AppStartupState.StartupMode.APP_IS_DEPRECATED import org.oppia.android.app.model.AppStartupState.StartupMode.USER_IS_ONBOARDED import org.oppia.android.app.model.AppStartupState.StartupMode.USER_NOT_YET_ONBOARDED @@ -33,11 +28,10 @@ import org.oppia.android.app.model.OnboardingState import org.oppia.android.data.persistence.PersistentCacheStore import org.oppia.android.domain.oppialogger.LogStorageModule import org.oppia.android.testing.TestLogReportingModule +import org.oppia.android.testing.data.DataProviderTestMonitor import org.oppia.android.testing.robolectric.RobolectricModule import org.oppia.android.testing.threading.TestCoroutineDispatchers import org.oppia.android.testing.threading.TestDispatcherModule -import org.oppia.android.util.data.AsyncResult -import org.oppia.android.util.data.DataProviders.Companion.toLiveData import org.oppia.android.util.data.DataProvidersInjector import org.oppia.android.util.data.DataProvidersInjectorProvider import org.oppia.android.util.locale.LocaleProdModule @@ -49,43 +43,17 @@ import org.oppia.android.util.networking.NetworkConnectionUtilDebugModule import org.oppia.android.util.system.OppiaClockModule import org.robolectric.Shadows.shadowOf import org.robolectric.annotation.Config -import java.text.SimpleDateFormat -import java.time.Duration -import java.time.Instant -import java.util.Date -import java.util.Locale -import javax.inject.Inject -import javax.inject.Singleton /** Tests for [AppStartupStateController]. */ +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") @RunWith(AndroidJUnit4::class) @Config(application = AppStartupStateControllerTest.TestApplication::class) class AppStartupStateControllerTest { - @Rule - @JvmField - val mockitoRule: MockitoRule = MockitoJUnit.rule() - - @Rule - @JvmField - val executorRule = InstantTaskExecutorRule() - - @Inject - lateinit var context: Context - - @Inject - lateinit var appStartupStateController: AppStartupStateController - - @Inject - lateinit var cacheFactory: PersistentCacheStore.Factory - - @Inject - lateinit var testCoroutineDispatchers: TestCoroutineDispatchers - - @Mock - lateinit var mockOnboardingObserver: Observer> - - @Captor - lateinit var appStartupStateCaptor: ArgumentCaptor> + @Inject lateinit var context: Context + @Inject lateinit var appStartupStateController: AppStartupStateController + @Inject lateinit var testCoroutineDispatchers: TestCoroutineDispatchers + @Inject lateinit var monitorFactory: DataProviderTestMonitor.Factory // TODO(#3792): Remove this usage of Locale (probably by introducing a test utility in the locale // package to generate these strings). @@ -95,29 +63,24 @@ class AppStartupStateControllerTest { fun testController_providesInitialLiveData_indicatesUserHasNotOnboardedTheApp() { setUpDefaultTestApplicationComponent() - val appStartupState = appStartupStateController.getAppStartupState().toLiveData() - appStartupState.observeForever(mockOnboardingObserver) - testCoroutineDispatchers.runCurrent() + val appStartupState = appStartupStateController.getAppStartupState() - verify(mockOnboardingObserver, atLeastOnce()).onChanged(appStartupStateCaptor.capture()) - assertThat(appStartupStateCaptor.value.isSuccess()).isTrue() - assertThat(appStartupStateCaptor.getStartupMode()).isEqualTo(USER_NOT_YET_ONBOARDED) + val mode = monitorFactory.waitForNextSuccessfulResult(appStartupState) + assertThat(mode.startupMode).isEqualTo(USER_NOT_YET_ONBOARDED) } @Test fun testControllerObserver_observedAfterSettingAppOnboarded_providesLiveData_userDidNotOnboardApp() { // ktlint-disable max-line-length setUpDefaultTestApplicationComponent() - val appStartupState = appStartupStateController.getAppStartupState().toLiveData() + val appStartupState = appStartupStateController.getAppStartupState() - appStartupState.observeForever(mockOnboardingObserver) appStartupStateController.markOnboardingFlowCompleted() testCoroutineDispatchers.runCurrent() // The result should not indicate that the user onboarded the app because markUserOnboardedApp // does not notify observers of the change. - verify(mockOnboardingObserver, atLeastOnce()).onChanged(appStartupStateCaptor.capture()) - assertThat(appStartupStateCaptor.value.isSuccess()).isTrue() - assertThat(appStartupStateCaptor.getStartupMode()).isEqualTo(USER_NOT_YET_ONBOARDED) + val mode = monitorFactory.waitForNextSuccessfulResult(appStartupState) + assertThat(mode.startupMode).isEqualTo(USER_NOT_YET_ONBOARDED) } @Test @@ -130,15 +93,12 @@ class AppStartupStateControllerTest { // Create the application after previous arrangement to simulate a re-creation. setUpDefaultTestApplicationComponent() - val appStartupState = appStartupStateController.getAppStartupState().toLiveData() - appStartupState.observeForever(mockOnboardingObserver) - testCoroutineDispatchers.runCurrent() + val appStartupState = appStartupStateController.getAppStartupState() // The app should be considered onboarded since a new LiveData instance was observed after // marking the app as onboarded. - verify(mockOnboardingObserver, atLeastOnce()).onChanged(appStartupStateCaptor.capture()) - assertThat(appStartupStateCaptor.value.isSuccess()).isTrue() - assertThat(appStartupStateCaptor.getStartupMode()).isEqualTo(USER_IS_ONBOARDED) + val mode = monitorFactory.waitForNextSuccessfulResult(appStartupState) + assertThat(mode.startupMode).isEqualTo(USER_IS_ONBOARDED) } @Test @@ -162,14 +122,11 @@ class AppStartupStateControllerTest { // Create the application after previous arrangement to simulate a re-creation. setUpDefaultTestApplicationComponent() - val appStartupState = appStartupStateController.getAppStartupState().toLiveData() - appStartupState.observeForever(mockOnboardingObserver) - testCoroutineDispatchers.runCurrent() + val appStartupState = appStartupStateController.getAppStartupState() // The app should be considered not yet onboarded since the previous history was cleared. - verify(mockOnboardingObserver, atLeastOnce()).onChanged(appStartupStateCaptor.capture()) - assertThat(appStartupStateCaptor.value.isSuccess()).isTrue() - assertThat(appStartupStateCaptor.getStartupMode()).isEqualTo(USER_NOT_YET_ONBOARDED) + val mode = monitorFactory.waitForNextSuccessfulResult(appStartupState) + assertThat(mode.startupMode).isEqualTo(USER_NOT_YET_ONBOARDED) } @Test @@ -177,13 +134,10 @@ class AppStartupStateControllerTest { setUpTestApplicationComponent() setUpOppiaApplication(expirationEnabled = true, expDate = dateStringAfterToday()) - val appStartupState = appStartupStateController.getAppStartupState().toLiveData() - appStartupState.observeForever(mockOnboardingObserver) - testCoroutineDispatchers.runCurrent() + val appStartupState = appStartupStateController.getAppStartupState() - verify(mockOnboardingObserver, atLeastOnce()).onChanged(appStartupStateCaptor.capture()) - assertThat(appStartupStateCaptor.value.isSuccess()).isTrue() - assertThat(appStartupStateCaptor.getStartupMode()).isEqualTo(USER_NOT_YET_ONBOARDED) + val mode = monitorFactory.waitForNextSuccessfulResult(appStartupState) + assertThat(mode.startupMode).isEqualTo(USER_NOT_YET_ONBOARDED) } @Test @@ -191,13 +145,10 @@ class AppStartupStateControllerTest { setUpTestApplicationComponent() setUpOppiaApplication(expirationEnabled = true, expDate = dateStringForToday()) - val appStartupState = appStartupStateController.getAppStartupState().toLiveData() - appStartupState.observeForever(mockOnboardingObserver) - testCoroutineDispatchers.runCurrent() + val appStartupState = appStartupStateController.getAppStartupState() - verify(mockOnboardingObserver, atLeastOnce()).onChanged(appStartupStateCaptor.capture()) - assertThat(appStartupStateCaptor.value.isSuccess()).isTrue() - assertThat(appStartupStateCaptor.getStartupMode()).isEqualTo(APP_IS_DEPRECATED) + val mode = monitorFactory.waitForNextSuccessfulResult(appStartupState) + assertThat(mode.startupMode).isEqualTo(APP_IS_DEPRECATED) } @Test @@ -205,13 +156,10 @@ class AppStartupStateControllerTest { setUpTestApplicationComponent() setUpOppiaApplication(expirationEnabled = true, expDate = dateStringBeforeToday()) - val appStartupState = appStartupStateController.getAppStartupState().toLiveData() - appStartupState.observeForever(mockOnboardingObserver) - testCoroutineDispatchers.runCurrent() + val appStartupState = appStartupStateController.getAppStartupState() - verify(mockOnboardingObserver, atLeastOnce()).onChanged(appStartupStateCaptor.capture()) - assertThat(appStartupStateCaptor.value.isSuccess()).isTrue() - assertThat(appStartupStateCaptor.getStartupMode()).isEqualTo(APP_IS_DEPRECATED) + val mode = monitorFactory.waitForNextSuccessfulResult(appStartupState) + assertThat(mode.startupMode).isEqualTo(APP_IS_DEPRECATED) } @Test @@ -219,13 +167,10 @@ class AppStartupStateControllerTest { setUpTestApplicationComponent() setUpOppiaApplication(expirationEnabled = false, expDate = dateStringBeforeToday()) - val appStartupState = appStartupStateController.getAppStartupState().toLiveData() - appStartupState.observeForever(mockOnboardingObserver) - testCoroutineDispatchers.runCurrent() + val appStartupState = appStartupStateController.getAppStartupState() - verify(mockOnboardingObserver, atLeastOnce()).onChanged(appStartupStateCaptor.capture()) - assertThat(appStartupStateCaptor.value.isSuccess()).isTrue() - assertThat(appStartupStateCaptor.getStartupMode()).isEqualTo(USER_NOT_YET_ONBOARDED) + val mode = monitorFactory.waitForNextSuccessfulResult(appStartupState) + assertThat(mode.startupMode).isEqualTo(USER_NOT_YET_ONBOARDED) } @Test @@ -240,13 +185,10 @@ class AppStartupStateControllerTest { setUpTestApplicationComponent() setUpOppiaApplication(expirationEnabled = true, expDate = dateStringAfterToday()) - val appStartupState = appStartupStateController.getAppStartupState().toLiveData() - appStartupState.observeForever(mockOnboardingObserver) - testCoroutineDispatchers.runCurrent() + val appStartupState = appStartupStateController.getAppStartupState() - verify(mockOnboardingObserver, atLeastOnce()).onChanged(appStartupStateCaptor.capture()) - assertThat(appStartupStateCaptor.value.isSuccess()).isTrue() - assertThat(appStartupStateCaptor.getStartupMode()).isEqualTo(USER_NOT_YET_ONBOARDED) + val mode = monitorFactory.waitForNextSuccessfulResult(appStartupState) + assertThat(mode.startupMode).isEqualTo(USER_NOT_YET_ONBOARDED) } @Test @@ -261,13 +203,10 @@ class AppStartupStateControllerTest { setUpTestApplicationComponent() setUpOppiaApplication(expirationEnabled = true, expDate = dateStringBeforeToday()) - val appStartupState = appStartupStateController.getAppStartupState().toLiveData() - appStartupState.observeForever(mockOnboardingObserver) - testCoroutineDispatchers.runCurrent() + val appStartupState = appStartupStateController.getAppStartupState() - verify(mockOnboardingObserver, atLeastOnce()).onChanged(appStartupStateCaptor.capture()) - assertThat(appStartupStateCaptor.value.isSuccess()).isTrue() - assertThat(appStartupStateCaptor.getStartupMode()).isEqualTo(APP_IS_DEPRECATED) + val mode = monitorFactory.waitForNextSuccessfulResult(appStartupState) + assertThat(mode.startupMode).isEqualTo(APP_IS_DEPRECATED) } @Test @@ -285,14 +224,11 @@ class AppStartupStateControllerTest { setUpTestApplicationComponent() setUpOppiaApplication(expirationEnabled = true, expDate = dateStringAfterToday()) - val appStartupState = appStartupStateController.getAppStartupState().toLiveData() - appStartupState.observeForever(mockOnboardingObserver) - testCoroutineDispatchers.runCurrent() + val appStartupState = appStartupStateController.getAppStartupState() // The user should be considered onboarded, but the app is not yet deprecated. - verify(mockOnboardingObserver, atLeastOnce()).onChanged(appStartupStateCaptor.capture()) - assertThat(appStartupStateCaptor.value.isSuccess()).isTrue() - assertThat(appStartupStateCaptor.getStartupMode()).isEqualTo(USER_IS_ONBOARDED) + val mode = monitorFactory.waitForNextSuccessfulResult(appStartupState) + assertThat(mode.startupMode).isEqualTo(USER_IS_ONBOARDED) } @Test @@ -310,14 +246,11 @@ class AppStartupStateControllerTest { setUpTestApplicationComponent() setUpOppiaApplication(expirationEnabled = true, expDate = dateStringBeforeToday()) - val appStartupState = appStartupStateController.getAppStartupState().toLiveData() - appStartupState.observeForever(mockOnboardingObserver) - testCoroutineDispatchers.runCurrent() + val appStartupState = appStartupStateController.getAppStartupState() // Despite the user completing the onboarding flow, the app is still deprecated. - verify(mockOnboardingObserver, atLeastOnce()).onChanged(appStartupStateCaptor.capture()) - assertThat(appStartupStateCaptor.value.isSuccess()).isTrue() - assertThat(appStartupStateCaptor.getStartupMode()).isEqualTo(APP_IS_DEPRECATED) + val mode = monitorFactory.waitForNextSuccessfulResult(appStartupState) + assertThat(mode.startupMode).isEqualTo(APP_IS_DEPRECATED) } private fun setUpTestApplicationComponent() { @@ -400,10 +333,6 @@ class AppStartupStateControllerTest { packageManager.installPackage(packageInfo) } - private fun ArgumentCaptor>.getStartupMode(): StartupMode { - return value.getOrThrow().startupMode - } - // TODO(#89): Move this to a common test application component. @Module class TestModule { diff --git a/domain/src/test/java/org/oppia/android/domain/oppialogger/analytics/AnalyticsControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/oppialogger/analytics/AnalyticsControllerTest.kt index 3f9c6e417ef..54dd94d3a0c 100644 --- a/domain/src/test/java/org/oppia/android/domain/oppialogger/analytics/AnalyticsControllerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/oppialogger/analytics/AnalyticsControllerTest.kt @@ -2,7 +2,6 @@ package org.oppia.android.domain.oppialogger.analytics import android.app.Application import android.content.Context -import androidx.lifecycle.Observer import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat @@ -11,16 +10,8 @@ import dagger.Component import dagger.Module import dagger.Provides import org.junit.Before -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentCaptor -import org.mockito.Captor -import org.mockito.Mock -import org.mockito.Mockito.atLeastOnce -import org.mockito.Mockito.verify -import org.mockito.junit.MockitoJUnit -import org.mockito.junit.MockitoRule import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.ACTIVITYCONTEXT_NOT_SET import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.CONCEPT_CARD_CONTEXT import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.EXPLORATION_CONTEXT @@ -30,18 +21,14 @@ import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.STORY_CO import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.TOPIC_CONTEXT import org.oppia.android.app.model.EventLog.EventAction import org.oppia.android.app.model.EventLog.Priority -import org.oppia.android.app.model.OppiaEventLogs import org.oppia.android.domain.oppialogger.EventLogStorageCacheSize import org.oppia.android.domain.oppialogger.OppiaLogger import org.oppia.android.testing.FakeEventLogger import org.oppia.android.testing.TestLogReportingModule import org.oppia.android.testing.robolectric.RobolectricModule -import org.oppia.android.testing.threading.TestCoroutineDispatchers import org.oppia.android.testing.threading.TestDispatcherModule import org.oppia.android.testing.time.FakeOppiaClockModule -import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.data.DataProviders -import org.oppia.android.util.data.DataProviders.Companion.toLiveData import org.oppia.android.util.data.DataProvidersInjector import org.oppia.android.util.data.DataProvidersInjectorProvider import org.oppia.android.util.locale.LocaleProdModule @@ -56,6 +43,7 @@ import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode import javax.inject.Inject import javax.inject.Singleton +import org.oppia.android.testing.data.DataProviderTestMonitor private const val TEST_TIMESTAMP = 1556094120000 private const val TEST_TOPIC_ID = "test_topicId" @@ -66,38 +54,18 @@ private const val TEST_SKILL_ID = "test_skillId" private const val TEST_SKILL_LIST_ID = "test_skillListId" private const val TEST_SUB_TOPIC_ID = 1 +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) @Config(application = AnalyticsControllerTest.TestApplication::class) class AnalyticsControllerTest { - - @Rule - @JvmField - val mockitoRule: MockitoRule = MockitoJUnit.rule() - - @Inject - lateinit var analyticsController: AnalyticsController - - @Inject - lateinit var oppiaLogger: OppiaLogger - - @Inject - lateinit var networkConnectionUtil: NetworkConnectionDebugUtil - - @Inject - lateinit var fakeEventLogger: FakeEventLogger - - @Inject - lateinit var testCoroutineDispatchers: TestCoroutineDispatchers - - @Inject - lateinit var dataProviders: DataProviders - - @Mock - lateinit var mockOppiaEventLogsObserver: Observer> - - @Captor - lateinit var oppiaEventLogsResultCaptor: ArgumentCaptor> + @Inject lateinit var analyticsController: AnalyticsController + @Inject lateinit var oppiaLogger: OppiaLogger + @Inject lateinit var networkConnectionUtil: NetworkConnectionDebugUtil + @Inject lateinit var fakeEventLogger: FakeEventLogger + @Inject lateinit var dataProviders: DataProviders + @Inject lateinit var monitorFactory: DataProviderTestMonitor.Factory @Before fun setUp() { @@ -360,7 +328,8 @@ class AnalyticsControllerTest { .isEqualTo(ACTIVITYCONTEXT_NOT_SET) } - // TODO(#3621): Addition of tests tracking behaviour of the controller after uploading of logs to the remote service. + // TODO(#3621): Addition of tests tracking behaviour of the controller after uploading of logs to + // the remote service. @Test fun testController_logTransitionEvent_withNoNetwork_checkLogsEventToStore() { @@ -376,15 +345,9 @@ class AnalyticsControllerTest { ) ) - val eventLogs = analyticsController.getEventLogStore().toLiveData() - eventLogs.observeForever(this.mockOppiaEventLogsObserver) - testCoroutineDispatchers.advanceUntilIdle() - verify( - this.mockOppiaEventLogsObserver, - atLeastOnce() - ).onChanged(oppiaEventLogsResultCaptor.capture()) + val eventLogsProvider = analyticsController.getEventLogStore() - val eventLog = oppiaEventLogsResultCaptor.value.getOrThrow().getEventLog(0) + val eventLog = monitorFactory.waitForNextSuccessfulResult(eventLogsProvider).getEventLog(0) // ESSENTIAL priority confirms that the event logged is a transition event. assertThat(eventLog.priority).isEqualTo(Priority.ESSENTIAL) assertThat(eventLog.context.activityContextCase).isEqualTo(QUESTION_CONTEXT) @@ -406,15 +369,9 @@ class AnalyticsControllerTest { ) ) - val eventLogs = analyticsController.getEventLogStore().toLiveData() - eventLogs.observeForever(this.mockOppiaEventLogsObserver) - testCoroutineDispatchers.advanceUntilIdle() - verify( - this.mockOppiaEventLogsObserver, - atLeastOnce() - ).onChanged(oppiaEventLogsResultCaptor.capture()) + val eventLogsProvider = analyticsController.getEventLogStore() - val eventLog = oppiaEventLogsResultCaptor.value.getOrThrow().getEventLog(0) + val eventLog = monitorFactory.waitForNextSuccessfulResult(eventLogsProvider).getEventLog(0) // OPTIONAL priority confirms that the event logged is a click event. assertThat(eventLog.priority).isEqualTo(Priority.OPTIONAL) assertThat(eventLog.context.activityContextCase).isEqualTo(QUESTION_CONTEXT) @@ -427,16 +384,10 @@ class AnalyticsControllerTest { networkConnectionUtil.setCurrentConnectionStatus(NONE) logMultipleEvents() - val eventLogs = analyticsController.getEventLogStore().toLiveData() - eventLogs.observeForever(this.mockOppiaEventLogsObserver) - testCoroutineDispatchers.advanceUntilIdle() - verify( - this.mockOppiaEventLogsObserver, - atLeastOnce() - ).onChanged(oppiaEventLogsResultCaptor.capture()) + val eventLogsProvider = analyticsController.getEventLogStore() - val eventLogStoreSize = oppiaEventLogsResultCaptor.value.getOrThrow().eventLogList.size - assertThat(eventLogStoreSize).isEqualTo(2) + val eventLogs = monitorFactory.waitForNextSuccessfulResult(eventLogsProvider) + assertThat(eventLogs.eventLogList).hasSize(2) } @Test @@ -463,16 +414,11 @@ class AnalyticsControllerTest { ) ) - val eventLogs = analyticsController.getEventLogStore().toLiveData() - eventLogs.observeForever(this.mockOppiaEventLogsObserver) - testCoroutineDispatchers.advanceUntilIdle() - verify( - this.mockOppiaEventLogsObserver, - atLeastOnce() - ).onChanged(oppiaEventLogsResultCaptor.capture()) + val eventLogsProvider = analyticsController.getEventLogStore() - val firstEventLog = oppiaEventLogsResultCaptor.value.getOrThrow().getEventLog(0) - val secondEventLog = oppiaEventLogsResultCaptor.value.getOrThrow().getEventLog(1) + val eventLogs = monitorFactory.waitForNextSuccessfulResult(eventLogsProvider) + val firstEventLog = eventLogs.getEventLog(0) + val secondEventLog = eventLogs.getEventLog(1) // OPTIONAL priority confirms that the event logged is a click event. assertThat(firstEventLog.priority).isEqualTo(Priority.OPTIONAL) @@ -504,16 +450,10 @@ class AnalyticsControllerTest { ) ) - val cachedEventLogs = analyticsController.getEventLogStore().toLiveData() - cachedEventLogs.observeForever(this.mockOppiaEventLogsObserver) - testCoroutineDispatchers.advanceUntilIdle() - verify( - this.mockOppiaEventLogsObserver, - atLeastOnce() - ).onChanged(oppiaEventLogsResultCaptor.capture()) + val logsProvider = analyticsController.getEventLogStore() val uploadedEventLog = fakeEventLogger.getMostRecentEvent() - val cachedEventLog = oppiaEventLogsResultCaptor.value.getOrThrow().getEventLog(0) + val cachedEventLog = monitorFactory.waitForNextSuccessfulResult(logsProvider).getEventLog(0) // ESSENTIAL priority confirms that the event logged is a transition event. assertThat(uploadedEventLog.priority).isEqualTo(Priority.ESSENTIAL) @@ -533,25 +473,23 @@ class AnalyticsControllerTest { networkConnectionUtil.setCurrentConnectionStatus(NONE) logMultipleEvents() - val cachedEventLogs = analyticsController.getEventLogStore().toLiveData() - cachedEventLogs.observeForever(this.mockOppiaEventLogsObserver) - testCoroutineDispatchers.advanceUntilIdle() - verify( - this.mockOppiaEventLogsObserver, - atLeastOnce() - ).onChanged(oppiaEventLogsResultCaptor.capture()) - - val firstEventLog = oppiaEventLogsResultCaptor.value.getOrThrow().getEventLog(0) - val secondEventLog = oppiaEventLogsResultCaptor.value.getOrThrow().getEventLog(1) - val eventLogStoreSize = oppiaEventLogsResultCaptor.value.getOrThrow().eventLogList.size - assertThat(eventLogStoreSize).isEqualTo(2) - // In this case, 3 ESSENTIAL and 1 OPTIONAL event was logged. So while pruning, none of the retained logs should have OPTIONAL priority. + val logsProvider = analyticsController.getEventLogStore() + + val eventLogs = monitorFactory.waitForNextSuccessfulResult(logsProvider) + val firstEventLog = eventLogs.getEventLog(0) + val secondEventLog = eventLogs.getEventLog(1) + assertThat(eventLogs.eventLogList).hasSize(2) + // In this case, 3 ESSENTIAL and 1 OPTIONAL event was logged. So while pruning, none of the + // retained logs should have OPTIONAL priority. assertThat(firstEventLog.priority).isNotEqualTo(Priority.OPTIONAL) assertThat(secondEventLog.priority).isNotEqualTo(Priority.OPTIONAL) - // If we analyse the implementation of logMultipleEvents(), we can see that record pruning will begin from the logging of the third record. - // At first, the second event log will be removed as it has OPTIONAL priority and the event logged at the third place will become the event record at the second place in the store. - // When the forth event gets logged then the pruning will be purely based on timestamp of the event as both event logs have ESSENTIAL priority. - // As the third event's timestamp was lesser than that of the first event, it will be pruned from the store and the forth event will become the second event in the store. + // If we analyse the implementation of logMultipleEvents(), we can see that record pruning will + // begin from the logging of the third record. At first, the second event log will be removed as + // it has OPTIONAL priority and the event logged at the third place will become the event record + // at the second place in the store. When the forth event gets logged then the pruning will be + // purely based on timestamp of the event as both event logs have ESSENTIAL priority. As the + // third event's timestamp was lesser than that of the first event, it will be pruned from the + // store and the forth event will become the second event in the store. assertThat(firstEventLog.timestamp).isEqualTo(1556094120000) assertThat(secondEventLog.timestamp).isEqualTo(1556094100000) } diff --git a/domain/src/test/java/org/oppia/android/domain/oppialogger/exceptions/ExceptionsControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/oppialogger/exceptions/ExceptionsControllerTest.kt index 656263a82a0..fc7da13ceb7 100644 --- a/domain/src/test/java/org/oppia/android/domain/oppialogger/exceptions/ExceptionsControllerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/oppialogger/exceptions/ExceptionsControllerTest.kt @@ -2,7 +2,6 @@ package org.oppia.android.domain.oppialogger.exceptions import android.app.Application import android.content.Context -import androidx.lifecycle.Observer import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat @@ -10,29 +9,21 @@ import dagger.BindsInstance import dagger.Component import dagger.Module import dagger.Provides +import javax.inject.Inject +import javax.inject.Singleton import org.junit.Before -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentCaptor -import org.mockito.Captor -import org.mockito.Mock -import org.mockito.Mockito.atLeastOnce -import org.mockito.Mockito.verify -import org.mockito.junit.MockitoJUnit -import org.mockito.junit.MockitoRule import org.oppia.android.app.model.ExceptionLog.ExceptionType -import org.oppia.android.app.model.OppiaExceptionLogs import org.oppia.android.domain.oppialogger.ExceptionLogStorageCacheSize import org.oppia.android.testing.FakeExceptionLogger import org.oppia.android.testing.TestLogReportingModule +import org.oppia.android.testing.data.DataProviderTestMonitor import org.oppia.android.testing.robolectric.RobolectricModule import org.oppia.android.testing.threading.TestCoroutineDispatchers import org.oppia.android.testing.threading.TestDispatcherModule import org.oppia.android.testing.time.FakeOppiaClockModule -import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.data.DataProviders -import org.oppia.android.util.data.DataProviders.Companion.toLiveData import org.oppia.android.util.data.DataProvidersInjector import org.oppia.android.util.data.DataProvidersInjectorProvider import org.oppia.android.util.locale.LocaleProdModule @@ -45,43 +36,24 @@ import org.oppia.android.util.networking.NetworkConnectionUtil.ProdConnectionSta import org.oppia.android.util.networking.NetworkConnectionUtilDebugModule import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode -import javax.inject.Inject -import javax.inject.Singleton private const val TEST_TIMESTAMP_IN_MILLIS_ONE = 1556094120000 private const val TEST_TIMESTAMP_IN_MILLIS_TWO = 1556094110000 private const val TEST_TIMESTAMP_IN_MILLIS_THREE = 1556094100000 private const val TEST_TIMESTAMP_IN_MILLIS_FOUR = 1556094000000 +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) @Config(application = ExceptionsControllerTest.TestApplication::class) class ExceptionsControllerTest { - - @Rule - @JvmField - val mockitoRule: MockitoRule = MockitoJUnit.rule() - - @Inject - lateinit var dataProviders: DataProviders - - @Inject - lateinit var exceptionsController: ExceptionsController - - @Inject - lateinit var networkConnectionUtil: NetworkConnectionDebugUtil - - @Inject - lateinit var fakeExceptionLogger: FakeExceptionLogger - - @Inject - lateinit var testCoroutineDispatchers: TestCoroutineDispatchers - - @Mock - lateinit var mockOppiaExceptionLogsObserver: Observer> - - @Captor - lateinit var oppiaExceptionLogsResultCaptor: ArgumentCaptor> + @Inject lateinit var dataProviders: DataProviders + @Inject lateinit var exceptionsController: ExceptionsController + @Inject lateinit var networkConnectionUtil: NetworkConnectionDebugUtil + @Inject lateinit var fakeExceptionLogger: FakeExceptionLogger + @Inject lateinit var testCoroutineDispatchers: TestCoroutineDispatchers + @Inject lateinit var monitorFactory: DataProviderTestMonitor.Factory @Before fun setUp() { @@ -108,7 +80,8 @@ class ExceptionsControllerTest { assertThat(exceptionLogged).isEqualTo(exceptionThrown) } - // TODO(#3621): Addition of tests tracking behaviour of the controller after uploading of logs to the remote service. + // TODO(#3621): Addition of tests tracking behaviour of the controller after uploading of logs to + // the remote service. @Test fun testController_logException_nonFatal_withNoNetwork_logsToCacheStore() { @@ -116,17 +89,10 @@ class ExceptionsControllerTest { networkConnectionUtil.setCurrentConnectionStatus(NONE) exceptionsController.logNonFatalException(exceptionThrown, TEST_TIMESTAMP_IN_MILLIS_ONE) - val cachedExceptions = - exceptionsController.getExceptionLogStore().toLiveData() - cachedExceptions.observeForever(mockOppiaExceptionLogsObserver) - testCoroutineDispatchers.advanceUntilIdle() + val cachedExceptionsProvider = exceptionsController.getExceptionLogStore() - verify( - mockOppiaExceptionLogsObserver, - atLeastOnce() - ).onChanged(oppiaExceptionLogsResultCaptor.capture()) - - val exceptionLog = oppiaExceptionLogsResultCaptor.value.getOrThrow().getExceptionLog(0) + val exceptionsLog = monitorFactory.waitForNextSuccessfulResult(cachedExceptionsProvider) + val exceptionLog = exceptionsLog.getExceptionLog(0) val exception = exceptionLog.toException() val thrownExceptionStackTraceElems = exception.stackTrace.extractRelevantDetails() val thrownCauseExceptionStackTraceElems = exception.cause?.stackTrace?.extractRelevantDetails() @@ -148,15 +114,10 @@ class ExceptionsControllerTest { val exceptionThrown = Exception("TEST MESSAGE", Throwable("TEST")) exceptionsController.logFatalException(exceptionThrown, TEST_TIMESTAMP_IN_MILLIS_ONE) - val cachedExceptions = - exceptionsController.getExceptionLogStore().toLiveData() - cachedExceptions.observeForever(mockOppiaExceptionLogsObserver) - testCoroutineDispatchers.advanceUntilIdle() - - verify(mockOppiaExceptionLogsObserver, atLeastOnce()) - .onChanged(oppiaExceptionLogsResultCaptor.capture()) + val cachedExceptionsProvider = exceptionsController.getExceptionLogStore() - val exceptionLog = oppiaExceptionLogsResultCaptor.value.getOrThrow().getExceptionLog(0) + val exceptionsLog = monitorFactory.waitForNextSuccessfulResult(cachedExceptionsProvider) + val exceptionLog = exceptionsLog.getExceptionLog(0) val exception = exceptionLog.toException() val thrownExceptionStackTraceElems = exception.stackTrace.extractRelevantDetails() val thrownCauseExceptionStackTraceElems = exception.cause?.stackTrace?.extractRelevantDetails() @@ -193,25 +154,25 @@ class ExceptionsControllerTest { TEST_TIMESTAMP_IN_MILLIS_FOUR ) - val cachedExceptions = - exceptionsController.getExceptionLogStore().toLiveData() - cachedExceptions.observeForever(mockOppiaExceptionLogsObserver) - testCoroutineDispatchers.advanceUntilIdle() + val cachedExceptionsProvider = exceptionsController.getExceptionLogStore() - verify(mockOppiaExceptionLogsObserver, atLeastOnce()) - .onChanged(oppiaExceptionLogsResultCaptor.capture()) + val exceptionsLog = monitorFactory.waitForNextSuccessfulResult(cachedExceptionsProvider) + val exceptionOne = exceptionsLog.getExceptionLog(0) + val exceptionTwo = exceptionsLog.getExceptionLog(1) - val exceptionOne = oppiaExceptionLogsResultCaptor.value.getOrThrow().getExceptionLog(0) - val exceptionTwo = oppiaExceptionLogsResultCaptor.value.getOrThrow().getExceptionLog(1) - - // In this case, 3 fatal and 1 non-fatal exceptions were logged. The order of logging was fatal->non-fatal->fatal->fatal. - // So after pruning, none of the retained logs should have non-fatal exception type. + // In this case, 3 fatal and 1 non-fatal exceptions were logged. The order of logging was + // fatal->non-fatal->fatal->fatal. So after pruning, none of the retained logs should have + // non-fatal exception type. assertThat(exceptionOne.exceptionType).isNotEqualTo(ExceptionType.NON_FATAL) assertThat(exceptionTwo.exceptionType).isNotEqualTo(ExceptionType.NON_FATAL) - // If we analyse the order of logging of exceptions, we can see that record pruning will begin from the logging of the third record. - // At first, the second exception log will be removed as it has non-fatal exception type and the exception logged at the third place will become the exception record at the second place in the store. - // When the forth exception gets logged then the pruning will be purely based on timestamp of the exception as both exception logs have fatal exception type. - // As the third exceptions's timestamp was lesser than that of the first event, it will be pruned from the store and the forth exception will become the second exception in the store. + // If we analyse the order of logging of exceptions, we can see that record pruning will begin + // from the logging of the third record. At first, the second exception log will be removed as + // it has non-fatal exception type and the exception logged at the third place will become the + // exception record at the second place in the store. When the forth exception gets logged then + // the pruning will be purely based on timestamp of the exception as both exception logs have + // fatal exception type. As the third exceptions's timestamp was lesser than that of the first + // event, it will be pruned from the store and the forth exception will become the second + // exception in the store. assertThat(exceptionOne.timestampInMillis).isEqualTo(TEST_TIMESTAMP_IN_MILLIS_ONE) assertThat(exceptionTwo.timestampInMillis).isEqualTo(TEST_TIMESTAMP_IN_MILLIS_FOUR) assertThat(exceptionOne.message).isEqualTo("TEST1") @@ -235,16 +196,10 @@ class ExceptionsControllerTest { TEST_TIMESTAMP_IN_MILLIS_THREE ) - val cachedExceptions = - exceptionsController.getExceptionLogStore().toLiveData() - cachedExceptions.observeForever(mockOppiaExceptionLogsObserver) - testCoroutineDispatchers.advanceUntilIdle() - - verify(mockOppiaExceptionLogsObserver, atLeastOnce()) - .onChanged(oppiaExceptionLogsResultCaptor.capture()) - val cacheStoreSize = oppiaExceptionLogsResultCaptor.value.getOrThrow().exceptionLogList.size + val cachedExceptionsProvider = exceptionsController.getExceptionLogStore() - assertThat(cacheStoreSize).isEqualTo(2) + val exceptionsLog = monitorFactory.waitForNextSuccessfulResult(cachedExceptionsProvider) + assertThat(exceptionsLog.exceptionLogList).hasSize(2) } @Test @@ -254,16 +209,11 @@ class ExceptionsControllerTest { networkConnectionUtil.setCurrentConnectionStatus(NONE) exceptionsController.logFatalException(exceptionThrown, TEST_TIMESTAMP_IN_MILLIS_ONE) - val cachedExceptions = - exceptionsController.getExceptionLogStore().toLiveData() - cachedExceptions.observeForever(mockOppiaExceptionLogsObserver) - testCoroutineDispatchers.advanceUntilIdle() + val cachedExceptionsProvider = exceptionsController.getExceptionLogStore() - verify(mockOppiaExceptionLogsObserver, atLeastOnce()) - .onChanged(oppiaExceptionLogsResultCaptor.capture()) + val exceptionsLog = monitorFactory.waitForNextSuccessfulResult(cachedExceptionsProvider) val exceptionFromRemoteService = fakeExceptionLogger.getMostRecentException() - val exceptionFromCacheStorage = - oppiaExceptionLogsResultCaptor.value.getOrThrow().getExceptionLog(0) + val exceptionFromCacheStorage = exceptionsLog.getExceptionLog(0) val exception = exceptionFromCacheStorage.toException() val thrownExceptionStackTraceElems = exception.stackTrace.extractRelevantDetails() val thrownCauseExceptionStackTraceElems = exception.cause?.stackTrace?.extractRelevantDetails() @@ -287,15 +237,11 @@ class ExceptionsControllerTest { exceptionsController.logNonFatalException(exceptionThrown, TEST_TIMESTAMP_IN_MILLIS_ONE) exceptionsController.logFatalException(exceptionThrown, TEST_TIMESTAMP_IN_MILLIS_ONE) - val cachedExceptions = - exceptionsController.getExceptionLogStore().toLiveData() - cachedExceptions.observeForever(mockOppiaExceptionLogsObserver) - testCoroutineDispatchers.advanceUntilIdle() + val cachedExceptionsProvider = exceptionsController.getExceptionLogStore() - verify(mockOppiaExceptionLogsObserver, atLeastOnce()) - .onChanged(oppiaExceptionLogsResultCaptor.capture()) - val exceptionOne = oppiaExceptionLogsResultCaptor.value.getOrThrow().getExceptionLog(0) - val exceptionTwo = oppiaExceptionLogsResultCaptor.value.getOrThrow().getExceptionLog(1) + val exceptionsLog = monitorFactory.waitForNextSuccessfulResult(cachedExceptionsProvider) + val exceptionOne = exceptionsLog.getExceptionLog(0) + val exceptionTwo = exceptionsLog.getExceptionLog(1) assertThat(exceptionOne.exceptionType).isEqualTo(ExceptionType.NON_FATAL) assertThat(exceptionTwo.exceptionType).isEqualTo(ExceptionType.FATAL) } @@ -306,15 +252,10 @@ class ExceptionsControllerTest { val exceptionThrown = Exception() exceptionsController.logNonFatalException(exceptionThrown, TEST_TIMESTAMP_IN_MILLIS_ONE) - val cachedExceptions = - exceptionsController.getExceptionLogStore().toLiveData() - cachedExceptions.observeForever(mockOppiaExceptionLogsObserver) - testCoroutineDispatchers.advanceUntilIdle() + val cachedExceptionsProvider = exceptionsController.getExceptionLogStore() - verify(mockOppiaExceptionLogsObserver, atLeastOnce()) - .onChanged(oppiaExceptionLogsResultCaptor.capture()) - val exceptionLog = oppiaExceptionLogsResultCaptor.value.getOrThrow().getExceptionLog(0) - val exception = exceptionLog.toException() + val exceptionsLog = monitorFactory.waitForNextSuccessfulResult(cachedExceptionsProvider) + val exception = exceptionsLog.getExceptionLog(0).toException() val thrownExceptionStackTraceElems = exception.stackTrace.extractRelevantDetails() val expectedExceptionStackTraceElems = exceptionThrown.stackTrace.extractRelevantDetails() assertThat(exception.message).isEqualTo(null) @@ -330,15 +271,10 @@ class ExceptionsControllerTest { val exceptionThrown = Exception("TEST") exceptionsController.logNonFatalException(exceptionThrown, TEST_TIMESTAMP_IN_MILLIS_ONE) - val cachedExceptions = - exceptionsController.getExceptionLogStore().toLiveData() - cachedExceptions.observeForever(mockOppiaExceptionLogsObserver) - testCoroutineDispatchers.advanceUntilIdle() + val cachedExceptionsProvider = exceptionsController.getExceptionLogStore() - verify(mockOppiaExceptionLogsObserver, atLeastOnce()) - .onChanged(oppiaExceptionLogsResultCaptor.capture()) - val exceptionLog = oppiaExceptionLogsResultCaptor.value.getOrThrow().getExceptionLog(0) - val exception = exceptionLog.toException() + val exceptionsLog = monitorFactory.waitForNextSuccessfulResult(cachedExceptionsProvider) + val exception = exceptionsLog.getExceptionLog(0).toException() val thrownExceptionStackTraceElems = exception.stackTrace.extractRelevantDetails() val expectedExceptionStackTraceElems = exceptionThrown.stackTrace.extractRelevantDetails() assertThat(exception.message).isEqualTo("TEST") diff --git a/domain/src/test/java/org/oppia/android/domain/oppialogger/exceptions/UncaughtExceptionLoggerStartupListenerTest.kt b/domain/src/test/java/org/oppia/android/domain/oppialogger/exceptions/UncaughtExceptionLoggerStartupListenerTest.kt index a160aa5562c..3fdc3a009ef 100644 --- a/domain/src/test/java/org/oppia/android/domain/oppialogger/exceptions/UncaughtExceptionLoggerStartupListenerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/oppialogger/exceptions/UncaughtExceptionLoggerStartupListenerTest.kt @@ -2,7 +2,6 @@ package org.oppia.android.domain.oppialogger.exceptions import android.app.Application import android.content.Context -import androidx.lifecycle.Observer import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat @@ -11,28 +10,16 @@ import dagger.Component import dagger.Module import dagger.Provides import org.junit.Before -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentCaptor -import org.mockito.Captor -import org.mockito.Mock -import org.mockito.Mockito.atLeastOnce -import org.mockito.Mockito.verify -import org.mockito.junit.MockitoJUnit -import org.mockito.junit.MockitoRule -import org.oppia.android.app.model.OppiaExceptionLogs import org.oppia.android.domain.oppialogger.ExceptionLogStorageCacheSize import org.oppia.android.testing.FakeExceptionLogger import org.oppia.android.testing.TestLogReportingModule import org.oppia.android.testing.robolectric.RobolectricModule -import org.oppia.android.testing.threading.TestCoroutineDispatchers import org.oppia.android.testing.threading.TestDispatcherModule import org.oppia.android.testing.time.FakeOppiaClockModule import org.oppia.android.util.caching.AssetModule -import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.data.DataProviders -import org.oppia.android.util.data.DataProviders.Companion.toLiveData import org.oppia.android.util.data.DataProvidersInjector import org.oppia.android.util.data.DataProvidersInjectorProvider import org.oppia.android.util.locale.LocaleProdModule @@ -45,39 +32,20 @@ import org.robolectric.annotation.LooperMode import javax.inject.Inject import javax.inject.Qualifier import javax.inject.Singleton +import org.oppia.android.testing.data.DataProviderTestMonitor +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) @Config(application = UncaughtExceptionLoggerStartupListenerTest.TestApplication::class) class UncaughtExceptionLoggerStartupListenerTest { - - @Rule - @JvmField - val mockitoRule: MockitoRule = MockitoJUnit.rule() - - @Inject - lateinit var dataProviders: DataProviders - - @Inject - lateinit var uncaughtExceptionLoggerStartupListener: UncaughtExceptionLoggerStartupListener - - @Inject - lateinit var networkConnectionUtil: NetworkConnectionDebugUtil - - @Inject - lateinit var exceptionsController: ExceptionsController - - @Inject - lateinit var fakeExceptionLogger: FakeExceptionLogger - - @Inject - lateinit var testCoroutineDispatchers: TestCoroutineDispatchers - - @Mock - lateinit var mockOppiaExceptionLogsObserver: Observer> - - @Captor - lateinit var oppiaExceptionLogsResultCaptor: ArgumentCaptor> + @Inject lateinit var dataProviders: DataProviders + @Inject lateinit var uncaughtExceptionStartupListener: UncaughtExceptionLoggerStartupListener + @Inject lateinit var networkConnectionUtil: NetworkConnectionDebugUtil + @Inject lateinit var exceptionsController: ExceptionsController + @Inject lateinit var fakeExceptionLogger: FakeExceptionLogger + @Inject lateinit var monitorFactory: DataProviderTestMonitor.Factory @Before fun setUp() { @@ -88,28 +56,22 @@ class UncaughtExceptionLoggerStartupListenerTest { fun testHandler_throwException_withNoNetwork_verifyLogInCache() { networkConnectionUtil.setCurrentConnectionStatus(NONE) val exceptionThrown = Exception("TEST") - uncaughtExceptionLoggerStartupListener.uncaughtException( + uncaughtExceptionStartupListener.uncaughtException( Thread.currentThread(), exceptionThrown ) val cachedExceptions = exceptionsController.getExceptionLogStore() - cachedExceptions.toLiveData().observeForever(mockOppiaExceptionLogsObserver) - testCoroutineDispatchers.advanceUntilIdle() - - verify(mockOppiaExceptionLogsObserver, atLeastOnce()) - .onChanged(oppiaExceptionLogsResultCaptor.capture()) - - val exceptionLog = oppiaExceptionLogsResultCaptor.value.getOrThrow().getExceptionLog(0) - val exception = exceptionLog.toException() + val exceptionLogs = monitorFactory.waitForNextSuccessfulResult(cachedExceptions) + val exception = exceptionLogs.getExceptionLog(0).toException() assertThat(exception.message).matches("java.lang.Exception: TEST") } @Test fun testHandler_throwException_withNetwork_verifyLogToRemoteService() { val exceptionThrown = Exception("TEST") - uncaughtExceptionLoggerStartupListener.uncaughtException( + uncaughtExceptionStartupListener.uncaughtException( Thread.currentThread(), exceptionThrown ) diff --git a/domain/src/test/java/org/oppia/android/domain/platformparameter/PlatformParameterControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/platformparameter/PlatformParameterControllerTest.kt index b18153a13a2..dd9bbb7fb9e 100644 --- a/domain/src/test/java/org/oppia/android/domain/platformparameter/PlatformParameterControllerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/platformparameter/PlatformParameterControllerTest.kt @@ -2,8 +2,6 @@ package org.oppia.android.domain.platformparameter import android.app.Application import android.content.Context -import androidx.arch.core.executor.testing.InstantTaskExecutorRule -import androidx.lifecycle.Observer import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat @@ -11,24 +9,17 @@ import dagger.BindsInstance import dagger.Component import dagger.Module import dagger.Provides -import org.junit.Rule +import javax.inject.Inject +import javax.inject.Singleton import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentCaptor -import org.mockito.Captor -import org.mockito.Mock -import org.mockito.Mockito.atLeastOnce -import org.mockito.Mockito.verify -import org.mockito.junit.MockitoJUnit -import org.mockito.junit.MockitoRule import org.oppia.android.app.model.PlatformParameter import org.oppia.android.domain.oppialogger.LogStorageModule import org.oppia.android.testing.TestLogReportingModule +import org.oppia.android.testing.data.DataProviderTestMonitor import org.oppia.android.testing.robolectric.RobolectricModule import org.oppia.android.testing.threading.TestCoroutineDispatchers import org.oppia.android.testing.threading.TestDispatcherModule -import org.oppia.android.util.data.AsyncResult -import org.oppia.android.util.data.DataProviders.Companion.toLiveData import org.oppia.android.util.data.DataProvidersInjector import org.oppia.android.util.data.DataProvidersInjectorProvider import org.oppia.android.util.logging.EnableConsoleLog @@ -39,8 +30,6 @@ import org.oppia.android.util.networking.NetworkConnectionUtilDebugModule import org.oppia.android.util.platformparameter.PlatformParameterSingleton import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode -import javax.inject.Inject -import javax.inject.Singleton private const val STRING_PLATFORM_PARAMETER_NAME = "string_platform_parameter_name" private const val STRING_PLATFORM_PARAMETER_VALUE = "string_platform_parameter_value" @@ -52,39 +41,16 @@ private const val BOOLEAN_PLATFORM_PARAMETER_NAME = "boolean_platform_parameter_ private const val BOOLEAN_PLATFORM_PARAMETER_VALUE = true /** Tests for [PlatformParameterController]. */ +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) @Config(application = PlatformParameterControllerTest.TestApplication::class) class PlatformParameterControllerTest { - - @Rule - @JvmField - val mockitoRule: MockitoRule = MockitoJUnit.rule() - - @Rule - @JvmField - val executorRule = InstantTaskExecutorRule() - - @Inject - lateinit var platformParameterController: PlatformParameterController - - @Inject - lateinit var platformParameterSingleton: PlatformParameterSingleton - - @Inject - lateinit var testCoroutineDispatchers: TestCoroutineDispatchers - - @Mock - lateinit var mockUnitObserver: Observer> - - @Captor - lateinit var unitCaptor: ArgumentCaptor> - - @Mock - lateinit var mockObserverForAny: Observer > - - @Captor - lateinit var captorForAny: ArgumentCaptor> + @Inject lateinit var platformParameterController: PlatformParameterController + @Inject lateinit var platformParameterSingleton: PlatformParameterSingleton + @Inject lateinit var testCoroutineDispatchers: TestCoroutineDispatchers + @Inject lateinit var monitorFactory: DataProviderTestMonitor.Factory private val mockPlatformParameterList by lazy { listOf( @@ -100,12 +66,10 @@ class PlatformParameterControllerTest { @Test fun testController_noPreviousDatabase_readPlatformParameters_platformParameterMapIsEmpty() { setUpTestApplicationComponent() - platformParameterController.getParameterDatabase().toLiveData().observeForever(mockUnitObserver) - testCoroutineDispatchers.runCurrent() + val databaseProvider = platformParameterController.getParameterDatabase() // The platformParameterMap must be empty as there was no previously cached data. - verify(mockUnitObserver, atLeastOnce()).onChanged(unitCaptor.capture()) - assertThat(unitCaptor.value.isSuccess()).isTrue() + monitorFactory.waitForNextSuccessfulResult(databaseProvider) assertThat(platformParameterSingleton.getPlatformParameterMap()).isEmpty() } @@ -121,12 +85,10 @@ class PlatformParameterControllerTest { // Create the application after previous arrangement to simulate a re-creation. setUpTestApplicationComponent() - platformParameterController.getParameterDatabase().toLiveData().observeForever(mockUnitObserver) - testCoroutineDispatchers.runCurrent() + val databaseProvider = platformParameterController.getParameterDatabase() // The platformParameterMap must have values as application had cached platform parameter data. - verify(mockUnitObserver, atLeastOnce()).onChanged(unitCaptor.capture()) - assertThat(unitCaptor.value.isSuccess()).isTrue() + monitorFactory.waitForNextSuccessfulResult(databaseProvider) assertThat(platformParameterSingleton.getPlatformParameterMap()).isNotEmpty() verifyEntriesInsidePlatformParameterMap(platformParameterSingleton.getPlatformParameterMap()) } @@ -136,12 +98,10 @@ class PlatformParameterControllerTest { setUpTestApplicationComponent() platformParameterController.updatePlatformParameterDatabase(mockPlatformParameterList) testCoroutineDispatchers.runCurrent() - platformParameterController.getParameterDatabase().toLiveData().observeForever(mockUnitObserver) - testCoroutineDispatchers.runCurrent() + val databaseProvider = platformParameterController.getParameterDatabase() // The platformParameterMap must have values as we updated the database with dummy list. - verify(mockUnitObserver, atLeastOnce()).onChanged(unitCaptor.capture()) - assertThat(unitCaptor.value.isSuccess()).isTrue() + monitorFactory.waitForNextSuccessfulResult(databaseProvider) assertThat(platformParameterSingleton.getPlatformParameterMap()).isNotEmpty() verifyEntriesInsidePlatformParameterMap(platformParameterSingleton.getPlatformParameterMap()) } @@ -160,25 +120,21 @@ class PlatformParameterControllerTest { setUpTestApplicationComponent() platformParameterController.updatePlatformParameterDatabase(listOf()) testCoroutineDispatchers.runCurrent() - platformParameterController.getParameterDatabase().toLiveData().observeForever(mockUnitObserver) - testCoroutineDispatchers.runCurrent() + val databaseProvider = platformParameterController.getParameterDatabase() // The new set of values must be empty as we updated the database with an empty list. - verify(mockUnitObserver, atLeastOnce()).onChanged(unitCaptor.capture()) - assertThat(unitCaptor.value.isSuccess()).isTrue() + monitorFactory.waitForNextSuccessfulResult(databaseProvider) assertThat(platformParameterSingleton.getPlatformParameterMap()).isEmpty() } @Test fun testController_noPreviousDatabase_performUpdateOperation_returnsSuccess() { setUpTestApplicationComponent() - platformParameterController.updatePlatformParameterDatabase(mockPlatformParameterList) - .toLiveData().observeForever(mockObserverForAny) - testCoroutineDispatchers.runCurrent() + val updateProvider = + platformParameterController.updatePlatformParameterDatabase(mockPlatformParameterList) - // After a successful update operation we should receive a async result for success - verify(mockObserverForAny, atLeastOnce()).onChanged(captorForAny.capture()) - assertThat(captorForAny.value.isSuccess()).isTrue() + // After a successful update operation we should receive a success result. + monitorFactory.waitForNextSuccessfulResult(updateProvider) } @Test @@ -193,13 +149,11 @@ class PlatformParameterControllerTest { // Create the application after previous arrangement to simulate a re-creation. setUpTestApplicationComponent() - platformParameterController.updatePlatformParameterDatabase(mockPlatformParameterList) - .toLiveData().observeForever(mockObserverForAny) - testCoroutineDispatchers.runCurrent() + val updateProvider = + platformParameterController.updatePlatformParameterDatabase(mockPlatformParameterList) - // After a successful update operation we should receive a async result for success - verify(mockObserverForAny, atLeastOnce()).onChanged(captorForAny.capture()) - assertThat(captorForAny.value.isSuccess()).isTrue() + // After a successful update operation we should receive a success result. + monitorFactory.waitForNextSuccessfulResult(updateProvider) } /** @@ -274,7 +228,7 @@ class PlatformParameterControllerTest { interface Builder { @BindsInstance fun setApplication(application: Application): Builder - fun build(): PlatformParameterControllerTest.TestApplicationComponent + fun build(): TestApplicationComponent } fun inject(platformParameterControllerTest: PlatformParameterControllerTest) @@ -285,7 +239,7 @@ class PlatformParameterControllerTest { } class TestApplication : Application(), DataProvidersInjectorProvider { - private val component: PlatformParameterControllerTest.TestApplicationComponent by lazy { + private val component: TestApplicationComponent by lazy { DaggerPlatformParameterControllerTest_TestApplicationComponent.builder() .setApplication(this) .build() diff --git a/domain/src/test/java/org/oppia/android/domain/platformparameter/syncup/PlatformParameterSyncUpWorkerTest.kt b/domain/src/test/java/org/oppia/android/domain/platformparameter/syncup/PlatformParameterSyncUpWorkerTest.kt index 70ccd1e8c08..a9b693eafda 100644 --- a/domain/src/test/java/org/oppia/android/domain/platformparameter/syncup/PlatformParameterSyncUpWorkerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/platformparameter/syncup/PlatformParameterSyncUpWorkerTest.kt @@ -2,7 +2,6 @@ package org.oppia.android.domain.platformparameter.syncup import android.app.Application import android.content.Context -import androidx.lifecycle.Observer import androidx.test.core.app.ApplicationProvider import androidx.test.core.content.pm.ApplicationInfoBuilder import androidx.test.core.content.pm.PackageInfoBuilder @@ -21,14 +20,12 @@ import dagger.BindsInstance import dagger.Component import dagger.Module import dagger.Provides +import javax.inject.Inject +import javax.inject.Singleton import okhttp3.OkHttpClient import org.junit.Before -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import org.mockito.Mock -import org.mockito.junit.MockitoJUnit -import org.mockito.junit.MockitoRule import org.oppia.android.app.model.PlatformParameter import org.oppia.android.data.backends.gae.BaseUrl import org.oppia.android.data.backends.gae.JsonPrefixNetworkInterceptor @@ -42,6 +39,7 @@ import org.oppia.android.domain.platformparameter.PlatformParameterController import org.oppia.android.domain.platformparameter.PlatformParameterSingletonImpl import org.oppia.android.testing.FakeExceptionLogger import org.oppia.android.testing.TestLogReportingModule +import org.oppia.android.testing.data.DataProviderTestMonitor import org.oppia.android.testing.network.MockPlatformParameterService import org.oppia.android.testing.network.RetrofitTestModule import org.oppia.android.testing.platformparameter.TEST_BOOLEAN_PARAM_NAME @@ -55,8 +53,6 @@ import org.oppia.android.testing.robolectric.RobolectricModule import org.oppia.android.testing.threading.TestCoroutineDispatchers import org.oppia.android.testing.threading.TestDispatcherModule import org.oppia.android.testing.time.FakeOppiaClockModule -import org.oppia.android.util.data.AsyncResult -import org.oppia.android.util.data.DataProviders.Companion.toLiveData import org.oppia.android.util.data.DataProvidersInjector import org.oppia.android.util.data.DataProvidersInjectorProvider import org.oppia.android.util.locale.LocaleProdModule @@ -73,10 +69,10 @@ import org.robolectric.annotation.LooperMode import retrofit2.Retrofit import retrofit2.converter.moshi.MoshiConverterFactory import retrofit2.mock.MockRetrofit -import javax.inject.Inject -import javax.inject.Singleton /** Tests for [PlatformParameterSyncUpWorker]. */ +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) @Config( @@ -84,31 +80,13 @@ import javax.inject.Singleton manifest = Config.NONE ) class PlatformParameterSyncUpWorkerTest { - - @Rule - @JvmField - val mockitoRule: MockitoRule = MockitoJUnit.rule() - - @Mock - lateinit var mockUnitObserver: Observer> - - @Inject - lateinit var platformParameterSingleton: PlatformParameterSingleton - - @Inject - lateinit var platformParameterController: PlatformParameterController - - @Inject - lateinit var platformParameterSyncUpWorkerFactory: PlatformParameterSyncUpWorkerFactory - - @Inject - lateinit var testCoroutineDispatchers: TestCoroutineDispatchers - - @Inject - lateinit var context: Context - - @Inject - lateinit var fakeExceptionLogger: FakeExceptionLogger + @Inject lateinit var platformParameterSingleton: PlatformParameterSingleton + @Inject lateinit var platformParameterController: PlatformParameterController + @Inject lateinit var platformParameterSyncUpWorkerFactory: PlatformParameterSyncUpWorkerFactory + @Inject lateinit var testCoroutineDispatchers: TestCoroutineDispatchers + @Inject lateinit var context: Context + @Inject lateinit var fakeExceptionLogger: FakeExceptionLogger + @Inject lateinit var monitorFactory: DataProviderTestMonitor.Factory private val expectedTestStringParameter = PlatformParameter.newBuilder() .setName(TEST_STRING_PARAM_NAME) @@ -173,8 +151,7 @@ class PlatformParameterSyncUpWorkerTest { assertThat(workInfo.get().state).isEqualTo(WorkInfo.State.SUCCEEDED) // Retrieve the previously cached Platform Parameters from Cache Store. - platformParameterController.getParameterDatabase().toLiveData().observeForever(mockUnitObserver) - testCoroutineDispatchers.runCurrent() + monitorFactory.ensureDataProviderExecutes(platformParameterController.getParameterDatabase()) // Values retrieved from Cache store will be sent to Platform Parameter Singleton by the // Controller in the form of a Map, therefore verify the retrieved values from that Map. @@ -242,8 +219,7 @@ class PlatformParameterSyncUpWorkerTest { assertThat(workInfo.get().state).isEqualTo(WorkInfo.State.SUCCEEDED) // Retrieve the previously cached Platform Parameters from Cache Store. - platformParameterController.getParameterDatabase().toLiveData().observeForever(mockUnitObserver) - testCoroutineDispatchers.runCurrent() + monitorFactory.ensureDataProviderExecutes(platformParameterController.getParameterDatabase()) // Values retrieved from Cache store will be sent to Platform Parameter Singleton by the // Controller in the form of a Map, therefore verify the retrieved values from that Map. @@ -327,8 +303,7 @@ class PlatformParameterSyncUpWorkerTest { .isEqualTo(PlatformParameterSyncUpWorker.EMPTY_RESPONSE_EXCEPTION_MSG) // Retrieve the previously cached Platform Parameters from Cache Store. - platformParameterController.getParameterDatabase().toLiveData().observeForever(mockUnitObserver) - testCoroutineDispatchers.runCurrent() + monitorFactory.ensureDataProviderExecutes(platformParameterController.getParameterDatabase()) // Values retrieved from Cache store will be sent to Platform Parameter Singleton by the // Controller in the form of a Map, therefore verify the retrieved values from that Map. diff --git a/domain/src/test/java/org/oppia/android/domain/profile/ProfileManagementControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/profile/ProfileManagementControllerTest.kt index 752699cee61..74c38d924b2 100644 --- a/domain/src/test/java/org/oppia/android/domain/profile/ProfileManagementControllerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/profile/ProfileManagementControllerTest.kt @@ -2,7 +2,7 @@ package org.oppia.android.domain.profile import android.app.Application import android.content.Context -import androidx.lifecycle.Observer +import android.net.Uri import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat @@ -11,23 +11,13 @@ import dagger.Component import dagger.Module import dagger.Provides import org.junit.Before -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentCaptor -import org.mockito.Captor -import org.mockito.Mock -import org.mockito.Mockito.atLeastOnce -import org.mockito.Mockito.verify -import org.mockito.junit.MockitoJUnit -import org.mockito.junit.MockitoRule import org.oppia.android.app.model.AppLanguage import org.oppia.android.app.model.AudioLanguage -import org.oppia.android.app.model.DeviceSettings import org.oppia.android.app.model.Profile import org.oppia.android.app.model.ProfileDatabase import org.oppia.android.app.model.ProfileId -import org.oppia.android.app.model.ReadingTextSize import org.oppia.android.domain.oppialogger.LogStorageModule import org.oppia.android.testing.TestLogReportingModule import org.oppia.android.testing.profile.ProfileTestHelper @@ -35,8 +25,6 @@ import org.oppia.android.testing.robolectric.RobolectricModule import org.oppia.android.testing.threading.TestCoroutineDispatchers import org.oppia.android.testing.threading.TestDispatcherModule import org.oppia.android.testing.time.FakeOppiaClockModule -import org.oppia.android.util.data.AsyncResult -import org.oppia.android.util.data.DataProviders.Companion.toLiveData import org.oppia.android.util.data.DataProvidersInjector import org.oppia.android.util.data.DataProvidersInjectorProvider import org.oppia.android.util.locale.LocaleProdModule @@ -51,65 +39,45 @@ import java.io.File import java.io.FileInputStream import javax.inject.Inject import javax.inject.Singleton +import org.oppia.android.app.model.AppLanguage.CHINESE_APP_LANGUAGE +import org.oppia.android.app.model.AudioLanguage.FRENCH_AUDIO_LANGUAGE +import org.oppia.android.app.model.ReadingTextSize.MEDIUM_TEXT_SIZE +import org.oppia.android.testing.data.DataProviderTestMonitor +import org.oppia.android.util.data.DataProvider /** Tests for [ProfileManagementControllerTest]. */ +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) @Config(application = ProfileManagementControllerTest.TestApplication::class) class ProfileManagementControllerTest { - @Rule - @JvmField - val mockitoRule: MockitoRule = MockitoJUnit.rule() - - @Inject - lateinit var context: Context - - @Inject - lateinit var profileTestHelper: ProfileTestHelper - - @Inject - lateinit var profileManagementController: ProfileManagementController - - @Inject - lateinit var testCoroutineDispatchers: TestCoroutineDispatchers - - @Mock - lateinit var mockProfilesObserver: Observer>> - - @Captor - lateinit var profilesResultCaptor: ArgumentCaptor>> - - @Mock - lateinit var mockProfileObserver: Observer> - - @Captor - lateinit var profileResultCaptor: ArgumentCaptor> - - @Mock - lateinit var mockUpdateResultObserver: Observer> - - @Captor - lateinit var updateResultCaptor: ArgumentCaptor> - - @Mock - lateinit var mockWasProfileAddedResultObserver: Observer> - - @Captor - lateinit var wasProfileAddedResultCaptor: ArgumentCaptor> - - @Mock - lateinit var mockDeviceSettingsObserver: Observer> - - @Captor - lateinit var deviceSettingsResultCaptor: ArgumentCaptor> - - private val PROFILES_LIST = listOf( - Profile.newBuilder().setName("James").setPin("123").setAllowDownloadAccess(true).build(), - Profile.newBuilder().setName("Sean").setPin("234").setAllowDownloadAccess(false).build(), - Profile.newBuilder().setName("Ben").setPin("345").setAllowDownloadAccess(true).build(), - Profile.newBuilder().setName("Rajat").setPin("456").setAllowDownloadAccess(false).build(), - Profile.newBuilder().setName("Veena").setPin("567").setAllowDownloadAccess(true).build() - ) + @Inject lateinit var context: Context + @Inject lateinit var profileTestHelper: ProfileTestHelper + @Inject lateinit var profileManagementController: ProfileManagementController + @Inject lateinit var testCoroutineDispatchers: TestCoroutineDispatchers + @Inject lateinit var monitorFactory: DataProviderTestMonitor.Factory + + private companion object { + private val PROFILES_LIST = listOf( + Profile.newBuilder().setName("James").setPin("123").setAllowDownloadAccess(true).build(), + Profile.newBuilder().setName("Sean").setPin("234").setAllowDownloadAccess(false).build(), + Profile.newBuilder().setName("Ben").setPin("345").setAllowDownloadAccess(true).build(), + Profile.newBuilder().setName("Rajat").setPin("456").setAllowDownloadAccess(false).build(), + Profile.newBuilder().setName("Veena").setPin("567").setAllowDownloadAccess(true).build() + ) + + private val ADMIN_PROFILE_ID_0 = ProfileId.newBuilder().setInternalId(0).build() + private val PROFILE_ID_1 = ProfileId.newBuilder().setInternalId(1).build() + private val PROFILE_ID_2 = ProfileId.newBuilder().setInternalId(2).build() + private val PROFILE_ID_3 = ProfileId.newBuilder().setInternalId(3).build() + private val PROFILE_ID_4 = ProfileId.newBuilder().setInternalId(4).build() + private val PROFILE_ID_6 = ProfileId.newBuilder().setInternalId(6).build() + + private const val DEFAULT_PIN = "12345" + private const val DEFAULT_ALLOW_DOWNLOAD_ACCESS = true + private const val DEFAULT_AVATAR_COLOR_RGB = -10710042 + } @Before fun setUp() { @@ -122,25 +90,17 @@ class ProfileManagementControllerTest { @Test fun testAddProfile_addProfile_checkProfileIsAdded() { - profileManagementController.addProfile( - name = "James", - pin = "123", - avatarImagePath = null, - allowDownloadAccess = true, - colorRgb = -10710042, - isAdmin = true - ).toLiveData().observeForever(mockUpdateResultObserver) - testCoroutineDispatchers.runCurrent() + val dataProvider = addAdminProfile(name = "James", pin = "123") - val profileDatabase = readProfileDatabase() - verifyUpdateSucceeded() + monitorFactory.waitForNextSuccessfulResult(dataProvider) + val profileDatabase = readProfileDatabase() val profile = profileDatabase.profilesMap[0]!! assertThat(profile.name).isEqualTo("James") assertThat(profile.pin).isEqualTo("123") assertThat(profile.allowDownloadAccess).isEqualTo(true) assertThat(profile.id.internalId).isEqualTo(0) - assertThat(profile.readingTextSize).isEqualTo(ReadingTextSize.MEDIUM_TEXT_SIZE) + assertThat(profile.readingTextSize).isEqualTo(MEDIUM_TEXT_SIZE) assertThat(profile.appLanguage).isEqualTo(AppLanguage.ENGLISH_APP_LANGUAGE) assertThat(profile.audioLanguage).isEqualTo(AudioLanguage.ENGLISH_AUDIO_LANGUAGE) assertThat(File(getAbsoluteDirPath("0")).isDirectory).isTrue() @@ -149,60 +109,35 @@ class ProfileManagementControllerTest { @Test fun testAddProfile_addProfileWithNotUniqueName_checkResultIsFailure() { addTestProfiles() - testCoroutineDispatchers.runCurrent() - profileManagementController.addProfile( - name = "JAMES", - pin = "321", - avatarImagePath = null, - allowDownloadAccess = false, - colorRgb = -10710042, - isAdmin = true - ).toLiveData().observeForever(mockUpdateResultObserver) - testCoroutineDispatchers.runCurrent() + val dataProvider = addAdminProfile(name = "JAMES", pin = "321") - verifyUpdateFailed() - assertThat(updateResultCaptor.value.getErrorOrNull()).hasMessageThat() - .contains("JAMES is not unique to other profiles") + val failure = monitorFactory.waitForNextFailureResult(dataProvider) + assertThat(failure).hasMessageThat().contains("JAMES is not unique to other profiles") } @Test fun testAddProfile_addProfileWithNumberInName_checkResultIsFailure() { addTestProfiles() - testCoroutineDispatchers.runCurrent() - profileManagementController.addProfile( - name = "James034", - pin = "321", - avatarImagePath = null, - allowDownloadAccess = false, - colorRgb = -10710042, - isAdmin = true - ).toLiveData().observeForever(mockUpdateResultObserver) - testCoroutineDispatchers.runCurrent() + val dataProvider = addAdminProfile(name = "James034", pin = "321") - verifyUpdateFailed() - assertThat(updateResultCaptor.value.getErrorOrNull()).hasMessageThat() - .contains("James034 does not contain only letters") + val failure = monitorFactory.waitForNextFailureResult(dataProvider) + assertThat(failure).hasMessageThat().contains("James034 does not contain only letters") } @Test fun testGetProfile_addManyProfiles_checkGetProfileIsCorrect() { addTestProfiles() - testCoroutineDispatchers.runCurrent() - profileManagementController.getProfile(ProfileId.newBuilder().setInternalId(3).build()) - .toLiveData() - .observeForever(mockProfileObserver) - testCoroutineDispatchers.runCurrent() + val dataProvider = profileManagementController.getProfile(PROFILE_ID_3) - verifyGetProfileSucceeded() - val profile = profileResultCaptor.value.getOrThrow() + val profile = monitorFactory.waitForNextSuccessfulResult(dataProvider) assertThat(profile.name).isEqualTo("Rajat") assertThat(profile.pin).isEqualTo("456") assertThat(profile.allowDownloadAccess).isEqualTo(false) assertThat(profile.id.internalId).isEqualTo(3) - assertThat(profile.readingTextSize).isEqualTo(ReadingTextSize.MEDIUM_TEXT_SIZE) + assertThat(profile.readingTextSize).isEqualTo(MEDIUM_TEXT_SIZE) assertThat(profile.appLanguage).isEqualTo(AppLanguage.ENGLISH_APP_LANGUAGE) assertThat(profile.audioLanguage).isEqualTo(AudioLanguage.ENGLISH_AUDIO_LANGUAGE) } @@ -210,13 +145,10 @@ class ProfileManagementControllerTest { @Test fun testGetProfiles_addManyProfiles_checkAllProfilesAreAdded() { addTestProfiles() - testCoroutineDispatchers.runCurrent() - profileManagementController.getProfiles().toLiveData().observeForever(mockProfilesObserver) - testCoroutineDispatchers.runCurrent() + val dataProvider = profileManagementController.getProfiles() - verifyGetMultipleProfilesSucceeded() - val profiles = profilesResultCaptor.value.getOrThrow().sortedBy { + val profiles = monitorFactory.waitForNextSuccessfulResult(dataProvider).sortedBy { it.id.internalId } assertThat(profiles.size).isEqualTo(PROFILES_LIST.size) @@ -226,22 +158,12 @@ class ProfileManagementControllerTest { @Test fun testGetProfiles_addManyProfiles_restartApplication_addProfile_checkAllProfilesAreAdded() { addTestProfiles() - testCoroutineDispatchers.runCurrent() setUpTestApplicationComponent() - profileManagementController.addProfile( - name = "Nikita", - pin = "678", - avatarImagePath = null, - allowDownloadAccess = false, - colorRgb = -10710042, - isAdmin = false - ).toLiveData() - profileManagementController.getProfiles().toLiveData().observeForever(mockProfilesObserver) - testCoroutineDispatchers.runCurrent() + addNonAdminProfileAndWait(name = "Nikita", pin = "678", allowDownloadAccess = false) + val dataProvider = profileManagementController.getProfiles() - verifyGetMultipleProfilesSucceeded() - val profiles = profilesResultCaptor.value.getOrThrow().sortedBy { + val profiles = monitorFactory.waitForNextSuccessfulResult(dataProvider).sortedBy { it.id.internalId } assertThat(profiles.size).isEqualTo(PROFILES_LIST.size + 1) @@ -251,244 +173,159 @@ class ProfileManagementControllerTest { @Test fun testUpdateName_addProfiles_updateWithUniqueName_checkUpdateIsSuccessful() { addTestProfiles() - testCoroutineDispatchers.runCurrent() - val profileId = ProfileId.newBuilder().setInternalId(2).build() - profileManagementController.updateName(profileId, "John").toLiveData() - .observeForever(mockUpdateResultObserver) - profileManagementController.getProfile( - profileId - ).toLiveData().observeForever(mockProfileObserver) - testCoroutineDispatchers.runCurrent() + val updateProvider = profileManagementController.updateName(PROFILE_ID_2, "John") + val profileProvider = profileManagementController.getProfile(PROFILE_ID_2) - verifyUpdateSucceeded() - verifyGetProfileSucceeded() - assertThat(profileResultCaptor.value.getOrThrow().name).isEqualTo("John") + monitorFactory.waitForNextSuccessfulResult(updateProvider) + val profile = monitorFactory.waitForNextSuccessfulResult(profileProvider) + assertThat(profile.name).isEqualTo("John") } @Test fun testUpdateName_addProfiles_updateWithNotUniqueName_checkUpdatedFailed() { addTestProfiles() - testCoroutineDispatchers.runCurrent() - val profileId = ProfileId.newBuilder().setInternalId(2).build() - profileManagementController.updateName(profileId, "James").toLiveData() - .observeForever(mockUpdateResultObserver) - testCoroutineDispatchers.runCurrent() + val updateProvider = profileManagementController.updateName(PROFILE_ID_2, "James") - verifyUpdateFailed() - assertThat(updateResultCaptor.value.getErrorOrNull()).hasMessageThat() - .contains("James is not unique to other profiles") + val error = monitorFactory.waitForNextFailureResult(updateProvider) + assertThat(error).hasMessageThat().contains("James is not unique to other profiles") } @Test fun testUpdateName_addProfiles_updateWithBadProfileId_checkUpdatedFailed() { addTestProfiles() - testCoroutineDispatchers.runCurrent() - val profileId = ProfileId.newBuilder().setInternalId(6).build() - profileManagementController.updateName(profileId, "John").toLiveData() - .observeForever(mockUpdateResultObserver) - testCoroutineDispatchers.runCurrent() + val updateProvider = profileManagementController.updateName(PROFILE_ID_6, "John") - verifyUpdateFailed() - assertThat(updateResultCaptor.value.getErrorOrNull()).hasMessageThat() - .contains("ProfileId 6 does not match an existing Profile") + val error = monitorFactory.waitForNextFailureResult(updateProvider) + assertThat(error).hasMessageThat().contains("ProfileId 6 does not match an existing Profile") } @Test fun testUpdateName_addProfiles_updateProfileAvatar_checkUpdateIsSuccessful() { addTestProfiles() - testCoroutineDispatchers.runCurrent() - val profileId = ProfileId.newBuilder().setInternalId(2).build() - profileManagementController + val updateProvider = profileManagementController .updateProfileAvatar( - profileId, + PROFILE_ID_2, /* avatarImagePath = */ null, - colorRgb = -10710042 - ).toLiveData() - .observeForever(mockUpdateResultObserver) - profileManagementController.getProfile( - profileId - ).toLiveData().observeForever(mockProfileObserver) - testCoroutineDispatchers.runCurrent() + colorRgb = DEFAULT_AVATAR_COLOR_RGB + ) + val profileProvider = profileManagementController.getProfile(PROFILE_ID_2) - verifyUpdateSucceeded() - verifyGetProfileSucceeded() - assertThat(profileResultCaptor.value.getOrThrow().avatar.avatarColorRgb) - .isEqualTo(-10710042) + monitorFactory.waitForNextSuccessfulResult(updateProvider) + val profile = monitorFactory.waitForNextSuccessfulResult(profileProvider) + assertThat(profile.avatar.avatarColorRgb).isEqualTo(DEFAULT_AVATAR_COLOR_RGB) } @Test fun testUpdatePin_addProfiles_updatePin_checkUpdateIsSuccessful() { addTestProfiles() - testCoroutineDispatchers.runCurrent() - val profileId = ProfileId.newBuilder().setInternalId(2).build() - profileManagementController.updatePin(profileId, "321").toLiveData() - .observeForever(mockUpdateResultObserver) - profileManagementController.getProfile( - profileId - ).toLiveData().observeForever(mockProfileObserver) - testCoroutineDispatchers.runCurrent() + val updateProvider = profileManagementController.updatePin(PROFILE_ID_2, "321") + val profileProvider = profileManagementController.getProfile(PROFILE_ID_2) - verifyUpdateSucceeded() - verifyGetProfileSucceeded() - assertThat(profileResultCaptor.value.getOrThrow().pin).isEqualTo("321") + monitorFactory.waitForNextSuccessfulResult(updateProvider) + val profile = monitorFactory.waitForNextSuccessfulResult(profileProvider) + assertThat(profile.pin).isEqualTo("321") } @Test fun testUpdatePin_addProfiles_updateWithBadProfileId_checkUpdateFailed() { addTestProfiles() - testCoroutineDispatchers.runCurrent() - val profileId = ProfileId.newBuilder().setInternalId(6).build() - profileManagementController.updatePin(profileId, "321").toLiveData() - .observeForever(mockUpdateResultObserver) + val updateProvider = profileManagementController.updatePin(PROFILE_ID_6, "321") testCoroutineDispatchers.runCurrent() - verifyUpdateFailed() - assertThat(updateResultCaptor.value.getErrorOrNull()).hasMessageThat() - .contains("ProfileId 6 does not match an existing Profile") + val error = monitorFactory.waitForNextFailureResult(updateProvider) + assertThat(error).hasMessageThat().contains("ProfileId 6 does not match an existing Profile") } @Test fun testUpdateAllowDownloadAccess_addProfiles_updateDownloadAccess_checkUpdateIsSuccessful() { addTestProfiles() - testCoroutineDispatchers.runCurrent() - val profileId = ProfileId.newBuilder().setInternalId(2).build() - profileManagementController.updateAllowDownloadAccess(profileId, false).toLiveData() - .observeForever(mockUpdateResultObserver) - profileManagementController.getProfile( - profileId - ).toLiveData().observeForever(mockProfileObserver) - testCoroutineDispatchers.runCurrent() + val updateProvider = profileManagementController.updateAllowDownloadAccess(PROFILE_ID_2, false) + val profileProvider = profileManagementController.getProfile(PROFILE_ID_2) - verifyUpdateSucceeded() - verifyGetProfileSucceeded() - assertThat(profileResultCaptor.value.getOrThrow().allowDownloadAccess) - .isEqualTo(false) + monitorFactory.waitForNextSuccessfulResult(updateProvider) + val profile = monitorFactory.waitForNextSuccessfulResult(profileProvider) + assertThat(profile.allowDownloadAccess).isEqualTo(false) } @Test fun testUpdateAllowDownloadAccess_addProfiles_updateWithBadProfileId_checkUpdatedFailed() { addTestProfiles() - testCoroutineDispatchers.runCurrent() - val profileId = ProfileId.newBuilder().setInternalId(6).build() - profileManagementController.updateAllowDownloadAccess(profileId, false).toLiveData() - .observeForever(mockUpdateResultObserver) + val updateProvider = profileManagementController.updateAllowDownloadAccess(PROFILE_ID_6, false) testCoroutineDispatchers.runCurrent() - verifyUpdateFailed() - assertThat(updateResultCaptor.value.getErrorOrNull()).hasMessageThat() - .contains("ProfileId 6 does not match an existing Profile") + val error = monitorFactory.waitForNextFailureResult(updateProvider) + assertThat(error).hasMessageThat().contains("ProfileId 6 does not match an existing Profile") } @Test fun testUpdateReadingTextSize_addProfiles_updateWithFontSize18_checkUpdateIsSuccessful() { addTestProfiles() - testCoroutineDispatchers.runCurrent() - val profileId = ProfileId.newBuilder().setInternalId(2).build() - profileManagementController.updateReadingTextSize(profileId, ReadingTextSize.MEDIUM_TEXT_SIZE) - .toLiveData() - .observeForever(mockUpdateResultObserver) - profileManagementController.getProfile( - profileId - ).toLiveData().observeForever(mockProfileObserver) - testCoroutineDispatchers.runCurrent() + val updateProvider = + profileManagementController.updateReadingTextSize(PROFILE_ID_2, MEDIUM_TEXT_SIZE) - verifyUpdateSucceeded() - verifyGetProfileSucceeded() - assertThat(profileResultCaptor.value.getOrThrow().readingTextSize) - .isEqualTo(ReadingTextSize.MEDIUM_TEXT_SIZE) + val profileProvider = profileManagementController.getProfile(PROFILE_ID_2) + monitorFactory.waitForNextSuccessfulResult(updateProvider) + val profile = monitorFactory.waitForNextSuccessfulResult(profileProvider) + assertThat(profile.readingTextSize).isEqualTo(MEDIUM_TEXT_SIZE) } @Test fun testUpdateAppLanguage_addProfiles_updateWithChineseLanguage_checkUpdateIsSuccessful() { addTestProfiles() - testCoroutineDispatchers.runCurrent() - val profileId = ProfileId.newBuilder().setInternalId(2).build() - profileManagementController.updateAppLanguage(profileId, AppLanguage.CHINESE_APP_LANGUAGE) - .toLiveData() - .observeForever(mockUpdateResultObserver) - profileManagementController.getProfile( - profileId - ).toLiveData().observeForever(mockProfileObserver) - testCoroutineDispatchers.runCurrent() + val updateProvider = + profileManagementController.updateAppLanguage(PROFILE_ID_2, CHINESE_APP_LANGUAGE) - verifyUpdateSucceeded() - verifyGetProfileSucceeded() - assertThat(profileResultCaptor.value.getOrThrow().appLanguage) - .isEqualTo(AppLanguage.CHINESE_APP_LANGUAGE) + val profileProvider = profileManagementController.getProfile(PROFILE_ID_2) + monitorFactory.waitForNextSuccessfulResult(updateProvider) + val profile = monitorFactory.waitForNextSuccessfulResult(profileProvider) + assertThat(profile.appLanguage).isEqualTo(CHINESE_APP_LANGUAGE) } @Test fun testUpdateAudioLanguage_addProfiles_updateWithFrenchLanguage_checkUpdateIsSuccessful() { addTestProfiles() - testCoroutineDispatchers.runCurrent() - val profileId = ProfileId.newBuilder().setInternalId(2).build() - profileManagementController - .updateAudioLanguage(profileId, AudioLanguage.FRENCH_AUDIO_LANGUAGE).toLiveData() - .observeForever(mockUpdateResultObserver) - profileManagementController.getProfile( - profileId - ).toLiveData().observeForever(mockProfileObserver) - testCoroutineDispatchers.runCurrent() + val updateProvider = + profileManagementController.updateAudioLanguage(PROFILE_ID_2, FRENCH_AUDIO_LANGUAGE) + val profileProvider = profileManagementController.getProfile(PROFILE_ID_2) - verifyUpdateSucceeded() - verifyGetProfileSucceeded() - assertThat(profileResultCaptor.value.getOrThrow().audioLanguage) - .isEqualTo(AudioLanguage.FRENCH_AUDIO_LANGUAGE) + monitorFactory.waitForNextSuccessfulResult(updateProvider) + val profile = monitorFactory.waitForNextSuccessfulResult(profileProvider) + assertThat(profile.audioLanguage).isEqualTo(FRENCH_AUDIO_LANGUAGE) } @Test fun testDeleteProfile_addProfiles_deleteProfile_checkDeletionIsSuccessful() { addTestProfiles() - testCoroutineDispatchers.runCurrent() - val profileId = ProfileId.newBuilder().setInternalId(2).build() - profileManagementController.deleteProfile( - profileId - ).toLiveData().observeForever(mockUpdateResultObserver) - profileManagementController.getProfile( - profileId - ).toLiveData().observeForever(mockProfileObserver) - testCoroutineDispatchers.runCurrent() + val deleteProvider = profileManagementController.deleteProfile(PROFILE_ID_2) + val profileProvider = profileManagementController.getProfile(PROFILE_ID_2) - verifyUpdateSucceeded() - verify(mockProfileObserver, atLeastOnce()).onChanged(profileResultCaptor.capture()) - assertThat(profileResultCaptor.value.isFailure()).isTrue() + monitorFactory.waitForNextSuccessfulResult(deleteProvider) + monitorFactory.waitForNextFailureResult(profileProvider) assertThat(File(getAbsoluteDirPath("2")).isDirectory).isFalse() } @Test fun testDeleteProfile_addProfiles_deleteProfiles_addProfile_checkIdIsNotReused() { addTestProfiles() - testCoroutineDispatchers.runCurrent() - val profileId3 = ProfileId.newBuilder().setInternalId(3).build() - val profileId4 = ProfileId.newBuilder().setInternalId(4).build() - profileManagementController.deleteProfile(profileId3).toLiveData() - profileManagementController.deleteProfile(profileId4).toLiveData() - profileManagementController.addProfile( - name = "John", - pin = "321", - avatarImagePath = null, - allowDownloadAccess = false, - colorRgb = -10710042, - isAdmin = true - ).toLiveData() - profileManagementController.getProfiles().toLiveData().observeForever(mockProfilesObserver) - testCoroutineDispatchers.runCurrent() + profileManagementController.deleteProfile(PROFILE_ID_3) + profileManagementController.deleteProfile(PROFILE_ID_4) + addAdminProfileAndWait(name = "John", pin = "321") - verifyGetMultipleProfilesSucceeded() - val profiles = profilesResultCaptor.value.getOrThrow().sortedBy { + val profilesProvider = profileManagementController.getProfiles() + val profiles = monitorFactory.waitForNextSuccessfulResult(profilesProvider).sortedBy { it.id.internalId } assertThat(profiles.size).isEqualTo(4) @@ -502,21 +339,15 @@ class ProfileManagementControllerTest { @Test fun testDeleteProfile_addProfiles_deleteProfiles_restartApplication_checkDeletionIsSuccessful() { addTestProfiles() - testCoroutineDispatchers.runCurrent() - val profileId1 = ProfileId.newBuilder().setInternalId(1).build() - val profileId2 = ProfileId.newBuilder().setInternalId(2).build() - val profileId3 = ProfileId.newBuilder().setInternalId(3).build() - profileManagementController.deleteProfile(profileId1).toLiveData() - profileManagementController.deleteProfile(profileId2).toLiveData() - profileManagementController.deleteProfile(profileId3).toLiveData() + profileManagementController.deleteProfile(PROFILE_ID_1) + profileManagementController.deleteProfile(PROFILE_ID_2) + profileManagementController.deleteProfile(PROFILE_ID_3) testCoroutineDispatchers.runCurrent() setUpTestApplicationComponent() - profileManagementController.getProfiles().toLiveData().observeForever(mockProfilesObserver) - testCoroutineDispatchers.runCurrent() - verifyGetMultipleProfilesSucceeded() - val profiles = profilesResultCaptor.value.getOrThrow() + val profilesProvider = profileManagementController.getProfiles() + val profiles = monitorFactory.waitForNextSuccessfulResult(profilesProvider) assertThat(profiles.size).isEqualTo(2) assertThat(profiles.first().name).isEqualTo("James") assertThat(profiles.last().name).isEqualTo("Veena") @@ -528,38 +359,25 @@ class ProfileManagementControllerTest { @Test fun testLoginToProfile_addProfiles_loginToProfile_checkGetProfileIdAndLoginTimestampIsCorrect() { addTestProfiles() - testCoroutineDispatchers.runCurrent() - val profileId = ProfileId.newBuilder().setInternalId(2).build() - profileManagementController.loginToProfile( - profileId - ).toLiveData().observeForever(mockUpdateResultObserver) - profileManagementController.getProfile( - profileId - ).toLiveData().observeForever(mockProfileObserver) - testCoroutineDispatchers.runCurrent() + val loginProvider = profileManagementController.loginToProfile(PROFILE_ID_2) - verifyUpdateSucceeded() - verifyGetProfileSucceeded() - assertThat(profileManagementController.getCurrentProfileId().internalId) - .isEqualTo(2) - assertThat(profileResultCaptor.value.getOrThrow().lastLoggedInTimestampMs) - .isNotEqualTo(0) + val profileProvider = profileManagementController.getProfile(PROFILE_ID_2) + monitorFactory.waitForNextSuccessfulResult(loginProvider) + val profile = monitorFactory.waitForNextSuccessfulResult(profileProvider) + assertThat(profileManagementController.getCurrentProfileId().internalId).isEqualTo(2) + assertThat(profile.lastLoggedInTimestampMs).isNotEqualTo(0) } @Test fun testLoginToProfile_addProfiles_loginToProfileWithBadProfileId_checkLoginFailed() { addTestProfiles() - testCoroutineDispatchers.runCurrent() - val profileId = ProfileId.newBuilder().setInternalId(6).build() - profileManagementController.loginToProfile( - profileId - ).toLiveData().observeForever(mockUpdateResultObserver) - testCoroutineDispatchers.runCurrent() + val loginProvider = profileManagementController.loginToProfile(PROFILE_ID_6) - verifyUpdateFailed() - assertThat(updateResultCaptor.value.getErrorOrNull()).hasMessageThat() + val error = monitorFactory.waitForNextFailureResult(loginProvider) + assertThat(error) + .hasMessageThat() .contains( "org.oppia.android.domain.profile.ProfileManagementController\$ProfileNotFoundException: " + "ProfileId 6 is not associated with an existing profile" @@ -568,439 +386,178 @@ class ProfileManagementControllerTest { @Test fun testWasProfileEverAdded_addAdminProfile_checkIfProfileEverAdded() { - profileManagementController.addProfile( - name = "James", - pin = "123", - avatarImagePath = null, - allowDownloadAccess = true, - colorRgb = -10710042, - isAdmin = true - ).toLiveData().observeForever(mockUpdateResultObserver) - testCoroutineDispatchers.runCurrent() + val addProvider = addAdminProfile(name = "James", pin = "123") - val profileDatabase = readProfileDatabase() + monitorFactory.waitForNextSuccessfulResult(addProvider) - verifyUpdateSucceeded() + val profileDatabase = readProfileDatabase() assertThat(profileDatabase.wasProfileEverAdded).isEqualTo(false) } @Test fun testWasProfileEverAdded_addAdminProfile_getWasProfileEverAdded() { - profileManagementController.addProfile( - name = "James", - pin = "12345", - avatarImagePath = null, - allowDownloadAccess = true, - colorRgb = -10710042, - isAdmin = true - ).toLiveData().observeForever(mockUpdateResultObserver) - testCoroutineDispatchers.runCurrent() + addAdminProfileAndWait(name = "James") - profileManagementController.getWasProfileEverAdded().toLiveData() - .observeForever(mockWasProfileAddedResultObserver) - testCoroutineDispatchers.runCurrent() + val wasProfileAddedProvider = profileManagementController.getWasProfileEverAdded() - verifyWasProfileEverAddedSucceeded() - val wasProfileEverAdded = wasProfileAddedResultCaptor.value.getOrThrow() + val wasProfileEverAdded = monitorFactory.waitForNextSuccessfulResult(wasProfileAddedProvider) assertThat(wasProfileEverAdded).isFalse() } @Test fun testWasProfileEverAdded_addAdminProfile_addUserProfile_checkIfProfileEverAdded() { - profileManagementController.addProfile( - name = "James", - pin = "12345", - avatarImagePath = null, - allowDownloadAccess = true, - colorRgb = -10710042, - isAdmin = true - ).toLiveData().observeForever(mockUpdateResultObserver) - testCoroutineDispatchers.runCurrent() - - profileManagementController.addProfile( - name = "Rajat", - pin = "01234", - avatarImagePath = null, - allowDownloadAccess = true, - colorRgb = -10710042, - isAdmin = false - ).toLiveData().observeForever(mockUpdateResultObserver) - testCoroutineDispatchers.runCurrent() + addAdminProfileAndWait(name = "James") + addNonAdminProfileAndWait(name = "Rajat", pin = "01234") val profileDatabase = readProfileDatabase() - verifyUpdateSucceeded() assertThat(profileDatabase.wasProfileEverAdded).isEqualTo(true) assertThat(profileDatabase.profilesMap.size).isEqualTo(2) } @Test fun testWasProfileEverAdded_addAdminProfile_addUserProfile_getWasProfileEverAdded() { - profileManagementController.addProfile( - name = "James", - pin = "12345", - avatarImagePath = null, - allowDownloadAccess = true, - colorRgb = -10710042, - isAdmin = true - ).toLiveData().observeForever(mockUpdateResultObserver) - testCoroutineDispatchers.runCurrent() - - profileManagementController.addProfile( - name = "Rajat", - pin = "01234", - avatarImagePath = null, - allowDownloadAccess = true, - colorRgb = -10710042, - isAdmin = false - ).toLiveData().observeForever(mockUpdateResultObserver) - testCoroutineDispatchers.runCurrent() + addAdminProfileAndWait(name = "James") + addNonAdminProfileAndWait(name = "Rajat", pin = "01234") - profileManagementController.getWasProfileEverAdded().toLiveData() - .observeForever(mockWasProfileAddedResultObserver) + val wasProfileAddedProvider = profileManagementController.getWasProfileEverAdded() testCoroutineDispatchers.runCurrent() - verifyWasProfileEverAddedSucceeded() - - val wasProfileEverAdded = wasProfileAddedResultCaptor.value.getOrThrow() + val wasProfileEverAdded = monitorFactory.waitForNextSuccessfulResult(wasProfileAddedProvider) assertThat(wasProfileEverAdded).isTrue() } @Test fun testWasProfileEverAdded_addAdminProfile_addUserProfile_deleteUserProfile_profileIsAdded() { - profileManagementController.addProfile( - name = "James", - pin = "12345", - avatarImagePath = null, - allowDownloadAccess = true, - colorRgb = -10710042, - isAdmin = true - ).toLiveData().observeForever(mockUpdateResultObserver) - testCoroutineDispatchers.runCurrent() - - profileManagementController.addProfile( - name = "Rajat", - pin = "01234", - avatarImagePath = null, - allowDownloadAccess = true, - colorRgb = -10710042, - isAdmin = false - ).toLiveData().observeForever(mockUpdateResultObserver) - testCoroutineDispatchers.runCurrent() + addAdminProfileAndWait(name = "James") + addNonAdminProfileAndWait(name = "Rajat", pin = "01234") - val profileId1 = ProfileId.newBuilder().setInternalId(1).build() - profileManagementController.deleteProfile(profileId1).toLiveData() + profileManagementController.deleteProfile(PROFILE_ID_1) testCoroutineDispatchers.runCurrent() val profileDatabase = readProfileDatabase() - - verifyUpdateSucceeded() assertThat(profileDatabase.profilesMap.size).isEqualTo(1) } @Test fun testWasProfileEverAdded_addAdminProfile_addUserProfile_deleteUserProfile_profileWasAdded() { - profileManagementController.addProfile( - name = "James", - pin = "12345", - avatarImagePath = null, - allowDownloadAccess = true, - colorRgb = -10710042, - isAdmin = true - ).toLiveData().observeForever(mockUpdateResultObserver) - testCoroutineDispatchers.runCurrent() - - profileManagementController.addProfile( - name = "Rajat", - pin = "01234", - avatarImagePath = null, - allowDownloadAccess = true, - colorRgb = -10710042, - isAdmin = false - ).toLiveData().observeForever(mockUpdateResultObserver) - testCoroutineDispatchers.runCurrent() - - val profileId1 = ProfileId.newBuilder().setInternalId(1).build() - profileManagementController.deleteProfile(profileId1).toLiveData() + addAdminProfileAndWait(name = "James") + addNonAdminProfileAndWait(name = "Rajat", pin = "01234") + profileManagementController.deleteProfile(PROFILE_ID_1) testCoroutineDispatchers.runCurrent() - profileManagementController.getWasProfileEverAdded().toLiveData() - .observeForever(mockWasProfileAddedResultObserver) + val wasProfileAddedProvider = profileManagementController.getWasProfileEverAdded() testCoroutineDispatchers.runCurrent() - verifyWasProfileEverAddedSucceeded() - val wasProfileEverAdded = wasProfileAddedResultCaptor.value.getOrThrow() + val wasProfileEverAdded = monitorFactory.waitForNextSuccessfulResult(wasProfileAddedProvider) assertThat(wasProfileEverAdded).isTrue() } @Test fun testAddAdminProfile_addAnotherAdminProfile_checkSecondAdminProfileWasNotAdded() { - profileManagementController.addProfile( - name = "Rohit", - pin = "12345", - avatarImagePath = null, - allowDownloadAccess = true, - colorRgb = -10710042, - isAdmin = true - ).toLiveData().observeForever(mockUpdateResultObserver) - testCoroutineDispatchers.runCurrent() + addAdminProfileAndWait(name = "Rohit") - profileManagementController.addProfile( - name = "Ben", - pin = "12345", - avatarImagePath = null, - allowDownloadAccess = true, - colorRgb = -10710042, - isAdmin = true - ).toLiveData().observeForever(mockUpdateResultObserver) - testCoroutineDispatchers.runCurrent() + val addProfile2 = addAdminProfile(name = "Ben") - verifyUpdateFailed() - assertThat(updateResultCaptor.value.getErrorOrNull()).hasMessageThat() - .contains("Profile cannot be an admin") + val error = monitorFactory.waitForNextFailureResult(addProfile2) + assertThat(error).hasMessageThat().contains("Profile cannot be an admin") } @Test fun testDeviceSettings_addAdminProfile_getDefaultDeviceSettings_isSuccessful() { - profileManagementController.addProfile( - name = "James", - pin = "12345", - avatarImagePath = null, - allowDownloadAccess = true, - colorRgb = -10710042, - isAdmin = true - ).toLiveData().observeForever(mockUpdateResultObserver) - testCoroutineDispatchers.runCurrent() + addAdminProfileAndWait(name = "James") - profileManagementController.getDeviceSettings() - .toLiveData() - .observeForever(mockDeviceSettingsObserver) - testCoroutineDispatchers.runCurrent() - verifyGetDeviceSettingsSucceeded() + val deviceSettingsProvider = profileManagementController.getDeviceSettings() - val deviceSettings = deviceSettingsResultCaptor.value.getOrThrow() + val deviceSettings = monitorFactory.waitForNextSuccessfulResult(deviceSettingsProvider) assertThat(deviceSettings.allowDownloadAndUpdateOnlyOnWifi).isFalse() assertThat(deviceSettings.automaticallyUpdateTopics).isFalse() } @Test fun testDeviceSettings_addAdminProfile_updateDeviceWifiSettings_getDeviceSettings_isSuccessful() { - profileManagementController.addProfile( - name = "James", - pin = "12345", - avatarImagePath = null, - allowDownloadAccess = true, - colorRgb = -10710042, - isAdmin = true - ).toLiveData().observeForever(mockUpdateResultObserver) - testCoroutineDispatchers.runCurrent() - - val adminProfileId = ProfileId.newBuilder().setInternalId(0).build() - profileManagementController.updateWifiPermissionDeviceSettings( - adminProfileId, - /* downloadAndUpdateOnWifiOnly = */ true - ).toLiveData().observeForever(mockUpdateResultObserver) - testCoroutineDispatchers.runCurrent() + addAdminProfileAndWait(name = "James") - verifyUpdateSucceeded() + val updateProvider = profileManagementController.updateWifiPermissionDeviceSettings( + ADMIN_PROFILE_ID_0, + downloadAndUpdateOnWifiOnly = true + ) + monitorFactory.ensureDataProviderExecutes(updateProvider) - profileManagementController.getDeviceSettings() - .toLiveData() - .observeForever(mockDeviceSettingsObserver) - testCoroutineDispatchers.runCurrent() - - verifyGetDeviceSettingsSucceeded() - val deviceSettings = deviceSettingsResultCaptor.value.getOrThrow() + val deviceSettingsProvider = profileManagementController.getDeviceSettings() + val deviceSettings = monitorFactory.waitForNextSuccessfulResult(deviceSettingsProvider) assertThat(deviceSettings.allowDownloadAndUpdateOnlyOnWifi).isTrue() assertThat(deviceSettings.automaticallyUpdateTopics).isFalse() } @Test fun testDeviceSettings_addAdminProfile_updateTopicsAutoDeviceSettings_isSuccessful() { - profileManagementController.addProfile( - name = "James", - pin = "12345", - avatarImagePath = null, - allowDownloadAccess = true, - colorRgb = -10710042, - isAdmin = true - ).toLiveData().observeForever(mockUpdateResultObserver) - testCoroutineDispatchers.runCurrent() - - val adminProfileId = ProfileId.newBuilder().setInternalId(0).build() - profileManagementController - .updateTopicAutomaticallyPermissionDeviceSettings( - adminProfileId, - /* automaticallyUpdateTopics = */ true - ).toLiveData() - .observeForever(mockUpdateResultObserver) - testCoroutineDispatchers.runCurrent() - - verifyUpdateSucceeded() - - profileManagementController.getDeviceSettings() - .toLiveData() - .observeForever(mockDeviceSettingsObserver) - testCoroutineDispatchers.runCurrent() + addAdminProfileAndWait(name = "James") - verify( - mockDeviceSettingsObserver, - atLeastOnce() - ).onChanged(deviceSettingsResultCaptor.capture()) - assertThat(deviceSettingsResultCaptor.value.isSuccess()).isTrue() + val updateProvider = + profileManagementController.updateTopicAutomaticallyPermissionDeviceSettings( + ADMIN_PROFILE_ID_0, automaticallyUpdateTopics = true + ) + monitorFactory.ensureDataProviderExecutes(updateProvider) - val deviceSettings = deviceSettingsResultCaptor.value.getOrThrow() + val deviceSettingsProvider = profileManagementController.getDeviceSettings() + val deviceSettings = monitorFactory.waitForNextSuccessfulResult(deviceSettingsProvider) assertThat(deviceSettings.allowDownloadAndUpdateOnlyOnWifi).isFalse() assertThat(deviceSettings.automaticallyUpdateTopics).isTrue() } @Test fun testDeviceSettings_addAdminProfile_updateDeviceWifiSettings_andTopicDevSettings_succeeds() { - profileManagementController.addProfile( - name = "James", - pin = "12345", - avatarImagePath = null, - allowDownloadAccess = true, - colorRgb = -10710042, - isAdmin = true - ).toLiveData().observeForever(mockUpdateResultObserver) - testCoroutineDispatchers.runCurrent() - - val adminProfileId = ProfileId.newBuilder().setInternalId(0).build() - profileManagementController.updateWifiPermissionDeviceSettings( - adminProfileId, - /* downloadAndUpdateOnWifiOnly = */ true - ).toLiveData().observeForever(mockUpdateResultObserver) - testCoroutineDispatchers.runCurrent() - verifyUpdateSucceeded() + addAdminProfileAndWait(name = "James") - profileManagementController.updateTopicAutomaticallyPermissionDeviceSettings( - adminProfileId, - /* automaticallyUpdateTopics = */ true - ).toLiveData().observeForever(mockUpdateResultObserver) - testCoroutineDispatchers.runCurrent() - verifyUpdateSucceeded() - - profileManagementController.getDeviceSettings() - .toLiveData() - .observeForever(mockDeviceSettingsObserver) - testCoroutineDispatchers.runCurrent() - verifyGetDeviceSettingsSucceeded() + val updateProvider1 = + profileManagementController.updateWifiPermissionDeviceSettings( + ADMIN_PROFILE_ID_0, downloadAndUpdateOnWifiOnly = true + ) + monitorFactory.ensureDataProviderExecutes(updateProvider1) + val updateProvider2 = + profileManagementController.updateTopicAutomaticallyPermissionDeviceSettings( + ADMIN_PROFILE_ID_0, automaticallyUpdateTopics = true + ) + monitorFactory.ensureDataProviderExecutes(updateProvider2) - val deviceSettings = deviceSettingsResultCaptor.value.getOrThrow() + val deviceSettingsProvider = profileManagementController.getDeviceSettings() + val deviceSettings = monitorFactory.waitForNextSuccessfulResult(deviceSettingsProvider) assertThat(deviceSettings.allowDownloadAndUpdateOnlyOnWifi).isTrue() assertThat(deviceSettings.automaticallyUpdateTopics).isTrue() } @Test fun testDeviceSettings_updateDeviceWifiSettings_fromUserProfile_isFailure() { - profileManagementController.addProfile( - name = "James", - pin = "12345", - avatarImagePath = null, - allowDownloadAccess = true, - colorRgb = -10710042, - isAdmin = true - ).toLiveData().observeForever(mockUpdateResultObserver) - testCoroutineDispatchers.runCurrent() + addAdminProfileAndWait(name = "James") + addNonAdminProfileAndWait(name = "Rajat", pin = "01234") - profileManagementController.addProfile( - name = "Rajat", - pin = "01234", - avatarImagePath = null, - allowDownloadAccess = true, - colorRgb = -10710042, - isAdmin = false - ).toLiveData().observeForever(mockUpdateResultObserver) - testCoroutineDispatchers.runCurrent() + val updateProvider = + profileManagementController.updateWifiPermissionDeviceSettings( + PROFILE_ID_1, downloadAndUpdateOnWifiOnly = true + ) - val userProfileId = ProfileId.newBuilder().setInternalId(1).build() - profileManagementController.updateWifiPermissionDeviceSettings( - userProfileId, - /* downloadAndUpdateOnWifiOnly = */ true - ).toLiveData().observeForever(mockUpdateResultObserver) - testCoroutineDispatchers.runCurrent() - verifyUpdateFailed() + monitorFactory.waitForNextFailureResult(updateProvider) } @Test fun testDeviceSettings_updateTopicsAutomaticallyDeviceSettings_fromUserProfile_isFailure() { - profileManagementController.addProfile( - name = "James", - pin = "12345", - avatarImagePath = null, - allowDownloadAccess = true, - colorRgb = -10710042, - isAdmin = true - ).toLiveData().observeForever(mockUpdateResultObserver) - testCoroutineDispatchers.runCurrent() - - profileManagementController.addProfile( - name = "Rajat", - pin = "01234", - avatarImagePath = null, - allowDownloadAccess = true, - colorRgb = -10710042, - isAdmin = false - ).toLiveData().observeForever(mockUpdateResultObserver) - testCoroutineDispatchers.runCurrent() + addAdminProfileAndWait(name = "James") + addNonAdminProfileAndWait(name = "Rajat", pin = "01234") - val userProfileId = ProfileId.newBuilder().setInternalId(1).build() - profileManagementController.updateTopicAutomaticallyPermissionDeviceSettings( - userProfileId, - /* automaticallyUpdateTopics = */ true - ).toLiveData().observeForever(mockUpdateResultObserver) - testCoroutineDispatchers.runCurrent() - verifyUpdateFailed() - } - - private fun verifyGetDeviceSettingsSucceeded() { - verify( - mockDeviceSettingsObserver, - atLeastOnce() - ).onChanged(deviceSettingsResultCaptor.capture()) - assertThat(deviceSettingsResultCaptor.value.isSuccess()).isTrue() - } - - private fun verifyGetProfileSucceeded() { - verify(mockProfileObserver, atLeastOnce()).onChanged(profileResultCaptor.capture()) - assertThat(profileResultCaptor.value.isSuccess()).isTrue() - } - - private fun verifyGetMultipleProfilesSucceeded() { - verify(mockProfilesObserver, atLeastOnce()).onChanged(profilesResultCaptor.capture()) - assertThat(profilesResultCaptor.value.isSuccess()).isTrue() - } - - private fun verifyUpdateSucceeded() { - verify(mockUpdateResultObserver, atLeastOnce()).onChanged(updateResultCaptor.capture()) - assertThat(updateResultCaptor.value.isSuccess()).isTrue() - } - - private fun verifyUpdateFailed() { - verify(mockUpdateResultObserver, atLeastOnce()).onChanged(updateResultCaptor.capture()) - assertThat(updateResultCaptor.value.isFailure()).isTrue() - } + val updateProvider = + profileManagementController.updateTopicAutomaticallyPermissionDeviceSettings( + PROFILE_ID_1, automaticallyUpdateTopics = true + ) - private fun verifyWasProfileEverAddedSucceeded() { - verify( - mockWasProfileAddedResultObserver, - atLeastOnce() - ).onChanged(wasProfileAddedResultCaptor.capture()) - assertThat(wasProfileAddedResultCaptor.value.isSuccess()).isTrue() + monitorFactory.waitForNextFailureResult(updateProvider) } private fun addTestProfiles() { - PROFILES_LIST.forEach { - profileManagementController.addProfile( - name = it.name, - pin = it.pin, - avatarImagePath = null, - allowDownloadAccess = it.allowDownloadAccess, - colorRgb = -10710042, - isAdmin = false - ).toLiveData() + val profileAdditionProviders = PROFILES_LIST.map { + addNonAdminProfile(it.name, pin = it.pin, allowDownloadAccess = it.allowDownloadAccess) } + profileAdditionProviders.forEach(monitorFactory::ensureDataProviderExecutes) } private fun checkTestProfilesArePresent(resultList: List) { @@ -1023,13 +580,52 @@ class ProfileManagementControllerTest { private fun readProfileDatabase(): ProfileDatabase { return FileInputStream( - File( - context.filesDir, - "profile_database.cache" - ) + File(context.filesDir, "profile_database.cache") ).use(ProfileDatabase::parseFrom) } + private fun addAdminProfile(name: String, pin: String = DEFAULT_PIN): DataProvider = + addProfile(name, pin, isAdmin = true) + + private fun addAdminProfileAndWait(name: String, pin: String = DEFAULT_PIN) { + monitorFactory.ensureDataProviderExecutes(addAdminProfile(name, pin)) + } + + private fun addNonAdminProfile( + name: String, + pin: String = DEFAULT_PIN, + allowDownloadAccess: Boolean = DEFAULT_ALLOW_DOWNLOAD_ACCESS, + colorRgb: Int = DEFAULT_AVATAR_COLOR_RGB + ): DataProvider { + return addProfile( + name, pin, avatarImagePath = null, allowDownloadAccess, colorRgb, isAdmin = false + ) + } + + private fun addNonAdminProfileAndWait( + name: String, + pin: String = DEFAULT_PIN, + allowDownloadAccess: Boolean = DEFAULT_ALLOW_DOWNLOAD_ACCESS, + colorRgb: Int = DEFAULT_AVATAR_COLOR_RGB + ) { + monitorFactory.ensureDataProviderExecutes( + addNonAdminProfile(name, pin, allowDownloadAccess, colorRgb) + ) + } + + private fun addProfile( + name: String, + pin: String = DEFAULT_PIN, + avatarImagePath: Uri? = null, + allowDownloadAccess: Boolean = DEFAULT_ALLOW_DOWNLOAD_ACCESS, + colorRgb: Int = DEFAULT_AVATAR_COLOR_RGB, + isAdmin: Boolean + ): DataProvider { + return profileManagementController.addProfile( + name, pin, avatarImagePath, allowDownloadAccess, colorRgb, isAdmin + ) + } + // TODO(#89): Move this to a common test application component. @Module class TestModule { diff --git a/domain/src/test/java/org/oppia/android/domain/topic/StoryProgressControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/topic/StoryProgressControllerTest.kt index 39f554a9034..4c7ea4e279c 100644 --- a/domain/src/test/java/org/oppia/android/domain/topic/StoryProgressControllerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/topic/StoryProgressControllerTest.kt @@ -2,7 +2,6 @@ package org.oppia.android.domain.topic import android.app.Application import android.content.Context -import androidx.lifecycle.Observer import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat @@ -10,31 +9,22 @@ import dagger.BindsInstance import dagger.Component import dagger.Module import dagger.Provides +import javax.inject.Inject +import javax.inject.Singleton import org.junit.Before -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentCaptor -import org.mockito.Captor -import org.mockito.Mock -import org.mockito.Mockito.atLeastOnce -import org.mockito.Mockito.reset -import org.mockito.Mockito.verify -import org.mockito.junit.MockitoJUnit -import org.mockito.junit.MockitoRule import org.oppia.android.app.model.ChapterPlayState import org.oppia.android.app.model.ProfileId import org.oppia.android.domain.oppialogger.LogStorageModule import org.oppia.android.testing.TestLogReportingModule +import org.oppia.android.testing.data.DataProviderTestMonitor import org.oppia.android.testing.profile.ProfileTestHelper import org.oppia.android.testing.robolectric.RobolectricModule -import org.oppia.android.testing.threading.TestCoroutineDispatchers import org.oppia.android.testing.threading.TestDispatcherModule import org.oppia.android.testing.time.FakeOppiaClock import org.oppia.android.testing.time.FakeOppiaClockModule import org.oppia.android.util.caching.CacheAssetsLocally -import org.oppia.android.util.data.AsyncResult -import org.oppia.android.util.data.DataProviders.Companion.toLiveData import org.oppia.android.util.data.DataProvidersInjector import org.oppia.android.util.data.DataProvidersInjectorProvider import org.oppia.android.util.locale.LocaleProdModule @@ -45,45 +35,20 @@ import org.oppia.android.util.logging.LogLevel import org.oppia.android.util.networking.NetworkConnectionUtilDebugModule import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode -import javax.inject.Inject -import javax.inject.Singleton /** Tests for [StoryProgressController]. */ +// FunctionName: test names are conventionally named with underscores. +// SameParameterValue: tests should have specific context included/excluded for readability. +@Suppress("FunctionName", "SameParameterValue") @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) @Config(application = StoryProgressControllerTest.TestApplication::class) class StoryProgressControllerTest { - - @Rule - @JvmField - val mockitoRule: MockitoRule = MockitoJUnit.rule() - - @Inject - lateinit var testCoroutineDispatchers: TestCoroutineDispatchers - - @Inject - lateinit var context: Context - - @Inject - lateinit var storyProgressController: StoryProgressController - - @Inject - lateinit var profileTestHelper: ProfileTestHelper - - @Inject - lateinit var fakeOppiaClock: FakeOppiaClock - - @Mock - lateinit var mockRecordProgressObserver: Observer> - - @Captor - lateinit var recordProgressResultCaptor: ArgumentCaptor> - - @Mock - lateinit var mockRetrieveChapterPlayStateObserver: Observer> - - @Captor - lateinit var retrieveChapterPlayStateCaptor: ArgumentCaptor> + @Inject lateinit var context: Context + @Inject lateinit var storyProgressController: StoryProgressController + @Inject lateinit var profileTestHelper: ProfileTestHelper + @Inject lateinit var fakeOppiaClock: FakeOppiaClock + @Inject lateinit var monitorFactory: DataProviderTestMonitor.Factory private lateinit var profileId: ProfileId @@ -99,244 +64,225 @@ class StoryProgressControllerTest { @Test fun testStoryProgressController_recordCompletedChapter_isSuccessful() { - storyProgressController.recordCompletedChapter( - profileId, - FRACTIONS_TOPIC_ID, - FRACTIONS_STORY_ID_0, - FRACTIONS_EXPLORATION_ID_0, - fakeOppiaClock.getCurrentTimeMs() - ).toLiveData().observeForever(mockRecordProgressObserver) - testCoroutineDispatchers.runCurrent() - - verifyRecordProgressSucceeded() + val recordProvider = + storyProgressController.recordCompletedChapter( + profileId, + FRACTIONS_TOPIC_ID, + FRACTIONS_STORY_ID_0, + FRACTIONS_EXPLORATION_ID_0, + fakeOppiaClock.getCurrentTimeMs() + ) + + monitorFactory.waitForNextSuccessfulResult(recordProvider) } @Test fun testStoryProgressController_recordChapterAsInProgressSaved_isSuccessful() { - storyProgressController.recordChapterAsInProgressSaved( - profileId, - FRACTIONS_TOPIC_ID, - FRACTIONS_STORY_ID_0, - FRACTIONS_EXPLORATION_ID_0, - fakeOppiaClock.getCurrentTimeMs() - ).toLiveData().observeForever(mockRecordProgressObserver) - testCoroutineDispatchers.runCurrent() - - verifyRecordProgressSucceeded() + val recordProvider = + storyProgressController.recordChapterAsInProgressSaved( + profileId, + FRACTIONS_TOPIC_ID, + FRACTIONS_STORY_ID_0, + FRACTIONS_EXPLORATION_ID_0, + fakeOppiaClock.getCurrentTimeMs() + ) + + monitorFactory.waitForNextSuccessfulResult(recordProvider) } @Test fun testStoryProgressController_chapterCompleted_markChapterAsSaved_playStateIsCompleted() { - storyProgressController.recordCompletedChapter( - profileId, - FRACTIONS_TOPIC_ID, - FRACTIONS_STORY_ID_0, - FRACTIONS_EXPLORATION_ID_0, - fakeOppiaClock.getCurrentTimeMs() - ).toLiveData().observeForever(mockRecordProgressObserver) - testCoroutineDispatchers.runCurrent() - - verifyRecordProgressSucceeded() + monitorFactory.ensureDataProviderExecutes( + storyProgressController.recordCompletedChapter( + profileId, + FRACTIONS_TOPIC_ID, + FRACTIONS_STORY_ID_0, + FRACTIONS_EXPLORATION_ID_0, + fakeOppiaClock.getCurrentTimeMs() + ) + ) - storyProgressController.recordChapterAsInProgressSaved( + val recordInProgressProvider = storyProgressController.recordChapterAsInProgressSaved( profileId, FRACTIONS_TOPIC_ID, FRACTIONS_STORY_ID_0, FRACTIONS_EXPLORATION_ID_0, fakeOppiaClock.getCurrentTimeMs() - ).toLiveData().observeForever(mockRecordProgressObserver) - testCoroutineDispatchers.runCurrent() - - verifyRecordProgressSucceeded() - verifyChapterPlayStateIsCorrect( - profileId, - FRACTIONS_TOPIC_ID, - FRACTIONS_STORY_ID_0, - FRACTIONS_EXPLORATION_ID_0, - ChapterPlayState.COMPLETED ) + + monitorFactory.waitForNextSuccessfulResult(recordInProgressProvider) + val playState = + retrieveChapterPlayState( + profileId, + FRACTIONS_TOPIC_ID, + FRACTIONS_STORY_ID_0, + FRACTIONS_EXPLORATION_ID_0 + ) + assertThat(playState).isEqualTo(ChapterPlayState.COMPLETED) } @Test fun testStoryProgressController_chapterNotStarted_markChapterAsSaved_playStateIsSaved() { - storyProgressController.recordChapterAsInProgressSaved( - profileId, - FRACTIONS_TOPIC_ID, - FRACTIONS_STORY_ID_0, - FRACTIONS_EXPLORATION_ID_0, - fakeOppiaClock.getCurrentTimeMs() - ).toLiveData().observeForever(mockRecordProgressObserver) - testCoroutineDispatchers.runCurrent() - - verifyRecordProgressSucceeded() - verifyChapterPlayStateIsCorrect( - profileId, - FRACTIONS_TOPIC_ID, - FRACTIONS_STORY_ID_0, - FRACTIONS_EXPLORATION_ID_0, - ChapterPlayState.IN_PROGRESS_SAVED - ) + val recordCompletionProvider = + storyProgressController.recordChapterAsInProgressSaved( + profileId, + FRACTIONS_TOPIC_ID, + FRACTIONS_STORY_ID_0, + FRACTIONS_EXPLORATION_ID_0, + fakeOppiaClock.getCurrentTimeMs() + ) + + monitorFactory.waitForNextSuccessfulResult(recordCompletionProvider) + val playState = + retrieveChapterPlayState( + profileId, + FRACTIONS_TOPIC_ID, + FRACTIONS_STORY_ID_0, + FRACTIONS_EXPLORATION_ID_0 + ) + assertThat(playState).isEqualTo(ChapterPlayState.IN_PROGRESS_SAVED) } @Test fun testStoryProgressController_markChapterAsNotSaved_markChapterAsSaved_playStateIsSaved() { - storyProgressController.recordChapterAsInProgressNotSaved( - profileId, - FRACTIONS_TOPIC_ID, - FRACTIONS_STORY_ID_0, - FRACTIONS_EXPLORATION_ID_0, - fakeOppiaClock.getCurrentTimeMs() - ).toLiveData().observeForever(mockRecordProgressObserver) - testCoroutineDispatchers.runCurrent() - - verifyRecordProgressSucceeded() - - storyProgressController.recordChapterAsInProgressSaved( - profileId, - FRACTIONS_TOPIC_ID, - FRACTIONS_STORY_ID_0, - FRACTIONS_EXPLORATION_ID_0, - fakeOppiaClock.getCurrentTimeMs() - ).toLiveData().observeForever(mockRecordProgressObserver) - testCoroutineDispatchers.runCurrent() - - verifyRecordProgressSucceeded() - verifyChapterPlayStateIsCorrect( - profileId, - FRACTIONS_TOPIC_ID, - FRACTIONS_STORY_ID_0, - FRACTIONS_EXPLORATION_ID_0, - ChapterPlayState.IN_PROGRESS_SAVED + monitorFactory.ensureDataProviderExecutes( + storyProgressController.recordChapterAsInProgressNotSaved( + profileId, + FRACTIONS_TOPIC_ID, + FRACTIONS_STORY_ID_0, + FRACTIONS_EXPLORATION_ID_0, + fakeOppiaClock.getCurrentTimeMs() + ) ) + + val progressSavedProvider = + storyProgressController.recordChapterAsInProgressSaved( + profileId, + FRACTIONS_TOPIC_ID, + FRACTIONS_STORY_ID_0, + FRACTIONS_EXPLORATION_ID_0, + fakeOppiaClock.getCurrentTimeMs() + ) + + monitorFactory.waitForNextSuccessfulResult(progressSavedProvider) + val playState = + retrieveChapterPlayState( + profileId, + FRACTIONS_TOPIC_ID, + FRACTIONS_STORY_ID_0, + FRACTIONS_EXPLORATION_ID_0 + ) + assertThat(playState).isEqualTo(ChapterPlayState.IN_PROGRESS_SAVED) } @Test fun testStoryProgressController_recordChapterAsNotSaved_isSuccessful() { - storyProgressController.recordChapterAsInProgressNotSaved( - profileId, - FRACTIONS_TOPIC_ID, - FRACTIONS_STORY_ID_0, - FRACTIONS_EXPLORATION_ID_0, - fakeOppiaClock.getCurrentTimeMs() - ).toLiveData().observeForever(mockRecordProgressObserver) - testCoroutineDispatchers.runCurrent() - - verifyRecordProgressSucceeded() + val recordNotSavedProvider = + storyProgressController.recordChapterAsInProgressNotSaved( + profileId, + FRACTIONS_TOPIC_ID, + FRACTIONS_STORY_ID_0, + FRACTIONS_EXPLORATION_ID_0, + fakeOppiaClock.getCurrentTimeMs() + ) + + monitorFactory.waitForNextSuccessfulResult(recordNotSavedProvider) } @Test fun testStoryProgressController_chapterCompleted_markChapterAsNotSaved_playStateIsCompleted() { - storyProgressController.recordCompletedChapter( - profileId, - FRACTIONS_TOPIC_ID, - FRACTIONS_STORY_ID_0, - FRACTIONS_EXPLORATION_ID_0, - fakeOppiaClock.getCurrentTimeMs() - ).toLiveData().observeForever(mockRecordProgressObserver) - testCoroutineDispatchers.runCurrent() - - verifyRecordProgressSucceeded() - - storyProgressController.recordChapterAsInProgressNotSaved( - profileId, - FRACTIONS_TOPIC_ID, - FRACTIONS_STORY_ID_0, - FRACTIONS_EXPLORATION_ID_0, - fakeOppiaClock.getCurrentTimeMs() - ).toLiveData().observeForever(mockRecordProgressObserver) - testCoroutineDispatchers.runCurrent() - - verifyRecordProgressSucceeded() - verifyChapterPlayStateIsCorrect( - profileId, - FRACTIONS_TOPIC_ID, - FRACTIONS_STORY_ID_0, - FRACTIONS_EXPLORATION_ID_0, - ChapterPlayState.COMPLETED + monitorFactory.ensureDataProviderExecutes( + storyProgressController.recordCompletedChapter( + profileId, + FRACTIONS_TOPIC_ID, + FRACTIONS_STORY_ID_0, + FRACTIONS_EXPLORATION_ID_0, + fakeOppiaClock.getCurrentTimeMs() + ) ) + + val recordNotSavedProvider = + storyProgressController.recordChapterAsInProgressNotSaved( + profileId, + FRACTIONS_TOPIC_ID, + FRACTIONS_STORY_ID_0, + FRACTIONS_EXPLORATION_ID_0, + fakeOppiaClock.getCurrentTimeMs() + ) + + monitorFactory.waitForNextSuccessfulResult(recordNotSavedProvider) + val playState = + retrieveChapterPlayState( + profileId, + FRACTIONS_TOPIC_ID, + FRACTIONS_STORY_ID_0, + FRACTIONS_EXPLORATION_ID_0 + ) + assertThat(playState).isEqualTo(ChapterPlayState.COMPLETED) } @Test fun testStoryProgressController_chapterNotStarted_markChapterAsSaved_playStateIsNotSaved() { - storyProgressController.recordChapterAsInProgressNotSaved( - profileId, - FRACTIONS_TOPIC_ID, - FRACTIONS_STORY_ID_0, - FRACTIONS_EXPLORATION_ID_0, - fakeOppiaClock.getCurrentTimeMs() - ).toLiveData().observeForever(mockRecordProgressObserver) - testCoroutineDispatchers.runCurrent() - - verifyRecordProgressSucceeded() - verifyChapterPlayStateIsCorrect( - profileId, - FRACTIONS_TOPIC_ID, - FRACTIONS_STORY_ID_0, - FRACTIONS_EXPLORATION_ID_0, - ChapterPlayState.IN_PROGRESS_NOT_SAVED - ) + val progressNotSavedProvider = + storyProgressController.recordChapterAsInProgressNotSaved( + profileId, + FRACTIONS_TOPIC_ID, + FRACTIONS_STORY_ID_0, + FRACTIONS_EXPLORATION_ID_0, + fakeOppiaClock.getCurrentTimeMs() + ) + + monitorFactory.waitForNextSuccessfulResult(progressNotSavedProvider) + val playState = + retrieveChapterPlayState( + profileId, + FRACTIONS_TOPIC_ID, + FRACTIONS_STORY_ID_0, + FRACTIONS_EXPLORATION_ID_0 + ) + assertThat(playState).isEqualTo(ChapterPlayState.IN_PROGRESS_NOT_SAVED) } @Test fun testStoryProgressController_markChapterAsSaved_markChapterAsNotSaved_playStateIsNotSaved() { - storyProgressController.recordChapterAsInProgressSaved( - profileId, - FRACTIONS_TOPIC_ID, - FRACTIONS_STORY_ID_0, - FRACTIONS_EXPLORATION_ID_0, - fakeOppiaClock.getCurrentTimeMs() - ).toLiveData().observeForever(mockRecordProgressObserver) - testCoroutineDispatchers.runCurrent() - - verifyRecordProgressSucceeded() - - storyProgressController.recordChapterAsInProgressNotSaved( - profileId, - FRACTIONS_TOPIC_ID, - FRACTIONS_STORY_ID_0, - FRACTIONS_EXPLORATION_ID_0, - fakeOppiaClock.getCurrentTimeMs() - ).toLiveData().observeForever(mockRecordProgressObserver) - testCoroutineDispatchers.runCurrent() - - verifyRecordProgressSucceeded() - verifyChapterPlayStateIsCorrect( - profileId, - FRACTIONS_TOPIC_ID, - FRACTIONS_STORY_ID_0, - FRACTIONS_EXPLORATION_ID_0, - ChapterPlayState.IN_PROGRESS_NOT_SAVED + monitorFactory.ensureDataProviderExecutes( + storyProgressController.recordChapterAsInProgressSaved( + profileId, + FRACTIONS_TOPIC_ID, + FRACTIONS_STORY_ID_0, + FRACTIONS_EXPLORATION_ID_0, + fakeOppiaClock.getCurrentTimeMs() + ) ) - } - - private fun verifyChapterPlayStateIsCorrect( - profileId: ProfileId, - topicId: String, - storyId: String, - explorationId: String, - chapterPlayState: ChapterPlayState - ) { - storyProgressController.retrieveChapterPlayStateByExplorationId( - profileId, - topicId, - storyId, - explorationId - ).toLiveData().observeForever(mockRetrieveChapterPlayStateObserver) - - testCoroutineDispatchers.runCurrent() - - verify(mockRetrieveChapterPlayStateObserver, atLeastOnce()) - .onChanged(retrieveChapterPlayStateCaptor.capture()) - assertThat(retrieveChapterPlayStateCaptor.value.isSuccess()).isTrue() - assertThat(retrieveChapterPlayStateCaptor.value.getOrThrow()).isEqualTo(chapterPlayState) + val progressNotSavedProvider = + storyProgressController.recordChapterAsInProgressNotSaved( + profileId, + FRACTIONS_TOPIC_ID, + FRACTIONS_STORY_ID_0, + FRACTIONS_EXPLORATION_ID_0, + fakeOppiaClock.getCurrentTimeMs() + ) + + monitorFactory.waitForNextSuccessfulResult(progressNotSavedProvider) + val playState = + retrieveChapterPlayState( + profileId, + FRACTIONS_TOPIC_ID, + FRACTIONS_STORY_ID_0, + FRACTIONS_EXPLORATION_ID_0 + ) + assertThat(playState).isEqualTo(ChapterPlayState.IN_PROGRESS_NOT_SAVED) } - private fun verifyRecordProgressSucceeded() { - verify(mockRecordProgressObserver, atLeastOnce()) - .onChanged(recordProgressResultCaptor.capture()) - assertThat(recordProgressResultCaptor.value.isSuccess()).isTrue() - reset(mockRecordProgressObserver) + private fun retrieveChapterPlayState( + profileId: ProfileId, topicId: String, storyId: String, explorationId: String + ): ChapterPlayState { + val playStateProvider = + storyProgressController.retrieveChapterPlayStateByExplorationId( + profileId, topicId, storyId, explorationId + ) + return monitorFactory.waitForNextSuccessfulResult(playStateProvider) } // TODO(#89): Move this to a common test application component. diff --git a/domain/src/test/java/org/oppia/android/domain/topic/TopicControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/topic/TopicControllerTest.kt index 72329652946..3c11b74d378 100755 --- a/domain/src/test/java/org/oppia/android/domain/topic/TopicControllerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/topic/TopicControllerTest.kt @@ -2,7 +2,6 @@ package org.oppia.android.domain.topic import android.app.Application import android.content.Context -import androidx.lifecycle.Observer import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat @@ -11,27 +10,19 @@ import dagger.BindsInstance import dagger.Component import dagger.Module import dagger.Provides +import java.util.Locale +import javax.inject.Inject +import javax.inject.Singleton import org.junit.Before import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentCaptor -import org.mockito.Captor -import org.mockito.Mock -import org.mockito.Mockito.atLeastOnce -import org.mockito.Mockito.verify -import org.mockito.junit.MockitoJUnit -import org.mockito.junit.MockitoRule import org.oppia.android.app.model.ChapterPlayState import org.oppia.android.app.model.ChapterSummary -import org.oppia.android.app.model.CompletedStoryList -import org.oppia.android.app.model.OngoingTopicList import org.oppia.android.app.model.OppiaLanguage import org.oppia.android.app.model.ProfileId -import org.oppia.android.app.model.Question import org.oppia.android.app.model.StorySummary -import org.oppia.android.app.model.Topic import org.oppia.android.app.model.TopicPlayAvailability.AvailabilityCase.AVAILABLE_TO_PLAY_IN_FUTURE import org.oppia.android.app.model.TopicPlayAvailability.AvailabilityCase.AVAILABLE_TO_PLAY_NOW import org.oppia.android.app.model.WrittenTranslationLanguageSelection @@ -54,8 +45,6 @@ import org.oppia.android.util.caching.AssetModule import org.oppia.android.util.caching.CacheAssetsLocally import org.oppia.android.util.caching.LoadLessonProtosFromAssets import org.oppia.android.util.caching.TopicListToCache -import org.oppia.android.util.data.AsyncResult -import org.oppia.android.util.data.DataProviders.Companion.toLiveData import org.oppia.android.util.data.DataProvidersInjector import org.oppia.android.util.data.DataProvidersInjectorProvider import org.oppia.android.util.locale.LocaleProdModule @@ -66,9 +55,6 @@ import org.oppia.android.util.logging.LogLevel import org.oppia.android.util.networking.NetworkConnectionUtilDebugModule import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode -import java.util.Locale -import javax.inject.Inject -import javax.inject.Singleton private const val INVALID_STORY_ID_1 = "INVALID_STORY_ID_1" private const val INVALID_TOPIC_ID_1 = "INVALID_TOPIC_ID_1" @@ -80,73 +66,16 @@ private const val INVALID_TOPIC_ID_1 = "INVALID_TOPIC_ID_1" @LooperMode(LooperMode.Mode.PAUSED) @Config(application = TopicControllerTest.TestApplication::class) class TopicControllerTest { - @Rule - @JvmField - val mockitoRule: MockitoRule = MockitoJUnit.rule() - - @get:Rule - val oppiaTestRule = OppiaTestRule() - - @Inject - lateinit var context: Context - - @Inject - lateinit var storyProgressTestHelper: StoryProgressTestHelper - - @Inject - lateinit var topicController: TopicController - - @Inject - lateinit var fakeExceptionLogger: FakeExceptionLogger - - @Inject - lateinit var translationController: TranslationController - - @Mock - lateinit var mockCompletedStoryListObserver: Observer> - - @Captor - lateinit var completedStoryListResultCaptor: ArgumentCaptor> - - @Mock - lateinit var mockOngoingTopicListObserver: Observer> - - @Captor - lateinit var ongoingTopicListResultCaptor: ArgumentCaptor> - - @Mock - lateinit var mockQuestionListObserver: Observer>> - - @Captor - lateinit var questionListResultCaptor: ArgumentCaptor>> - - @Mock - lateinit var mockStorySummaryObserver: Observer> - - @Captor - lateinit var storySummaryResultCaptor: ArgumentCaptor> - - @Mock - lateinit var mockChapterSummaryObserver: Observer> - - @Captor - lateinit var chapterSummaryResultCaptor: ArgumentCaptor> - - @Mock - lateinit var mockTopicObserver: Observer> - - @Captor - lateinit var topicResultCaptor: ArgumentCaptor> - - @Inject - lateinit var testCoroutineDispatchers: TestCoroutineDispatchers - - @Inject - lateinit var fakeOppiaClock: FakeOppiaClock + @get:Rule val oppiaTestRule = OppiaTestRule() - // TODO(#3813): Migrate all tests in this suite to use this factory. - @Inject - lateinit var monitorFactory: DataProviderTestMonitor.Factory + @Inject lateinit var context: Context + @Inject lateinit var storyProgressTestHelper: StoryProgressTestHelper + @Inject lateinit var topicController: TopicController + @Inject lateinit var fakeExceptionLogger: FakeExceptionLogger + @Inject lateinit var translationController: TranslationController + @Inject lateinit var testCoroutineDispatchers: TestCoroutineDispatchers + @Inject lateinit var fakeOppiaClock: FakeOppiaClock + @Inject lateinit var monitorFactory: DataProviderTestMonitor.Factory private lateinit var profileId1: ProfileId private lateinit var profileId2: ProfileId @@ -161,76 +90,52 @@ class TopicControllerTest { @Test fun testRetrieveTopic_validSecondTopic_returnsCorrectTopic() { - topicController.getTopic( - profileId1, TEST_TOPIC_ID_1 - ).toLiveData().observeForever(mockTopicObserver) - testCoroutineDispatchers.runCurrent() + val topicProvider = topicController.getTopic(profileId1, TEST_TOPIC_ID_1) - verifyGetTopicSucceeded() - val topic = topicResultCaptor.value.getOrThrow() + val topic = monitorFactory.waitForNextSuccessfulResult(topicProvider) assertThat(topic.topicId).isEqualTo(TEST_TOPIC_ID_1) } @Test fun testRetrieveTopic_validSecondTopic_returnsTopicWithThumbnail() { - topicController.getTopic( - profileId1, TEST_TOPIC_ID_1 - ).toLiveData().observeForever(mockTopicObserver) - testCoroutineDispatchers.runCurrent() + val topicProvider = topicController.getTopic(profileId1, TEST_TOPIC_ID_1) - verifyGetTopicSucceeded() - val topic = topicResultCaptor.value!!.getOrThrow() + val topic = monitorFactory.waitForNextSuccessfulResult(topicProvider) assertThat(topic.topicThumbnail.backgroundColorRgb).isNotEqualTo(0) } @Test fun testRetrieveTopic_fractionsTopic_returnsCorrectTopic() { - topicController.getTopic( - profileId1, FRACTIONS_TOPIC_ID - ).toLiveData().observeForever(mockTopicObserver) - testCoroutineDispatchers.runCurrent() + val topicProvider = topicController.getTopic(profileId1, FRACTIONS_TOPIC_ID) - verifyGetTopicSucceeded() - val topic = topicResultCaptor.value!!.getOrThrow() + val topic = monitorFactory.waitForNextSuccessfulResult(topicProvider) assertThat(topic.topicId).isEqualTo(FRACTIONS_TOPIC_ID) assertThat(topic.storyCount).isEqualTo(1) } @Test fun testRetrieveTopic_fractionsTopic_hasCorrectDescription() { - topicController.getTopic( - profileId1, FRACTIONS_TOPIC_ID - ).toLiveData().observeForever(mockTopicObserver) - testCoroutineDispatchers.runCurrent() + val topicProvider = topicController.getTopic(profileId1, FRACTIONS_TOPIC_ID) - verifyGetTopicSucceeded() - val topic = topicResultCaptor.value!!.getOrThrow() + val topic = monitorFactory.waitForNextSuccessfulResult(topicProvider) assertThat(topic.topicId).isEqualTo(FRACTIONS_TOPIC_ID) assertThat(topic.description).contains("You'll often need to talk about") } @Test fun testRetrieveTopic_ratiosTopic_returnsCorrectTopic() { - topicController.getTopic( - profileId1, RATIOS_TOPIC_ID - ).toLiveData().observeForever(mockTopicObserver) - testCoroutineDispatchers.runCurrent() + val topicProvider = topicController.getTopic(profileId1, RATIOS_TOPIC_ID) - verifyGetTopicSucceeded() - val topic = topicResultCaptor.value!!.getOrThrow() + val topic = monitorFactory.waitForNextSuccessfulResult(topicProvider) assertThat(topic.topicId).isEqualTo(RATIOS_TOPIC_ID) assertThat(topic.storyCount).isEqualTo(2) } @Test fun testRetrieveTopic_ratiosTopic_hasCorrectDescription() { - topicController.getTopic( - profileId1, RATIOS_TOPIC_ID - ).toLiveData().observeForever(mockTopicObserver) - testCoroutineDispatchers.runCurrent() + val topicProvider = topicController.getTopic(profileId1, RATIOS_TOPIC_ID) - verifyGetTopicSucceeded() - val topic = topicResultCaptor.value!!.getOrThrow() + val topic = monitorFactory.waitForNextSuccessfulResult(topicProvider) assertThat(topic.topicId).isEqualTo(RATIOS_TOPIC_ID) assertThat(topic.description).contains( "Many everyday problems involve thinking about proportions" @@ -239,227 +144,165 @@ class TopicControllerTest { @Test fun testRetrieveTopic_invalidTopic_returnsFailure() { - topicController.getTopic( - profileId1, INVALID_TOPIC_ID_1 - ).toLiveData().observeForever(mockTopicObserver) - testCoroutineDispatchers.runCurrent() + val topicProvider = topicController.getTopic(profileId1, INVALID_TOPIC_ID_1) - verifyGetTopicFailed() + monitorFactory.waitForNextFailureResult(topicProvider) } @Test fun testRetrieveTopic_testTopic_published_returnsAsAvailable() { - topicController.getTopic( - profileId1, TEST_TOPIC_ID_0 - ).toLiveData().observeForever(mockTopicObserver) - testCoroutineDispatchers.runCurrent() + val topicProvider = topicController.getTopic(profileId1, TEST_TOPIC_ID_0) - verifyGetTopicSucceeded() - val topic = topicResultCaptor.value.getOrThrow() + val topic = monitorFactory.waitForNextSuccessfulResult(topicProvider) assertThat(topic.topicPlayAvailability.availabilityCase).isEqualTo(AVAILABLE_TO_PLAY_NOW) } @Test fun testRetrieveTopic_testTopic_unpublished_returnsAsAvailableInFuture() { - topicController.getTopic( - profileId1, TEST_TOPIC_ID_2 - ).toLiveData().observeForever(mockTopicObserver) - testCoroutineDispatchers.runCurrent() + val topicProvider = topicController.getTopic(profileId1, TEST_TOPIC_ID_2) - verifyGetTopicSucceeded() - val topic = topicResultCaptor.value.getOrThrow() + val topic = monitorFactory.waitForNextSuccessfulResult(topicProvider) assertThat(topic.topicPlayAvailability.availabilityCase).isEqualTo(AVAILABLE_TO_PLAY_IN_FUTURE) } @Test fun testRetrieveStory_validStory_isSuccessful() { - topicController.getStory(profileId1, TEST_TOPIC_ID_1, TEST_STORY_ID_2).toLiveData() - .observeForever(mockStorySummaryObserver) - testCoroutineDispatchers.runCurrent() + val storyProvider = topicController.getStory(profileId1, TEST_TOPIC_ID_1, TEST_STORY_ID_2) - verifyGetStorySucceeded() - val storyResult = storySummaryResultCaptor.value - assertThat(storyResult).isNotNull() - assertThat(storyResult!!.isSuccess()).isTrue() + monitorFactory.waitForNextSuccessfulResult(storyProvider) } @Test fun testRetrieveStory_validStory_returnsCorrectStory() { - topicController.getStory(profileId1, TEST_TOPIC_ID_1, TEST_STORY_ID_2).toLiveData() - .observeForever(mockStorySummaryObserver) - testCoroutineDispatchers.runCurrent() + val storyProvider = topicController.getStory(profileId1, TEST_TOPIC_ID_1, TEST_STORY_ID_2) - verifyGetStorySucceeded() - val story = storySummaryResultCaptor.value!!.getOrThrow() + val story = monitorFactory.waitForNextSuccessfulResult(storyProvider) assertThat(story.storyId).isEqualTo(TEST_STORY_ID_2) } @Test fun testRetrieveStory_validStory_returnsStoryWithName() { - topicController.getStory(profileId1, TEST_TOPIC_ID_1, TEST_STORY_ID_2).toLiveData() - .observeForever(mockStorySummaryObserver) - testCoroutineDispatchers.runCurrent() + val storyProvider = topicController.getStory(profileId1, TEST_TOPIC_ID_1, TEST_STORY_ID_2) - verifyGetStorySucceeded() - val story = storySummaryResultCaptor.value!!.getOrThrow() + val story = monitorFactory.waitForNextSuccessfulResult(storyProvider) assertThat(story.storyName).isEqualTo("Other Interesting Story") } @Test fun testRetrieveStory_fractionsStory_returnsCorrectStory() { - topicController.getStory(profileId1, FRACTIONS_TOPIC_ID, FRACTIONS_STORY_ID_0).toLiveData() - .observeForever(mockStorySummaryObserver) - testCoroutineDispatchers.runCurrent() + val storyProvider = + topicController.getStory(profileId1, FRACTIONS_TOPIC_ID, FRACTIONS_STORY_ID_0) - verifyGetStorySucceeded() - val story = storySummaryResultCaptor.value!!.getOrThrow() + val story = monitorFactory.waitForNextSuccessfulResult(storyProvider) assertThat(story.storyId).isEqualTo(FRACTIONS_STORY_ID_0) } @Test fun testRetrieveStory_fractionsStory_returnsStoryWithName() { - topicController.getStory(profileId1, FRACTIONS_TOPIC_ID, FRACTIONS_STORY_ID_0).toLiveData() - .observeForever(mockStorySummaryObserver) - testCoroutineDispatchers.runCurrent() + val storyProvider = + topicController.getStory(profileId1, FRACTIONS_TOPIC_ID, FRACTIONS_STORY_ID_0) - verifyGetStorySucceeded() - val story = storySummaryResultCaptor.value!!.getOrThrow() + val story = monitorFactory.waitForNextSuccessfulResult(storyProvider) assertThat(story.storyName).isEqualTo("Matthew Goes to the Bakery") } @Test fun testRetrieveStory_ratiosFirstStory_returnsCorrectStory() { - topicController.getStory(profileId1, RATIOS_TOPIC_ID, RATIOS_STORY_ID_0).toLiveData() - .observeForever(mockStorySummaryObserver) - testCoroutineDispatchers.runCurrent() + val storyProvider = topicController.getStory(profileId1, RATIOS_TOPIC_ID, RATIOS_STORY_ID_0) - verifyGetStorySucceeded() - val story = storySummaryResultCaptor.value!!.getOrThrow() + val story = monitorFactory.waitForNextSuccessfulResult(storyProvider) assertThat(story.storyId).isEqualTo(RATIOS_STORY_ID_0) assertThat(story.storyName).isEqualTo("Ratios: Part 1") } @Test fun testRetrieveStory_ratiosFirstStory_returnsStoryWithMultipleChapters() { - topicController.getStory(profileId1, RATIOS_TOPIC_ID, RATIOS_STORY_ID_0).toLiveData() - .observeForever(mockStorySummaryObserver) - testCoroutineDispatchers.runCurrent() + val storyProvider = topicController.getStory(profileId1, RATIOS_TOPIC_ID, RATIOS_STORY_ID_0) - verifyGetStorySucceeded() - val story = storySummaryResultCaptor.value!!.getOrThrow() - assertThat(getExplorationIds(story)).containsExactly( - RATIOS_EXPLORATION_ID_0, - RATIOS_EXPLORATION_ID_1 - ).inOrder() + val expIds = getExplorationIds(monitorFactory.waitForNextSuccessfulResult(storyProvider)) + assertThat(expIds).containsExactly(RATIOS_EXPLORATION_ID_0, RATIOS_EXPLORATION_ID_1).inOrder() } @Test fun testRetrieveStory_ratiosSecondStory_returnsCorrectStory() { - topicController.getStory(profileId1, RATIOS_TOPIC_ID, RATIOS_STORY_ID_1).toLiveData() - .observeForever(mockStorySummaryObserver) - testCoroutineDispatchers.runCurrent() + val storyProvider = topicController.getStory(profileId1, RATIOS_TOPIC_ID, RATIOS_STORY_ID_1) - verifyGetStorySucceeded() - val story = storySummaryResultCaptor.value!!.getOrThrow() + val story = monitorFactory.waitForNextSuccessfulResult(storyProvider) assertThat(story.storyId).isEqualTo(RATIOS_STORY_ID_1) assertThat(story.storyName).isEqualTo("Ratios: Part 2") } @Test fun testRetrieveStory_ratiosSecondStory_returnsStoryWithMultipleChapters() { - topicController.getStory(profileId1, RATIOS_TOPIC_ID, RATIOS_STORY_ID_1).toLiveData() - .observeForever(mockStorySummaryObserver) - testCoroutineDispatchers.runCurrent() + val storyProvider = topicController.getStory(profileId1, RATIOS_TOPIC_ID, RATIOS_STORY_ID_1) - verifyGetStorySucceeded() - val story = storySummaryResultCaptor.value!!.getOrThrow() - assertThat(getExplorationIds(story)).containsExactly( - RATIOS_EXPLORATION_ID_2, - RATIOS_EXPLORATION_ID_3 - ).inOrder() + val expIds = getExplorationIds(monitorFactory.waitForNextSuccessfulResult(storyProvider)) + assertThat(expIds).containsExactly(RATIOS_EXPLORATION_ID_2, RATIOS_EXPLORATION_ID_3).inOrder() } @Test fun testRetrieveStory_validStory_returnsStoryWithChapter() { - topicController.getStory(profileId1, TEST_TOPIC_ID_1, TEST_STORY_ID_2).toLiveData() - .observeForever(mockStorySummaryObserver) - testCoroutineDispatchers.runCurrent() + val storyProvider = topicController.getStory(profileId1, TEST_TOPIC_ID_1, TEST_STORY_ID_2) - verifyGetStorySucceeded() - val story = storySummaryResultCaptor.value!!.getOrThrow() + val story = monitorFactory.waitForNextSuccessfulResult(storyProvider) assertThat(getExplorationIds(story)).containsExactly(TEST_EXPLORATION_ID_4) } @Test fun testRetrieveStory_validStory_returnsStoryWithChapterName() { - topicController.getStory(profileId1, TEST_TOPIC_ID_1, TEST_STORY_ID_2).toLiveData() - .observeForever(mockStorySummaryObserver) - testCoroutineDispatchers.runCurrent() + val storyProvider = topicController.getStory(profileId1, TEST_TOPIC_ID_1, TEST_STORY_ID_2) - verifyGetStorySucceeded() - val story = storySummaryResultCaptor.value!!.getOrThrow() + val story = monitorFactory.waitForNextSuccessfulResult(storyProvider) assertThat(story.getChapter(0).name).isEqualTo("Fifth Exploration") } @Test fun testRetrieveStory_validStory_returnsStoryWithChapterSummary() { - topicController.getStory(profileId1, FRACTIONS_TOPIC_ID, FRACTIONS_STORY_ID_0).toLiveData() - .observeForever(mockStorySummaryObserver) - testCoroutineDispatchers.runCurrent() + val storyProvider = + topicController.getStory(profileId1, FRACTIONS_TOPIC_ID, FRACTIONS_STORY_ID_0) - verifyGetStorySucceeded() - val story = storySummaryResultCaptor.value!!.getOrThrow() + val story = monitorFactory.waitForNextSuccessfulResult(storyProvider) assertThat(story.getChapter(0).summary) .isEqualTo("Matthew learns about fractions.") } @Test fun testRetrieveStory_validStory_returnsStoryWithChapterThumbnail() { - topicController.getStory(profileId1, TEST_TOPIC_ID_1, TEST_STORY_ID_2).toLiveData() - .observeForever(mockStorySummaryObserver) - testCoroutineDispatchers.runCurrent() + val storyProvider = topicController.getStory(profileId1, TEST_TOPIC_ID_1, TEST_STORY_ID_2) - verifyGetStorySucceeded() - val story = storySummaryResultCaptor.value!!.getOrThrow() + val story = monitorFactory.waitForNextSuccessfulResult(storyProvider) val chapter = story.getChapter(0) assertThat(chapter.chapterThumbnail.backgroundColorRgb).isNotEqualTo(0) } @Test fun testRetrieveStory_invalidStory_returnsFailure() { - topicController.getStory(profileId1, INVALID_TOPIC_ID_1, INVALID_STORY_ID_1).toLiveData() - .observeForever(mockStorySummaryObserver) - testCoroutineDispatchers.runCurrent() + val storyProvider = topicController.getStory(profileId1, INVALID_TOPIC_ID_1, INVALID_STORY_ID_1) - verifyGetStoryFailed() - assertThat(storySummaryResultCaptor.value!!.isFailure()).isTrue() + monitorFactory.waitForNextFailureResult(storyProvider) } @Test fun testRetrieveChapter_validChapter_returnsCorrectChapterSummary() { - topicController.retrieveChapter( - FRACTIONS_TOPIC_ID, FRACTIONS_STORY_ID_0, FRACTIONS_EXPLORATION_ID_0 - ).toLiveData().observeForever(mockChapterSummaryObserver) - testCoroutineDispatchers.runCurrent() + val chapterProvider = + topicController.retrieveChapter( + FRACTIONS_TOPIC_ID, FRACTIONS_STORY_ID_0, FRACTIONS_EXPLORATION_ID_0 + ) - verifyRetrieveChapterSucceeded() - val chapterSummary = chapterSummaryResultCaptor.value.getOrThrow() + val chapterSummary = monitorFactory.waitForNextSuccessfulResult(chapterProvider) assertThat(chapterSummary.name).isEqualTo("What is a Fraction?") - assertThat(chapterSummary.summary) - .isEqualTo("Matthew learns about fractions.") + assertThat(chapterSummary.summary).isEqualTo("Matthew learns about fractions.") } @Test fun testRetrieveChapter_invalidChapter_returnsFailure() { - topicController.retrieveChapter( - FRACTIONS_TOPIC_ID, FRACTIONS_STORY_ID_0, RATIOS_EXPLORATION_ID_0 - ).toLiveData().observeForever(mockChapterSummaryObserver) - testCoroutineDispatchers.runCurrent() - - verifyRetrieveChapterFailed() - assertThat(chapterSummaryResultCaptor.value.getErrorOrNull()).isInstanceOf( - TopicController.ChapterNotFoundException::class.java - ) + val chapterProvider = + topicController.retrieveChapter( + FRACTIONS_TOPIC_ID, FRACTIONS_STORY_ID_0, RATIOS_EXPLORATION_ID_0 + ) + + val error = monitorFactory.waitForNextFailureResult(chapterProvider) + assertThat(error).isInstanceOf(TopicController.ChapterNotFoundException::class.java) } @Test @@ -710,29 +553,21 @@ class TopicControllerTest { @Test fun testRetrieveSubtopicTopic_validSubtopic_returnsSubtopicWithThumbnail() { - topicController.getTopic( - profileId1, FRACTIONS_TOPIC_ID - ).toLiveData().observeForever(mockTopicObserver) - testCoroutineDispatchers.runCurrent() + val topicProvider = topicController.getTopic(profileId1, FRACTIONS_TOPIC_ID) - verifyGetTopicSucceeded() - val topic = topicResultCaptor.value!!.getOrThrow() + val topic = monitorFactory.waitForNextSuccessfulResult(topicProvider) assertThat(topic.subtopicList[0].subtopicThumbnail.backgroundColorRgb).isNotEqualTo(0) } @Test @Ignore("Questions are not fully supported via protos") // TODO(#2976): Re-enable. fun testRetrieveQuestionsForSkillIds_returnsAllQuestions() { - val questionsListProvider = topicController - .retrieveQuestionsForSkillIds( + val questionsListProvider = + topicController.retrieveQuestionsForSkillIds( listOf(TEST_SKILL_ID_0, TEST_SKILL_ID_1) ) - questionsListProvider.toLiveData().observeForever(mockQuestionListObserver) - testCoroutineDispatchers.runCurrent() - verify(mockQuestionListObserver).onChanged(questionListResultCaptor.capture()) - assertThat(questionListResultCaptor.value.isSuccess()).isTrue() - val questionsList = questionListResultCaptor.value.getOrThrow() + val questionsList = monitorFactory.waitForNextSuccessfulResult(questionsListProvider) assertThat(questionsList.size).isEqualTo(5) val questionIds = questionsList.map { it.questionId } assertThat(questionIds).containsExactlyElementsIn( @@ -746,17 +581,10 @@ class TopicControllerTest { @Test @Ignore("Questions are not fully supported via protos") // TODO(#2976): Re-enable. fun testRetrieveQuestionsForFractionsSkillId0_returnsAllQuestions() { - val questionsListProvider = topicController - .retrieveQuestionsForSkillIds( - listOf(FRACTIONS_SKILL_ID_0) - ) - questionsListProvider.toLiveData() - .observeForever(mockQuestionListObserver) - testCoroutineDispatchers.runCurrent() - verify(mockQuestionListObserver).onChanged(questionListResultCaptor.capture()) + val questionsListProvider = + topicController.retrieveQuestionsForSkillIds(listOf(FRACTIONS_SKILL_ID_0)) - assertThat(questionListResultCaptor.value.isSuccess()).isTrue() - val questionsList = questionListResultCaptor.value.getOrThrow() + val questionsList = monitorFactory.waitForNextSuccessfulResult(questionsListProvider) assertThat(questionsList.size).isEqualTo(4) val questionIds = questionsList.map { it.questionId } assertThat(questionIds).containsExactlyElementsIn( @@ -770,17 +598,10 @@ class TopicControllerTest { @Test @Ignore("Questions are not fully supported via protos") // TODO(#2976): Re-enable. fun testRetrieveQuestionsForFractionsSkillId1_returnsAllQuestions() { - val questionsListProvider = topicController - .retrieveQuestionsForSkillIds( - listOf(FRACTIONS_SKILL_ID_1) - ) - questionsListProvider.toLiveData() - .observeForever(mockQuestionListObserver) - testCoroutineDispatchers.runCurrent() - verify(mockQuestionListObserver).onChanged(questionListResultCaptor.capture()) + val questionsListProvider = + topicController.retrieveQuestionsForSkillIds(listOf(FRACTIONS_SKILL_ID_1)) - assertThat(questionListResultCaptor.value.isSuccess()).isTrue() - val questionsList = questionListResultCaptor.value.getOrThrow() + val questionsList = monitorFactory.waitForNextSuccessfulResult(questionsListProvider) assertThat(questionsList.size).isEqualTo(3) val questionIds = questionsList.map { it.questionId } assertThat(questionIds).containsExactlyElementsIn( @@ -793,17 +614,10 @@ class TopicControllerTest { @Test @Ignore("Questions are not fully supported via protos") // TODO(#2976): Re-enable. fun testRetrieveQuestionsForFractionsSkillId2_returnsAllQuestions() { - val questionsListProvider = topicController - .retrieveQuestionsForSkillIds( - listOf(FRACTIONS_SKILL_ID_2) - ) - questionsListProvider.toLiveData() - .observeForever(mockQuestionListObserver) - testCoroutineDispatchers.runCurrent() - verify(mockQuestionListObserver).onChanged(questionListResultCaptor.capture()) + val questionsListProvider = + topicController.retrieveQuestionsForSkillIds(listOf(FRACTIONS_SKILL_ID_2)) - assertThat(questionListResultCaptor.value.isSuccess()).isTrue() - val questionsList = questionListResultCaptor.value.getOrThrow() + val questionsList = monitorFactory.waitForNextSuccessfulResult(questionsListProvider) assertThat(questionsList.size).isEqualTo(4) val questionIds = questionsList.map { it.questionId } assertThat(questionIds).containsExactlyElementsIn( @@ -817,17 +631,10 @@ class TopicControllerTest { @Test @Ignore("Questions are not fully supported via protos") // TODO(#2976): Re-enable. fun testRetrieveQuestionsForRatiosSkillId0_returnsAllQuestions() { - val questionsListProvider = topicController - .retrieveQuestionsForSkillIds( - listOf(RATIOS_SKILL_ID_0) - ) - questionsListProvider.toLiveData() - .observeForever(mockQuestionListObserver) - testCoroutineDispatchers.runCurrent() - verify(mockQuestionListObserver).onChanged(questionListResultCaptor.capture()) + val questionsListProvider = + topicController.retrieveQuestionsForSkillIds(listOf(RATIOS_SKILL_ID_0)) - assertThat(questionListResultCaptor.value.isSuccess()).isTrue() - val questionsList = questionListResultCaptor.value.getOrThrow() + val questionsList = monitorFactory.waitForNextSuccessfulResult(questionsListProvider) assertThat(questionsList.size).isEqualTo(1) val questionIds = questionsList.map { it.questionId } assertThat(questionIds).containsExactlyElementsIn( @@ -840,39 +647,27 @@ class TopicControllerTest { @Test @Ignore("Questions are not fully supported via protos") // TODO(#2976): Re-enable. fun testRetrieveQuestionsForInvalidSkillIds_returnsResultForValidSkillsOnly() { - val questionsListProvider = topicController - .retrieveQuestionsForSkillIds( + val questionsListProvider = + topicController.retrieveQuestionsForSkillIds( listOf(TEST_SKILL_ID_0, TEST_SKILL_ID_1, "NON_EXISTENT_SKILL_ID") ) - questionsListProvider.toLiveData() - .observeForever(mockQuestionListObserver) - testCoroutineDispatchers.runCurrent() - verify(mockQuestionListObserver).onChanged(questionListResultCaptor.capture()) - assertThat(questionListResultCaptor.value.isSuccess()).isTrue() - val questionsList = questionListResultCaptor.value.getOrThrow() + val questionsList = monitorFactory.waitForNextSuccessfulResult(questionsListProvider) assertThat(questionsList.size).isEqualTo(5) } @Test fun testGetTopic_invalidTopicId_getTopic_noResultFound() { - topicController.getTopic( - profileId1, INVALID_TOPIC_ID_1 - ).toLiveData().observeForever(mockTopicObserver) - testCoroutineDispatchers.runCurrent() + val topicProvider = topicController.getTopic(profileId1, INVALID_TOPIC_ID_1) - verifyGetTopicFailed() + monitorFactory.waitForNextFailureResult(topicProvider) } @Test fun testGetTopic_validTopicId_withoutAnyProgress_getTopicSucceedsWithCorrectProgress() { - topicController.getTopic( - profileId1, FRACTIONS_TOPIC_ID - ).toLiveData().observeForever(mockTopicObserver) - testCoroutineDispatchers.runCurrent() + val topicProvider = topicController.getTopic(profileId1, FRACTIONS_TOPIC_ID) - verifyGetTopicSucceeded() - val topic = topicResultCaptor.value.getOrThrow() + val topic = monitorFactory.waitForNextSuccessfulResult(topicProvider) assertThat(topic.topicId).isEqualTo(FRACTIONS_TOPIC_ID) assertThat(topic.storyList[0].chapterList[0].chapterPlayState) .isEqualTo(ChapterPlayState.NOT_STARTED) @@ -886,13 +681,9 @@ class TopicControllerTest { fun testGetTopic_recordProgress_getTopic_correctProgressFound() { markFractionsStory0Chapter0AsCompleted() - topicController.getTopic( - profileId1, FRACTIONS_TOPIC_ID - ).toLiveData().observeForever(mockTopicObserver) - testCoroutineDispatchers.runCurrent() + val topicProvider = topicController.getTopic(profileId1, FRACTIONS_TOPIC_ID) - verifyGetTopicSucceeded() - val topic = topicResultCaptor.value.getOrThrow() + val topic = monitorFactory.waitForNextSuccessfulResult(topicProvider) assertThat(topic.topicId).isEqualTo(FRACTIONS_TOPIC_ID) assertThat(topic.storyList[0].chapterList[0].chapterPlayState) .isEqualTo(ChapterPlayState.COMPLETED) @@ -902,21 +693,17 @@ class TopicControllerTest { @Test fun testGetStory_invalidData_getStory_noResultFound() { - topicController.getStory(profileId1, INVALID_TOPIC_ID_1, INVALID_STORY_ID_1).toLiveData() - .observeForever(mockStorySummaryObserver) - testCoroutineDispatchers.runCurrent() + val storyProvider = topicController.getStory(profileId1, INVALID_TOPIC_ID_1, INVALID_STORY_ID_1) - verifyGetStoryFailed() + monitorFactory.waitForNextFailureResult(storyProvider) } @Test fun testGetStory_validData_withoutAnyProgress_getStorySucceedsWithCorrectProgress() { - topicController.getStory(profileId1, FRACTIONS_TOPIC_ID, FRACTIONS_STORY_ID_0).toLiveData() - .observeForever(mockStorySummaryObserver) - testCoroutineDispatchers.runCurrent() + val storyProvider = + topicController.getStory(profileId1, FRACTIONS_TOPIC_ID, FRACTIONS_STORY_ID_0) - verifyGetStorySucceeded() - val storySummary = storySummaryResultCaptor.value.getOrThrow() + val storySummary = monitorFactory.waitForNextSuccessfulResult(storyProvider) assertThat(storySummary.storyId).isEqualTo(FRACTIONS_STORY_ID_0) assertThat(storySummary.chapterList[0].chapterPlayState) .isEqualTo(ChapterPlayState.NOT_STARTED) @@ -930,13 +717,9 @@ class TopicControllerTest { fun testGetStory_recordProgress_getTopic_correctProgressFound() { markFractionsStory0Chapter0AsCompleted() - topicController.getTopic( - profileId1, FRACTIONS_TOPIC_ID - ).toLiveData().observeForever(mockTopicObserver) - testCoroutineDispatchers.runCurrent() + val topicProvider = topicController.getTopic(profileId1, FRACTIONS_TOPIC_ID) - verifyGetTopicSucceeded() - val topic = topicResultCaptor.value.getOrThrow() + val topic = monitorFactory.waitForNextSuccessfulResult(topicProvider) assertThat(topic.topicId).isEqualTo(FRACTIONS_TOPIC_ID) assertThat(topic.storyList[0].chapterList[0].chapterPlayState) .isEqualTo(ChapterPlayState.COMPLETED) @@ -946,13 +729,9 @@ class TopicControllerTest { @Test fun testOngoingTopicList_validData_withoutAnyProgress_ongoingTopicListIsEmpty() { - topicController.getOngoingTopicList( - profileId1 - ).toLiveData().observeForever(mockOngoingTopicListObserver) - testCoroutineDispatchers.runCurrent() + val topicListProvider = topicController.getOngoingTopicList(profileId1) - verifyGetOngoingTopicListSucceeded() - val ongoingTopicList = ongoingTopicListResultCaptor.value.getOrThrow() + val ongoingTopicList = monitorFactory.waitForNextSuccessfulResult(topicListProvider) assertThat(ongoingTopicList.topicCount).isEqualTo(0) } @@ -960,13 +739,9 @@ class TopicControllerTest { fun testOngoingTopicList_recordOneChapterCompleted_correctOngoingList() { markFractionsStory0Chapter0AsCompleted() - topicController.getOngoingTopicList( - profileId1 - ).toLiveData().observeForever(mockOngoingTopicListObserver) - testCoroutineDispatchers.runCurrent() + val topicListProvider = topicController.getOngoingTopicList(profileId1) - verifyGetOngoingTopicListSucceeded() - val ongoingTopicList = ongoingTopicListResultCaptor.value.getOrThrow() + val ongoingTopicList = monitorFactory.waitForNextSuccessfulResult(topicListProvider) assertThat(ongoingTopicList.topicCount).isEqualTo(1) assertThat(ongoingTopicList.topicList[0].topicId).isEqualTo(FRACTIONS_TOPIC_ID) } @@ -976,13 +751,9 @@ class TopicControllerTest { markFractionsStory0Chapter0AsCompleted() markFractionsStory0Chapter1AsCompleted() - topicController.getOngoingTopicList( - profileId1 - ).toLiveData().observeForever(mockOngoingTopicListObserver) - testCoroutineDispatchers.runCurrent() + val topicListProvider = topicController.getOngoingTopicList(profileId1) - verifyGetOngoingTopicListSucceeded() - val ongoingTopicList = ongoingTopicListResultCaptor.value.getOrThrow() + val ongoingTopicList = monitorFactory.waitForNextSuccessfulResult(topicListProvider) assertThat(ongoingTopicList.topicCount).isEqualTo(0) } @@ -993,13 +764,9 @@ class TopicControllerTest { markFractionsStory0Chapter1AsCompleted() markRatiosStory0Chapter0AsCompleted() - topicController.getOngoingTopicList( - profileId1 - ).toLiveData().observeForever(mockOngoingTopicListObserver) - testCoroutineDispatchers.runCurrent() + val topicListProvider = topicController.getOngoingTopicList(profileId1) - verifyGetOngoingTopicListSucceeded() - val ongoingTopicList = ongoingTopicListResultCaptor.value.getOrThrow() + val ongoingTopicList = monitorFactory.waitForNextSuccessfulResult(topicListProvider) assertThat(ongoingTopicList.topicCount).isEqualTo(1) assertThat(ongoingTopicList.topicList[0].topicId).isEqualTo(RATIOS_TOPIC_ID) } @@ -1009,13 +776,9 @@ class TopicControllerTest { markRatiosStory0Chapter0AsCompleted() markRatiosStory0Chapter1AsCompleted() - topicController.getOngoingTopicList( - profileId1 - ).toLiveData().observeForever(mockOngoingTopicListObserver) - testCoroutineDispatchers.runCurrent() + val topicListProvider = topicController.getOngoingTopicList(profileId1) - verifyGetOngoingTopicListSucceeded() - val ongoingTopicList = ongoingTopicListResultCaptor.value.getOrThrow() + val ongoingTopicList = monitorFactory.waitForNextSuccessfulResult(topicListProvider) assertThat(ongoingTopicList.topicCount).isEqualTo(0) } @@ -1025,25 +788,18 @@ class TopicControllerTest { markRatiosStory0Chapter1AsCompleted() markRatiosStory1Chapter0AsCompleted() - topicController.getOngoingTopicList( - profileId1 - ).toLiveData().observeForever(mockOngoingTopicListObserver) - testCoroutineDispatchers.runCurrent() + val topicListProvider = topicController.getOngoingTopicList(profileId1) - verifyGetOngoingTopicListSucceeded() - val ongoingTopicList = ongoingTopicListResultCaptor.value.getOrThrow() + val ongoingTopicList = monitorFactory.waitForNextSuccessfulResult(topicListProvider) assertThat(ongoingTopicList.topicCount).isEqualTo(1) assertThat(ongoingTopicList.topicList[0].topicId).isEqualTo(RATIOS_TOPIC_ID) } @Test fun testCompletedStoryList_validData_withoutAnyProgress_completedStoryListIsEmpty() { - topicController.getCompletedStoryList(profileId1).toLiveData() - .observeForever(mockCompletedStoryListObserver) - testCoroutineDispatchers.runCurrent() + val storyList = topicController.getCompletedStoryList(profileId1) - verifyGetCompletedStoryListSucceeded() - val completedStoryList = completedStoryListResultCaptor.value.getOrThrow() + val completedStoryList = monitorFactory.waitForNextSuccessfulResult(storyList) assertThat(completedStoryList.completedStoryCount).isEqualTo(0) } @@ -1051,12 +807,9 @@ class TopicControllerTest { fun testCompletedStoryList_recordOneChapterProgress_completedStoryListIsEmpty() { markFractionsStory0Chapter0AsCompleted() - topicController.getCompletedStoryList(profileId1).toLiveData() - .observeForever(mockCompletedStoryListObserver) - testCoroutineDispatchers.runCurrent() + val storyList = topicController.getCompletedStoryList(profileId1) - verifyGetCompletedStoryListSucceeded() - val completedStoryList = completedStoryListResultCaptor.value.getOrThrow() + val completedStoryList = monitorFactory.waitForNextSuccessfulResult(storyList) assertThat(completedStoryList.completedStoryCount).isEqualTo(0) } @@ -1065,12 +818,9 @@ class TopicControllerTest { markFractionsStory0Chapter0AsCompleted() markFractionsStory0Chapter1AsCompleted() - topicController.getCompletedStoryList(profileId1).toLiveData() - .observeForever(mockCompletedStoryListObserver) - testCoroutineDispatchers.runCurrent() + val storyList = topicController.getCompletedStoryList(profileId1) - verifyGetCompletedStoryListSucceeded() - val completedStoryList = completedStoryListResultCaptor.value.getOrThrow() + val completedStoryList = monitorFactory.waitForNextSuccessfulResult(storyList) assertThat(completedStoryList.completedStoryCount).isEqualTo(1) assertThat(completedStoryList.completedStoryList[0].storyId).isEqualTo(FRACTIONS_STORY_ID_0) assertThat(completedStoryList.completedStoryList[0].topicId).isEqualTo(FRACTIONS_TOPIC_ID) @@ -1081,12 +831,10 @@ class TopicControllerTest { markFractionsStory0Chapter0AsCompleted() markFractionsStory0Chapter1AsCompleted() - topicController.getStory(profileId1, FRACTIONS_TOPIC_ID, FRACTIONS_STORY_ID_0).toLiveData() - .observeForever(mockStorySummaryObserver) - testCoroutineDispatchers.runCurrent() + val storyProvider = + topicController.getStory(profileId1, FRACTIONS_TOPIC_ID, FRACTIONS_STORY_ID_0) - verifyGetStorySucceeded() - val storySummary = storySummaryResultCaptor.value.getOrThrow() + val storySummary = monitorFactory.waitForNextSuccessfulResult(storyProvider) assertThat(storySummary.chapterCount).isEqualTo(2) assertThat(storySummary.chapterList[0].chapterPlayState).isEqualTo(ChapterPlayState.COMPLETED) assertThat(storySummary.chapterList[1].chapterPlayState).isEqualTo(ChapterPlayState.COMPLETED) @@ -1098,12 +846,9 @@ class TopicControllerTest { markRatiosStory0Chapter0AsCompleted() markRatiosStory0Chapter1AsCompleted() - topicController.getCompletedStoryList(profileId1).toLiveData() - .observeForever(mockCompletedStoryListObserver) - testCoroutineDispatchers.runCurrent() + val storyList = topicController.getCompletedStoryList(profileId1) - verifyGetCompletedStoryListSucceeded() - val completedStoryList = completedStoryListResultCaptor.value.getOrThrow() + val completedStoryList = monitorFactory.waitForNextSuccessfulResult(storyList) assertThat(completedStoryList.completedStoryCount).isEqualTo(1) assertThat(completedStoryList.completedStoryList[0].storyId).isEqualTo(RATIOS_STORY_ID_0) assertThat(completedStoryList.completedStoryList[0].topicId).isEqualTo(RATIOS_TOPIC_ID) @@ -1116,12 +861,9 @@ class TopicControllerTest { markRatiosStory0Chapter0AsCompleted() markRatiosStory0Chapter1AsCompleted() - topicController.getCompletedStoryList(profileId1).toLiveData() - .observeForever(mockCompletedStoryListObserver) - testCoroutineDispatchers.runCurrent() + val storyList = topicController.getCompletedStoryList(profileId1) - verifyGetCompletedStoryListSucceeded() - val completedStoryList = completedStoryListResultCaptor.value.getOrThrow() + val completedStoryList = monitorFactory.waitForNextSuccessfulResult(storyList) assertThat(completedStoryList.completedStoryCount).isEqualTo(2) assertThat(completedStoryList.completedStoryList[0].storyId).isEqualTo(FRACTIONS_STORY_ID_0) assertThat(completedStoryList.completedStoryList[1].storyId).isEqualTo(RATIOS_STORY_ID_0) @@ -1339,54 +1081,6 @@ class TopicControllerTest { monitorFactory.waitForNextSuccessfulResult(updateProvider) } - private fun verifyGetTopicSucceeded() { - verify(mockTopicObserver, atLeastOnce()).onChanged(topicResultCaptor.capture()) - assertThat(topicResultCaptor.value.isSuccess()).isTrue() - } - - private fun verifyGetTopicFailed() { - verify(mockTopicObserver, atLeastOnce()).onChanged(topicResultCaptor.capture()) - assertThat(topicResultCaptor.value.isFailure()).isTrue() - } - - private fun verifyGetStorySucceeded() { - verify(mockStorySummaryObserver, atLeastOnce()).onChanged(storySummaryResultCaptor.capture()) - assertThat(storySummaryResultCaptor.value.isSuccess()).isTrue() - } - - private fun verifyGetStoryFailed() { - verify(mockStorySummaryObserver, atLeastOnce()).onChanged(storySummaryResultCaptor.capture()) - assertThat(storySummaryResultCaptor.value.isFailure()).isTrue() - } - - private fun verifyRetrieveChapterSucceeded() { - verify(mockChapterSummaryObserver, atLeastOnce()) - .onChanged(chapterSummaryResultCaptor.capture()) - assertThat(chapterSummaryResultCaptor.value.isSuccess()).isTrue() - } - - private fun verifyRetrieveChapterFailed() { - verify(mockChapterSummaryObserver, atLeastOnce()) - .onChanged(chapterSummaryResultCaptor.capture()) - assertThat(chapterSummaryResultCaptor.value.isFailure()).isTrue() - } - - private fun verifyGetOngoingTopicListSucceeded() { - verify( - mockOngoingTopicListObserver, - atLeastOnce() - ).onChanged(ongoingTopicListResultCaptor.capture()) - assertThat(ongoingTopicListResultCaptor.value.isSuccess()).isTrue() - } - - private fun verifyGetCompletedStoryListSucceeded() { - verify( - mockCompletedStoryListObserver, - atLeastOnce() - ).onChanged(completedStoryListResultCaptor.capture()) - assertThat(completedStoryListResultCaptor.value.isSuccess()).isTrue() - } - private fun getExplorationIds(story: StorySummary): List { return story.chapterList.map(ChapterSummary::getExplorationId) } diff --git a/domain/src/test/java/org/oppia/android/domain/topic/TopicListControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/topic/TopicListControllerTest.kt index 6e8ec92869b..f21ddadfc20 100644 --- a/domain/src/test/java/org/oppia/android/domain/topic/TopicListControllerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/topic/TopicListControllerTest.kt @@ -2,7 +2,6 @@ package org.oppia.android.domain.topic import android.app.Application import android.content.Context -import androidx.lifecycle.Observer import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat @@ -10,25 +9,18 @@ import dagger.BindsInstance import dagger.Component import dagger.Module import dagger.Provides +import javax.inject.Inject +import javax.inject.Singleton import org.junit.Before -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentCaptor -import org.mockito.Captor -import org.mockito.Mock -import org.mockito.Mockito.atLeastOnce -import org.mockito.Mockito.verify -import org.mockito.junit.MockitoJUnit -import org.mockito.junit.MockitoRule import org.oppia.android.app.model.ProfileId -import org.oppia.android.app.model.PromotedActivityList import org.oppia.android.app.model.PromotedStory -import org.oppia.android.app.model.TopicList import org.oppia.android.app.model.TopicSummary import org.oppia.android.app.model.UpcomingTopic import org.oppia.android.domain.oppialogger.LogStorageModule import org.oppia.android.testing.TestLogReportingModule +import org.oppia.android.testing.data.DataProviderTestMonitor import org.oppia.android.testing.environment.TestEnvironmentConfig import org.oppia.android.testing.robolectric.RobolectricModule import org.oppia.android.testing.story.StoryProgressTestHelper @@ -40,8 +32,6 @@ import org.oppia.android.util.caching.AssetModule import org.oppia.android.util.caching.CacheAssetsLocally import org.oppia.android.util.caching.LoadLessonProtosFromAssets import org.oppia.android.util.caching.TopicListToCache -import org.oppia.android.util.data.AsyncResult -import org.oppia.android.util.data.DataProviders.Companion.toLiveData import org.oppia.android.util.data.DataProvidersInjector import org.oppia.android.util.data.DataProvidersInjectorProvider import org.oppia.android.util.gcsresource.DefaultResourceBucketName @@ -55,46 +45,20 @@ import org.oppia.android.util.parser.image.DefaultGcsPrefix import org.oppia.android.util.parser.image.ImageDownloadUrlTemplate import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode -import javax.inject.Inject -import javax.inject.Singleton /** Tests for [TopicListController]. */ +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) @Config(application = TopicListControllerTest.TestApplication::class) class TopicListControllerTest { - - @Rule - @JvmField - val mockitoRule: MockitoRule = MockitoJUnit.rule() - - @Inject - lateinit var context: Context - - @Inject - lateinit var topicListController: TopicListController - - @Inject - lateinit var storyProgressTestHelper: StoryProgressTestHelper - - @Inject - lateinit var testCoroutineDispatchers: TestCoroutineDispatchers - - @Inject - lateinit var fakeOppiaClock: FakeOppiaClock - - @Mock - lateinit var mockTopicListObserver: Observer> - - @Mock - lateinit var mockPromotedActivityListObserver: Observer> - - @Captor - lateinit var topicListResultCaptor: ArgumentCaptor> - - @Captor - lateinit var promotedActivityListResultCaptor: - ArgumentCaptor> + @Inject lateinit var context: Context + @Inject lateinit var topicListController: TopicListController + @Inject lateinit var storyProgressTestHelper: StoryProgressTestHelper + @Inject lateinit var testCoroutineDispatchers: TestCoroutineDispatchers + @Inject lateinit var fakeOppiaClock: FakeOppiaClock + @Inject lateinit var monitorFactory: DataProviderTestMonitor.Factory private lateinit var profileId0: ProfileId @@ -108,31 +72,24 @@ class TopicListControllerTest { fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS) } - private fun setUpTestApplicationComponent() { - ApplicationProvider.getApplicationContext().inject(this) - } - @Test fun testRetrieveTopicList_isSuccessful() { - val topicListLiveData = topicListController.getTopicList().toLiveData() + val topicListProvider = topicListController.getTopicList() - topicListLiveData.observeForever(mockTopicListObserver) - testCoroutineDispatchers.runCurrent() - - verify(mockTopicListObserver).onChanged(topicListResultCaptor.capture()) - val topicListResult = topicListResultCaptor.value - assertThat(topicListResult!!.isSuccess()).isTrue() + monitorFactory.waitForNextSuccessfulResult(topicListProvider) } @Test fun testRetrieveTopicList_providesListOfMultipleTopics() { val topicList = retrieveTopicList() + assertThat(topicList.topicSummaryCount).isGreaterThan(1) } @Test fun testRetrieveTopicList_firstTopic_hasCorrectTopicInfo() { val topicList = retrieveTopicList() + val firstTopic = topicList.getTopicSummary(0) assertThat(firstTopic.topicId).isEqualTo(TEST_TOPIC_ID_0) assertThat(firstTopic.name).isEqualTo("First Test Topic") @@ -141,6 +98,7 @@ class TopicListControllerTest { @Test fun testRetrieveTopicList_firstTopic_hasCorrectLessonCount() { val topicList = retrieveTopicList() + val firstTopic = topicList.getTopicSummary(0) assertThat(firstTopic.totalChapterCount).isEqualTo(2) } @@ -148,6 +106,7 @@ class TopicListControllerTest { @Test fun testRetrieveTopicList_secondTopic_hasCorrectTopicInfo() { val topicList = retrieveTopicList() + val secondTopic = topicList.getTopicSummary(1) assertThat(secondTopic.topicId).isEqualTo(TEST_TOPIC_ID_1) assertThat(secondTopic.name).isEqualTo("Second Test Topic") @@ -156,6 +115,7 @@ class TopicListControllerTest { @Test fun testRetrieveTopicList_secondTopic_hasCorrectLessonCount() { val topicList = retrieveTopicList() + val secondTopic = topicList.getTopicSummary(1) assertThat(secondTopic.totalChapterCount).isEqualTo(1) } @@ -163,6 +123,7 @@ class TopicListControllerTest { @Test fun testRetrieveTopicList_fractionsTopic_hasCorrectTopicInfo() { val topicList = retrieveTopicList() + val fractionsTopic = topicList.getTopicSummary(2) assertThat(fractionsTopic.topicId).isEqualTo(FRACTIONS_TOPIC_ID) assertThat(fractionsTopic.name).isEqualTo("Fractions") @@ -171,6 +132,7 @@ class TopicListControllerTest { @Test fun testRetrieveTopicList_fractionsTopic_hasCorrectLessonCount() { val topicList = retrieveTopicList() + val fractionsTopic = topicList.getTopicSummary(2) assertThat(fractionsTopic.totalChapterCount).isEqualTo(2) } @@ -178,6 +140,7 @@ class TopicListControllerTest { @Test fun testRetrieveTopicList_ratiosTopic_hasCorrectTopicInfo() { val topicList = retrieveTopicList() + val ratiosTopic = topicList.getTopicSummary(3) assertThat(ratiosTopic.topicId).isEqualTo(RATIOS_TOPIC_ID) assertThat(ratiosTopic.name).isEqualTo("Ratios and Proportional Reasoning") @@ -186,32 +149,26 @@ class TopicListControllerTest { @Test fun testRetrieveTopicList_ratiosTopic_hasCorrectLessonCount() { val topicList = retrieveTopicList() + val ratiosTopic = topicList.getTopicSummary(3) assertThat(ratiosTopic.totalChapterCount).isEqualTo(4) } @Test fun testRetrieveTopicList_doesNotContainUnavailableTopic() { - val topicListLiveData = topicListController.getTopicList().toLiveData() - - topicListLiveData.observeForever(mockTopicListObserver) - testCoroutineDispatchers.runCurrent() + val topicList = retrieveTopicList() // Verify that the topic list does not contain a not-yet published topic (since it can't be // played by the user). - verify(mockTopicListObserver).onChanged(topicListResultCaptor.capture()) - val topicList = topicListResultCaptor.value.getOrThrow() val topicIds = topicList.topicSummaryList.map(TopicSummary::getTopicId) assertThat(topicIds).doesNotContain(TEST_TOPIC_ID_2) } @Test fun testRetrievePromotedActivityList_defaultLesson_hasCorrectInfo() { - topicListController.getPromotedActivityList(profileId0).toLiveData() - .observeForever(mockPromotedActivityListObserver) - testCoroutineDispatchers.runCurrent() + val promotedActivityProvider = topicListController.getPromotedActivityList(profileId0) - verifyGetPromotedActivityListSucceeded() + monitorFactory.waitForNextSuccessfulResult(promotedActivityProvider) } @Test @@ -659,14 +616,6 @@ class TopicListControllerTest { ) } - private fun verifyGetPromotedActivityListSucceeded() { - verify( - mockPromotedActivityListObserver, - atLeastOnce() - ).onChanged(promotedActivityListResultCaptor.capture()) - assertThat(promotedActivityListResultCaptor.value.isSuccess()).isTrue() - } - private fun verifyPromotedStoryAsFirstTestTopicStory0Exploration0(promotedStory: PromotedStory) { assertThat(promotedStory.explorationId).isEqualTo(TEST_EXPLORATION_ID_2) assertThat(promotedStory.storyId).isEqualTo(TEST_STORY_ID_0) @@ -795,21 +744,14 @@ class TopicListControllerTest { assertThat(promotedStory.totalChapterCount).isEqualTo(2) } - private fun retrieveTopicList(): TopicList { - val topicListLiveData = topicListController.getTopicList().toLiveData() - topicListLiveData.observeForever(mockTopicListObserver) - testCoroutineDispatchers.runCurrent() - verify(mockTopicListObserver).onChanged(topicListResultCaptor.capture()) - return topicListResultCaptor.value.getOrThrow() - } + private fun retrieveTopicList() = + monitorFactory.waitForNextSuccessfulResult(topicListController.getTopicList()) + + private fun retrievePromotedActivityList() = + monitorFactory.waitForNextSuccessfulResult(topicListController.getPromotedActivityList(profileId0)) - private fun retrievePromotedActivityList(): PromotedActivityList { - testCoroutineDispatchers.runCurrent() - topicListController.getPromotedActivityList(profileId0).toLiveData() - .observeForever(mockPromotedActivityListObserver) - testCoroutineDispatchers.runCurrent() - verifyGetPromotedActivityListSucceeded() - return promotedActivityListResultCaptor.value.getOrThrow() + private fun setUpTestApplicationComponent() { + ApplicationProvider.getApplicationContext().inject(this) } // TODO(#89): Move this to a common test application component. diff --git a/testing/BUILD.bazel b/testing/BUILD.bazel index f06821e6197..dd6b3ea9991 100644 --- a/testing/BUILD.bazel +++ b/testing/BUILD.bazel @@ -36,6 +36,7 @@ kt_android_library( "//domain", "//domain/src/main/java/org/oppia/android/domain/classify:interactions_module", "//domain/src/main/java/org/oppia/android/domain/profile:profile_management_controller", + "//testing/src/main/java/org/oppia/android/testing/data:async_result_subject", "//testing/src/main/java/org/oppia/android/testing/threading:test_coroutine_dispatchers", "//testing/src/main/java/org/oppia/android/testing/time:fake_oppia_clock", "//third_party:androidx_core_core-ktx", @@ -75,6 +76,8 @@ TEST_DEPS = [ "//domain", "//domain/src/main/java/org/oppia/android/domain/onboarding/testing:retriever_test_module", "//domain/src/main/java/org/oppia/android/domain/profile:profile_management_controller", + "//testing/src/main/java/org/oppia/android/testing/data:async_result_subject", + "//testing/src/main/java/org/oppia/android/testing/data:data_provider_test_monitor", "//testing/src/main/java/org/oppia/android/testing/espresso:text_input_action", "//testing/src/main/java/org/oppia/android/testing/network", "//testing/src/main/java/org/oppia/android/testing/network:test_module", diff --git a/testing/src/main/java/org/oppia/android/testing/data/AsyncResultSubject.kt b/testing/src/main/java/org/oppia/android/testing/data/AsyncResultSubject.kt new file mode 100644 index 00000000000..0fd9e2e98d4 --- /dev/null +++ b/testing/src/main/java/org/oppia/android/testing/data/AsyncResultSubject.kt @@ -0,0 +1,104 @@ +package org.oppia.android.testing.data + +import com.google.common.truth.BooleanSubject +import com.google.common.truth.ComparableSubject +import com.google.common.truth.DoubleSubject +import com.google.common.truth.FailureMetadata +import com.google.common.truth.FloatSubject +import com.google.common.truth.IntegerSubject +import com.google.common.truth.IterableSubject +import com.google.common.truth.LongSubject +import com.google.common.truth.MapSubject +import com.google.common.truth.StringSubject +import com.google.common.truth.Subject +import com.google.common.truth.Subject.Factory +import com.google.common.truth.ThrowableSubject +import com.google.common.truth.Truth +import com.google.common.truth.Truth.assertThat +import com.google.common.truth.extensions.proto.LiteProtoSubject +import com.google.common.truth.extensions.proto.LiteProtoTruth.assertThat +import com.google.protobuf.MessageLite +import org.oppia.android.util.data.AsyncResult + +// TODO: file issue and add TODO to add tests. + +class AsyncResultSubject( + failureMetadata: FailureMetadata?, + @PublishedApi internal val actual: AsyncResult +) : Subject(failureMetadata, actual) { + fun isPending() { + ensureActualIsType>() + } + + fun isSuccess() { + ensureActualIsType>() + } + + fun isFailure() { + ensureActualIsType>() + } + + fun hasSuccessValueWhere(block: T.() -> Unit) = + ensureActualIsType>().value.block() + + fun isSuccessThat(): Subject = assertThat(ensureActualIsType>().value) + + /* NOTE TO DEVELOPERS: Add more subject types below, as needed. */ + + inline fun > isComparableSuccessThat(): ComparableSubject = + assertThat(extractSuccessValue()) + + fun isStringSuccessThat(): StringSubject = assertThat(extractSuccessValue()) + + fun isBooleanSuccessThat(): BooleanSubject = assertThat(extractSuccessValue()) + + fun isIntSuccessThat(): IntegerSubject = assertThat(extractSuccessValue()) + + fun isLongSuccessThat(): LongSubject = assertThat(extractSuccessValue()) + + fun isFloatSuccessThat(): FloatSubject = assertThat(extractSuccessValue()) + + fun isDoubleSuccessThat(): DoubleSubject = assertThat(extractSuccessValue()) + + fun isProtoSuccessThat(): LiteProtoSubject = assertThat(extractSuccessValue()) + + fun isIterableSuccessThat(): IterableSubject = assertThat(extractSuccessValue>()) + + fun asMapSuccessThat(): MapSubject = assertThat(extractSuccessValue>()) + + fun asThrowableSuccessThat(): ThrowableSubject = assertThat(extractSuccessValue()) + + fun isFailureThat(): ThrowableSubject = + assertThat(ensureActualIsType>().error) + + fun isNewerOrSameAgeAs(other: AsyncResult) { + assertThat(actual.isNewerThanOrSameAgeAs(other)).isTrue() + } + + fun isOlderThan(other: AsyncResult) { + assertThat(actual.isNewerThanOrSameAgeAs(other)).isFalse() + } + + @PublishedApi // See: https://stackoverflow.com/a/41905907/3689782. + internal inline fun extractSuccessValue(): T { + return ensureActualIsType>().value.also { + assertThat(it).isInstanceOf(T::class.java) + } + } + + @PublishedApi + internal inline fun ensureActualIsType(): T { + assertThat(actual).isInstanceOf(T::class.java) + // This extra check is just to ensure Kotlin knows 'actual' is of type 'T'. + check(actual is T) { "Error: Truth didn't correctly catch mis-typing." } + return actual + } + + companion object { + fun assertThat(actual: AsyncResult): AsyncResultSubject { + return Truth.assertAbout( + Factory, AsyncResult>(::AsyncResultSubject) + ).that(actual) + } + } +} diff --git a/testing/src/main/java/org/oppia/android/testing/data/BUILD.bazel b/testing/src/main/java/org/oppia/android/testing/data/BUILD.bazel index d7f87e9869d..2f1b3bff4ff 100644 --- a/testing/src/main/java/org/oppia/android/testing/data/BUILD.bazel +++ b/testing/src/main/java/org/oppia/android/testing/data/BUILD.bazel @@ -6,6 +6,21 @@ Package for common test utilities corresponding to data processing & data provid load("@dagger//:workspace_defs.bzl", "dagger_rules") load("@io_bazel_rules_kotlin//kotlin:kotlin.bzl", "kt_android_library") +kt_android_library( + name = "async_result_subject", + testonly = True, + srcs = [ + "AsyncResultSubject.kt", + ], + visibility = ["//:oppia_testing_visibility"], + deps = [ + "//third_party:com_google_protobuf_protobuf-javalite", + "//third_party:com_google_truth_extensions_truth-liteproto-extension", + "//third_party:com_google_truth_truth", + "//utility/src/main/java/org/oppia/android/util/data:async_result", + ], +) + kt_android_library( name = "data_provider_test_monitor", testonly = True, diff --git a/testing/src/main/java/org/oppia/android/testing/data/DataProviderTestMonitor.kt b/testing/src/main/java/org/oppia/android/testing/data/DataProviderTestMonitor.kt index d2cecc3b1b1..a7ee339db47 100644 --- a/testing/src/main/java/org/oppia/android/testing/data/DataProviderTestMonitor.kt +++ b/testing/src/main/java/org/oppia/android/testing/data/DataProviderTestMonitor.kt @@ -3,6 +3,7 @@ package org.oppia.android.testing.data import androidx.lifecycle.LiveData import androidx.lifecycle.Observer import androidx.test.platform.app.InstrumentationRegistry +import java.lang.IllegalStateException import org.mockito.ArgumentCaptor import org.mockito.Mockito.atLeastOnce import org.mockito.Mockito.mock @@ -118,17 +119,24 @@ class DataProviderTestMonitor private constructor( } private fun retrieveSuccess(operation: () -> AsyncResult): T { - return operation().also { - // Sanity check. - check(it.isSuccess()) { "Expected next result to be a success, not: $it" } - }.getOrThrow() + return when (val result = operation()) { + // Sanity check. Ensure that the full failure stack trace is thrown. + is AsyncResult.Failure -> { + throw IllegalStateException( + "Expected next result to be a success, not: $result", result.error + ) + } + is AsyncResult.Pending -> error("Expected next result to be a success, not: $result") + is AsyncResult.Success -> result.value + } } private fun retrieveFailing(operation: () -> AsyncResult): Throwable { - return operation().also { - // Sanity check. - check(it.isFailure()) { "Expected next result to be a failure, not: $it" } - }.getErrorOrNull() ?: error("Expect result to have a failure error") + return when (val result = operation()) { + is AsyncResult.Failure -> result.error + is AsyncResult.Pending, is AsyncResult.Success -> + error("Expected next result to be a failure, not: $result") + } } /** @@ -148,6 +156,17 @@ class DataProviderTestMonitor private constructor( } } + // TODO: add documentation explaining this is useful for arrangement since it's not making + // assumptions about the result (other than there is one), which is necessary since LiveData + // must be active. Also, add tests & verify that users of the next two functions switch to this + // one, instead, when the extra assertion isn't needed. + fun ensureDataProviderExecutes(dataProvider: DataProvider) { + // Waiting for a result is the same as ensuring the conditions are right for the provider to + // execute (since it must return a result if it's executed, even if it's pending). + val monitor = createMonitor(dataProvider) + monitor.waitForNextResult().also { monitor.stopObservingDataProvider() } + } + /** * Convenience function for monitoring the specified data provider & waiting for its next result * (expected to be a success). See [waitForNextSuccessResult] for specifics. diff --git a/testing/src/main/java/org/oppia/android/testing/lightweightcheckpointing/ExplorationCheckpointTestHelper.kt b/testing/src/main/java/org/oppia/android/testing/lightweightcheckpointing/ExplorationCheckpointTestHelper.kt index 1186150a18e..567d2ce1274 100644 --- a/testing/src/main/java/org/oppia/android/testing/lightweightcheckpointing/ExplorationCheckpointTestHelper.kt +++ b/testing/src/main/java/org/oppia/android/testing/lightweightcheckpointing/ExplorationCheckpointTestHelper.kt @@ -23,6 +23,8 @@ import org.oppia.android.util.data.DataProvider import org.oppia.android.util.data.DataProviders.Companion.toLiveData import javax.inject.Inject import javax.inject.Singleton +import org.oppia.android.domain.exploration.lightweightcheckpointing.ExplorationCheckpointController.ExplorationCheckpointNotFoundException +import org.oppia.android.testing.data.AsyncResultSubject.Companion.assertThat /** The exploration title of Fractions topic, story 0, exploration 0. */ const val FRACTIONS_EXPLORATION_0_TITLE = "What is a Fraction?" @@ -240,7 +242,7 @@ class ExplorationCheckpointTestHelper @Inject constructor( InstrumentationRegistry.getInstrumentation().runOnMainSync { verify(mockExplorationCheckpointObserver, atLeastOnce()) .onChanged(explorationCheckpointCaptor.capture()) - assertThat(explorationCheckpointCaptor.value.isSuccess()).isTrue() + assertThat(explorationCheckpointCaptor.value is AsyncResult.Success).isTrue() } } @@ -270,11 +272,9 @@ class ExplorationCheckpointTestHelper @Inject constructor( InstrumentationRegistry.getInstrumentation().runOnMainSync { verify(mockExplorationCheckpointObserver, atLeastOnce()) .onChanged(explorationCheckpointCaptor.capture()) - assertThat(explorationCheckpointCaptor.value.isFailure()).isTrue() - - assertThat(explorationCheckpointCaptor.value.getErrorOrNull()).isInstanceOf( - ExplorationCheckpointController.ExplorationCheckpointNotFoundException::class.java - ) + assertThat(explorationCheckpointCaptor.value) + .isFailureThat() + .isInstanceOf(ExplorationCheckpointNotFoundException::class.java) } } @@ -322,7 +322,7 @@ class ExplorationCheckpointTestHelper @Inject constructor( InstrumentationRegistry.getInstrumentation().runOnMainSync { verify(mockLiveDataObserver, atLeastOnce()) .onChanged(liveDataResultCaptor.capture()) - assertThat(liveDataResultCaptor.value.isSuccess()).isTrue() + assertThat(liveDataResultCaptor.value is AsyncResult.Success).isTrue() } } diff --git a/testing/src/main/java/org/oppia/android/testing/story/StoryProgressTestHelper.kt b/testing/src/main/java/org/oppia/android/testing/story/StoryProgressTestHelper.kt index a5b1635949b..03db80e5ff7 100644 --- a/testing/src/main/java/org/oppia/android/testing/story/StoryProgressTestHelper.kt +++ b/testing/src/main/java/org/oppia/android/testing/story/StoryProgressTestHelper.kt @@ -929,7 +929,7 @@ class StoryProgressTestHelper @Inject constructor( // Verify that the observer was called, and that the result was successful. InstrumentationRegistry.getInstrumentation().runOnMainSync { verify(mockLiveDataObserver, atLeastOnce()).onChanged(liveDataResultCaptor.capture()) - assertThat(liveDataResultCaptor.value.isSuccess()).isTrue() + assertThat(liveDataResultCaptor.value is AsyncResult.Success).isTrue() } } diff --git a/testing/src/main/java/org/oppia/android/testing/threading/TestCoroutineDispatcher.kt b/testing/src/main/java/org/oppia/android/testing/threading/TestCoroutineDispatcher.kt index 18366e5d004..98239b60936 100644 --- a/testing/src/main/java/org/oppia/android/testing/threading/TestCoroutineDispatcher.kt +++ b/testing/src/main/java/org/oppia/android/testing/threading/TestCoroutineDispatcher.kt @@ -117,7 +117,8 @@ abstract class TestCoroutineDispatcher : CoroutineDispatcher() { } private companion object { - private const val STANDARD_TIMEOUT_SECONDS = 10L + // TODO: revert + private val STANDARD_TIMEOUT_SECONDS = TimeUnit.HOURS.toSeconds(1) private val TIMEOUT_WHEN_DEBUGGING_SECONDS = TimeUnit.HOURS.toSeconds(1) private fun computeTimeout(): Long { diff --git a/testing/src/test/java/org/oppia/android/testing/data/BUILD.bazel b/testing/src/test/java/org/oppia/android/testing/data/BUILD.bazel index 4b84736203b..7f891fac7cd 100644 --- a/testing/src/test/java/org/oppia/android/testing/data/BUILD.bazel +++ b/testing/src/test/java/org/oppia/android/testing/data/BUILD.bazel @@ -18,6 +18,7 @@ oppia_android_test( "//domain/src/main/java/org/oppia/android/domain/translation:translation_controller", "//model:languages_java_proto_lite", "//testing", + "//testing/src/main/java/org/oppia/android/testing/data:async_result_subject", "//testing/src/main/java/org/oppia/android/testing/data:data_provider_test_monitor", "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module", "//testing/src/main/java/org/oppia/android/testing/threading:test_coroutine_dispatchers", diff --git a/testing/src/test/java/org/oppia/android/testing/data/DataProviderTestMonitorTest.kt b/testing/src/test/java/org/oppia/android/testing/data/DataProviderTestMonitorTest.kt index 32984b7ef24..e41099d0524 100644 --- a/testing/src/test/java/org/oppia/android/testing/data/DataProviderTestMonitorTest.kt +++ b/testing/src/test/java/org/oppia/android/testing/data/DataProviderTestMonitorTest.kt @@ -33,6 +33,7 @@ import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode import javax.inject.Inject import javax.inject.Singleton +import org.oppia.android.testing.data.AsyncResultSubject.Companion.assertThat /** Tests for [DataProviderTestMonitor]. */ // FunctionName: test names are conventionally named with underscores. @@ -94,46 +95,44 @@ class DataProviderTestMonitorTest { @Test fun testWaitForNextResult_pendingDataProvider_returnsResult() { val dataProvider = dataProviders.createInMemoryDataProviderAsync("test") { - AsyncResult.pending() + AsyncResult.Pending() } val monitor = monitorFactory.createMonitor(dataProvider) val result = monitor.waitForNextResult() - assertThat(result.isPending()).isTrue() + assertThat(result).isPending() } @Test fun testWaitForNextResult_failingDataProvider_returnsResult() { val dataProvider = dataProviders.createInMemoryDataProviderAsync("test") { - AsyncResult.failed(Exception("Failure")) + AsyncResult.Failure(Exception("Failure")) } val monitor = monitorFactory.createMonitor(dataProvider) val result = monitor.waitForNextResult() - assertThat(result.isFailure()).isTrue() - assertThat(result.getErrorOrNull()).hasMessageThat().contains("Failure") + assertThat(result).isFailureThat().hasMessageThat().contains("Failure") } @Test fun testWaitForNextResult_successfulDataProvider_returnsResult() { val dataProvider = dataProviders.createInMemoryDataProviderAsync("test") { - AsyncResult.success("str value") + AsyncResult.Success("str value") } val monitor = monitorFactory.createMonitor(dataProvider) val result = monitor.waitForNextResult() - assertThat(result.isSuccess()).isTrue() - assertThat(result.getOrThrow()).isEqualTo("str value") + assertThat(result).isStringSuccessThat().isEqualTo("str value") } @Test fun testWaitForNextResult_failureThenSuccess_returnsLatest() { val dataProvider = createDataProviderWithResultsQueue( - "test", AsyncResult.failed(Exception("Failure")), AsyncResult.success("str value") + "test", AsyncResult.Failure(Exception("Failure")), AsyncResult.Success("str value") ) val monitor = monitorFactory.createMonitor(dataProvider) monitor.waitForNextResult() // Wait for the first result. @@ -142,15 +141,14 @@ class DataProviderTestMonitorTest { asyncDataSubscriptionManager.notifyChangeAsync("test") val result = monitor.waitForNextResult() - assertThat(result.isSuccess()).isTrue() - assertThat(result.getOrThrow()).isEqualTo("str value") + assertThat(result).isStringSuccessThat().isEqualTo("str value") } @Test fun testWaitForNextResult_differentValues_notified_returnsLatest() { val dataProvider = createDataProviderWithResultsQueue( - "test", AsyncResult.success("first"), AsyncResult.success("second") + "test", AsyncResult.Success("first"), AsyncResult.Success("second") ) val monitor = monitorFactory.createMonitor(dataProvider) monitor.waitForNextResult() @@ -158,8 +156,7 @@ class DataProviderTestMonitorTest { asyncDataSubscriptionManager.notifyChangeAsync("test") val result = monitor.waitForNextResult() - assertThat(result.isSuccess()).isTrue() - assertThat(result.getOrThrow()).isEqualTo("second") + assertThat(result).isStringSuccessThat().isEqualTo("second") } /* Tests for waitForNextSuccessResult */ @@ -167,7 +164,7 @@ class DataProviderTestMonitorTest { @Test fun testWaitForNextSuccessResult_pendingDataProvider_throwsException() { val dataProvider = dataProviders.createInMemoryDataProviderAsync("test") { - AsyncResult.pending() + AsyncResult.Pending() } val monitor = monitorFactory.createMonitor(dataProvider) @@ -179,7 +176,7 @@ class DataProviderTestMonitorTest { @Test fun testWaitForNextSuccessResult_failingDataProvider_throwsException() { val dataProvider = dataProviders.createInMemoryDataProviderAsync("test") { - AsyncResult.failed(Exception("Failure")) + AsyncResult.Failure(Exception("Failure")) } val monitor = monitorFactory.createMonitor(dataProvider) @@ -191,7 +188,7 @@ class DataProviderTestMonitorTest { @Test fun testWaitForNextSuccessResult_successfulDataProvider_returnsResult() { val dataProvider = dataProviders.createInMemoryDataProviderAsync("test") { - AsyncResult.success("str value") + AsyncResult.Success("str value") } val monitor = monitorFactory.createMonitor(dataProvider) @@ -204,7 +201,7 @@ class DataProviderTestMonitorTest { fun testWaitForNextSuccessResult_failureThenSuccess_notified_returnsLatest() { val dataProvider = createDataProviderWithResultsQueue( - "test", AsyncResult.failed(Exception("Failure")), AsyncResult.success("str value") + "test", AsyncResult.Failure(Exception("Failure")), AsyncResult.Success("str value") ) val monitor = monitorFactory.createMonitor(dataProvider) monitor.waitForNextResult() // Wait for the first result. @@ -219,7 +216,7 @@ class DataProviderTestMonitorTest { fun testWaitForNextSuccessResult_successThenFailure_notified_throwsException() { val dataProvider = createDataProviderWithResultsQueue( - "test", AsyncResult.success("str value"), AsyncResult.failed(Exception("Failure")) + "test", AsyncResult.Success("str value"), AsyncResult.Failure(Exception("Failure")) ) val monitor = monitorFactory.createMonitor(dataProvider) monitor.waitForNextResult() // Wait for the first result. @@ -234,7 +231,7 @@ class DataProviderTestMonitorTest { fun testWaitForNextSuccessResult_differentValues_notified_returnsLatest() { val dataProvider = createDataProviderWithResultsQueue( - "test", AsyncResult.success("first"), AsyncResult.success("second") + "test", AsyncResult.Success("first"), AsyncResult.Success("second") ) val monitor = monitorFactory.createMonitor(dataProvider) monitor.waitForNextResult() // Wait for the first result. @@ -250,7 +247,7 @@ class DataProviderTestMonitorTest { @Test fun testEnsureNextResultIsSuccess_successfulDataProvider_throwsException() { val dataProvider = dataProviders.createInMemoryDataProviderAsync("test") { - AsyncResult.success("str value") + AsyncResult.Success("str value") } val monitor = monitorFactory.createMonitor(dataProvider) @@ -261,7 +258,7 @@ class DataProviderTestMonitorTest { @Test fun testEnsureNextResultIsSuccess_successfulDataProvider_wait_returnsResult() { val dataProvider = dataProviders.createInMemoryDataProviderAsync("test") { - AsyncResult.success("str value") + AsyncResult.Success("str value") } val monitor = monitorFactory.createMonitor(dataProvider) @@ -274,7 +271,7 @@ class DataProviderTestMonitorTest { @Test fun testEnsureNextResultIsSuccess_pendingDataProvider_wait_throwsException() { val dataProvider = dataProviders.createInMemoryDataProviderAsync("test") { - AsyncResult.pending() + AsyncResult.Pending() } val monitor = monitorFactory.createMonitor(dataProvider) @@ -287,7 +284,7 @@ class DataProviderTestMonitorTest { @Test fun testEnsureNextResultIsSuccess_failingDataProvider_wait_throwsException() { val dataProvider = dataProviders.createInMemoryDataProviderAsync("test") { - AsyncResult.failed(Exception("Failure")) + AsyncResult.Failure(Exception("Failure")) } val monitor = monitorFactory.createMonitor(dataProvider) @@ -301,7 +298,7 @@ class DataProviderTestMonitorTest { fun testEnsureNextResultIsSuccess_failureThenSuccess_notified_throwsException() { val dataProvider = createDataProviderWithResultsQueue( - "test", AsyncResult.failed(Exception("Failure")), AsyncResult.success("str value") + "test", AsyncResult.Failure(Exception("Failure")), AsyncResult.Success("str value") ) val monitor = monitorFactory.createMonitor(dataProvider) monitor.waitForNextResult() // Wait for the first result. @@ -316,7 +313,7 @@ class DataProviderTestMonitorTest { fun testEnsureNextResultIsSuccess_failureThenSuccess_notified_wait_returnsLatest() { val dataProvider = createDataProviderWithResultsQueue( - "test", AsyncResult.failed(Exception("Failure")), AsyncResult.success("str value") + "test", AsyncResult.Failure(Exception("Failure")), AsyncResult.Success("str value") ) val monitor = monitorFactory.createMonitor(dataProvider) monitor.waitForNextResult() // Wait for the first result. @@ -332,7 +329,7 @@ class DataProviderTestMonitorTest { fun testEnsureNextResultIsSuccess_successThenFailure_notified_wait_throwsException() { val dataProvider = createDataProviderWithResultsQueue( - "test", AsyncResult.success("str value"), AsyncResult.failed(Exception("Failure")) + "test", AsyncResult.Success("str value"), AsyncResult.Failure(Exception("Failure")) ) val monitor = monitorFactory.createMonitor(dataProvider) monitor.waitForNextResult() // Wait for the first result. @@ -348,7 +345,7 @@ class DataProviderTestMonitorTest { fun testEnsureNextResultIsSuccess_differentValues_notified_wait_returnsLatest() { val dataProvider = createDataProviderWithResultsQueue( - "test", AsyncResult.success("first"), AsyncResult.success("second") + "test", AsyncResult.Success("first"), AsyncResult.Success("second") ) val monitor = monitorFactory.createMonitor(dataProvider) monitor.waitForNextResult() // Wait for the first result. @@ -365,7 +362,7 @@ class DataProviderTestMonitorTest { @Test fun testWaitForNextFailingResult_pendingDataProvider_throwsException() { val dataProvider = dataProviders.createInMemoryDataProviderAsync("test") { - AsyncResult.pending() + AsyncResult.Pending() } val monitor = monitorFactory.createMonitor(dataProvider) @@ -377,7 +374,7 @@ class DataProviderTestMonitorTest { @Test fun testWaitForNextFailingResult_failingDataProvider_returnsResult() { val dataProvider = dataProviders.createInMemoryDataProviderAsync("test") { - AsyncResult.failed(Exception("Failure")) + AsyncResult.Failure(Exception("Failure")) } val monitor = monitorFactory.createMonitor(dataProvider) @@ -389,7 +386,7 @@ class DataProviderTestMonitorTest { @Test fun testWaitForNextFailingResult_successfulDataProvider_throwsException() { val dataProvider = dataProviders.createInMemoryDataProviderAsync("test") { - AsyncResult.success("str value") + AsyncResult.Success("str value") } val monitor = monitorFactory.createMonitor(dataProvider) @@ -402,7 +399,7 @@ class DataProviderTestMonitorTest { fun testWaitForNextFailingResult_successThenFailure_notified_returnsLatest() { val dataProvider = createDataProviderWithResultsQueue( - "test", AsyncResult.success("str value"), AsyncResult.failed(Exception("Failure")) + "test", AsyncResult.Success("str value"), AsyncResult.Failure(Exception("Failure")) ) val monitor = monitorFactory.createMonitor(dataProvider) monitor.waitForNextResult() // Wait for the first result. @@ -417,7 +414,7 @@ class DataProviderTestMonitorTest { fun testWaitForNextFailingResult_failureThenSuccess_notified_throwsException() { val dataProvider = createDataProviderWithResultsQueue( - "test", AsyncResult.failed(Exception("Failure")), AsyncResult.success("str value") + "test", AsyncResult.Failure(Exception("Failure")), AsyncResult.Success("str value") ) val monitor = monitorFactory.createMonitor(dataProvider) monitor.waitForNextResult() // Wait for the first result. @@ -432,7 +429,7 @@ class DataProviderTestMonitorTest { fun testWaitForNextFailingResult_differentValues_notified_returnsLatest() { val dataProvider = createDataProviderWithResultsQueue( - "test", AsyncResult.failed(Exception("First")), AsyncResult.failed(Exception("Second")) + "test", AsyncResult.Failure(Exception("First")), AsyncResult.Failure(Exception("Second")) ) val monitor = monitorFactory.createMonitor(dataProvider) monitor.waitForNextResult() // Wait for the first result. @@ -448,7 +445,7 @@ class DataProviderTestMonitorTest { @Test fun testEnsureNextResultIsFailing_failingDataProvider_throwsException() { val dataProvider = dataProviders.createInMemoryDataProviderAsync("test") { - AsyncResult.failed(Exception("Failure")) + AsyncResult.Failure(Exception("Failure")) } val monitor = monitorFactory.createMonitor(dataProvider) @@ -459,7 +456,7 @@ class DataProviderTestMonitorTest { @Test fun testEnsureNextResultIsFailing_failingDataProvider_wait_returnsResult() { val dataProvider = dataProviders.createInMemoryDataProviderAsync("test") { - AsyncResult.failed(Exception("Failure")) + AsyncResult.Failure(Exception("Failure")) } val monitor = monitorFactory.createMonitor(dataProvider) @@ -472,7 +469,7 @@ class DataProviderTestMonitorTest { @Test fun testEnsureNextResultIsFailing_pendingDataProvider_wait_throwsException() { val dataProvider = dataProviders.createInMemoryDataProviderAsync("test") { - AsyncResult.pending() + AsyncResult.Pending() } val monitor = monitorFactory.createMonitor(dataProvider) @@ -485,7 +482,7 @@ class DataProviderTestMonitorTest { @Test fun testEnsureNextResultIsFailing_successfulDataProvider_wait_throwsException() { val dataProvider = dataProviders.createInMemoryDataProviderAsync("test") { - AsyncResult.success("str value") + AsyncResult.Success("str value") } val monitor = monitorFactory.createMonitor(dataProvider) @@ -499,7 +496,7 @@ class DataProviderTestMonitorTest { fun testEnsureNextResultIsFailing_successThenFailure_notified_throwsException() { val dataProvider = createDataProviderWithResultsQueue( - "test", AsyncResult.success("str value"), AsyncResult.failed(Exception("Failure")) + "test", AsyncResult.Success("str value"), AsyncResult.Failure(Exception("Failure")) ) val monitor = monitorFactory.createMonitor(dataProvider) monitor.waitForNextResult() // Wait for the first result. @@ -513,7 +510,7 @@ class DataProviderTestMonitorTest { fun testEnsureNextResultIsFailing_successThenFailure_notified_wait_returnsLatest() { val dataProvider = createDataProviderWithResultsQueue( - "test", AsyncResult.success("str value"), AsyncResult.failed(Exception("Failure")) + "test", AsyncResult.Success("str value"), AsyncResult.Failure(Exception("Failure")) ) val monitor = monitorFactory.createMonitor(dataProvider) monitor.waitForNextResult() // Wait for the first result. @@ -529,7 +526,7 @@ class DataProviderTestMonitorTest { fun testEnsureNextResultIsFailing_failureThenSuccess_notified_wait_throwsException() { val dataProvider = createDataProviderWithResultsQueue( - "test", AsyncResult.failed(Exception("Failure")), AsyncResult.success("str value") + "test", AsyncResult.Failure(Exception("Failure")), AsyncResult.Success("str value") ) val monitor = monitorFactory.createMonitor(dataProvider) monitor.waitForNextResult() // Wait for the first result. @@ -545,7 +542,7 @@ class DataProviderTestMonitorTest { fun testEnsureNextResultIsFailing_differentValues_notified_wait_returnsLatest() { val dataProvider = createDataProviderWithResultsQueue( - "test", AsyncResult.failed(Exception("First")), AsyncResult.failed(Exception("Second")) + "test", AsyncResult.Failure(Exception("First")), AsyncResult.Failure(Exception("Second")) ) val monitor = monitorFactory.createMonitor(dataProvider) monitor.waitForNextResult() // Wait for the first result. @@ -562,7 +559,7 @@ class DataProviderTestMonitorTest { @Test fun testVerifyProviderIsNotUpdated_pendingDataProvider_throwsException() { val dataProvider = dataProviders.createInMemoryDataProviderAsync("test") { - AsyncResult.pending() + AsyncResult.Pending() } val monitor = monitorFactory.createMonitor(dataProvider) @@ -573,7 +570,7 @@ class DataProviderTestMonitorTest { @Test fun testVerifyProviderIsNotUpdated_failingDataProvider_throwsException() { val dataProvider = dataProviders.createInMemoryDataProviderAsync("test") { - AsyncResult.failed(Exception("Failure")) + AsyncResult.Failure(Exception("Failure")) } val monitor = monitorFactory.createMonitor(dataProvider) @@ -584,7 +581,7 @@ class DataProviderTestMonitorTest { @Test fun testVerifyProviderIsNotUpdated_successfulDataProvider_throwsException() { val dataProvider = dataProviders.createInMemoryDataProviderAsync("test") { - AsyncResult.success("str value") + AsyncResult.Success("str value") } val monitor = monitorFactory.createMonitor(dataProvider) @@ -596,7 +593,7 @@ class DataProviderTestMonitorTest { fun testVerifyProviderIsNotUpdated_successThenFailure_notified_throwsException() { val dataProvider = createDataProviderWithResultsQueue( - "test", AsyncResult.success("str value"), AsyncResult.failed(Exception("Failure")) + "test", AsyncResult.Success("str value"), AsyncResult.Failure(Exception("Failure")) ) val monitor = monitorFactory.createMonitor(dataProvider) monitor.waitForNextResult() // Wait for the first result. @@ -610,7 +607,7 @@ class DataProviderTestMonitorTest { fun testVerifyProviderIsNotUpdated_failureThenSuccess_notified_throwsException() { val dataProvider = createDataProviderWithResultsQueue( - "test", AsyncResult.failed(Exception("Failure")), AsyncResult.success("str value") + "test", AsyncResult.Failure(Exception("Failure")), AsyncResult.Success("str value") ) val monitor = monitorFactory.createMonitor(dataProvider) monitor.waitForNextResult() // Wait for the first result. @@ -626,7 +623,7 @@ class DataProviderTestMonitorTest { fun testVerifyProviderIsNotUpdated_differentValues_notified_throwsException() { val dataProvider = createDataProviderWithResultsQueue( - "test", AsyncResult.success("first"), AsyncResult.success("second") + "test", AsyncResult.Success("first"), AsyncResult.Success("second") ) val monitor = monitorFactory.createMonitor(dataProvider) monitor.waitForNextResult() // Wait for the first result. @@ -641,7 +638,7 @@ class DataProviderTestMonitorTest { fun testVerifyProviderIsNotUpdated_waitForSuccess_noChanges_doesNotThrowException() { val dataProvider = createDataProviderWithResultsQueue( - "test", AsyncResult.success("first"), AsyncResult.success("second") + "test", AsyncResult.Success("first"), AsyncResult.Success("second") ) val monitor = monitorFactory.createMonitor(dataProvider) monitor.waitForNextSuccessResult() @@ -657,7 +654,7 @@ class DataProviderTestMonitorTest { @Test fun testWaitForNextSuccessfulResult_pendingDataProvider_throwsException() { val dataProvider = dataProviders.createInMemoryDataProviderAsync("test") { - AsyncResult.pending() + AsyncResult.Pending() } val failure = @@ -671,7 +668,7 @@ class DataProviderTestMonitorTest { @Test fun testWaitForNextSuccessfulResult_failingDataProvider_throwsException() { val dataProvider = dataProviders.createInMemoryDataProviderAsync("test") { - AsyncResult.failed(Exception("Failure")) + AsyncResult.Failure(Exception("Failure")) } val failure = assertThrows(IllegalStateException::class) { @@ -684,7 +681,7 @@ class DataProviderTestMonitorTest { @Test fun testWaitForNextSuccessfulResult_successfulDataProvider_returnsResult() { val dataProvider = dataProviders.createInMemoryDataProviderAsync("test") { - AsyncResult.success("str value") + AsyncResult.Success("str value") } val result = monitorFactory.waitForNextSuccessfulResult(dataProvider) @@ -696,7 +693,7 @@ class DataProviderTestMonitorTest { fun testWaitForNextSuccessfulResult_failureThenSuccess_consumed_returnsLatest() { val dataProvider = createDataProviderWithResultsQueue( - "test", AsyncResult.failed(Exception("Failure")), AsyncResult.success("str value") + "test", AsyncResult.Failure(Exception("Failure")), AsyncResult.Success("str value") ) monitorFactory.waitForNextFailureResult(dataProvider) @@ -709,7 +706,7 @@ class DataProviderTestMonitorTest { fun testWaitForNextSuccessfulResult_successThenFailure_consumed_throwsException() { val dataProvider = createDataProviderWithResultsQueue( - "test", AsyncResult.success("str value"), AsyncResult.failed(Exception("Failure")) + "test", AsyncResult.Success("str value"), AsyncResult.Failure(Exception("Failure")) ) monitorFactory.waitForNextSuccessfulResult(dataProvider) @@ -724,7 +721,7 @@ class DataProviderTestMonitorTest { fun testWaitForNextSuccessfulResult_differentValues_consumed_returnsLatest() { val dataProvider = createDataProviderWithResultsQueue( - "test", AsyncResult.success("first"), AsyncResult.success("second") + "test", AsyncResult.Success("first"), AsyncResult.Success("second") ) monitorFactory.waitForNextSuccessfulResult(dataProvider) @@ -737,7 +734,7 @@ class DataProviderTestMonitorTest { fun testWaitForNextSuccessfulResult_twiceForChangedProvider_returnsCorrectValues() { val dataProvider = createDataProviderWithResultsQueue( - "test", AsyncResult.success("first"), AsyncResult.success("second") + "test", AsyncResult.Success("first"), AsyncResult.Success("second") ) val firstResult = monitorFactory.waitForNextSuccessfulResult(dataProvider) @@ -752,7 +749,7 @@ class DataProviderTestMonitorTest { @Test fun testWaitForNextFailureResult_pendingDataProvider_throwsException() { val dataProvider = dataProviders.createInMemoryDataProviderAsync("test") { - AsyncResult.pending() + AsyncResult.Pending() } val failure = assertThrows(IllegalStateException::class) { @@ -765,7 +762,7 @@ class DataProviderTestMonitorTest { @Test fun testWaitForNextFailureResult_failingDataProvider_returnsResult() { val dataProvider = dataProviders.createInMemoryDataProviderAsync("test") { - AsyncResult.failed(Exception("Failure")) + AsyncResult.Failure(Exception("Failure")) } val result = monitorFactory.waitForNextFailureResult(dataProvider) @@ -776,7 +773,7 @@ class DataProviderTestMonitorTest { @Test fun testWaitForNextFailureResult_successfulDataProvider_throwsException() { val dataProvider = dataProviders.createInMemoryDataProviderAsync("test") { - AsyncResult.success("str value") + AsyncResult.Success("str value") } val failure = assertThrows(IllegalStateException::class) { @@ -790,7 +787,7 @@ class DataProviderTestMonitorTest { fun testWaitForNextFailureResult_successThenFailure_consumed_returnsLatest() { val dataProvider = createDataProviderWithResultsQueue( - "test", AsyncResult.success("str value"), AsyncResult.failed(Exception("Failure")) + "test", AsyncResult.Success("str value"), AsyncResult.Failure(Exception("Failure")) ) monitorFactory.waitForNextSuccessfulResult(dataProvider) @@ -803,7 +800,7 @@ class DataProviderTestMonitorTest { fun testWaitForNextFailureResult_failureThenSuccess_consumed_throwsException() { val dataProvider = createDataProviderWithResultsQueue( - "test", AsyncResult.failed(Exception("Failure")), AsyncResult.success("str value") + "test", AsyncResult.Failure(Exception("Failure")), AsyncResult.Success("str value") ) monitorFactory.waitForNextFailureResult(dataProvider) @@ -818,7 +815,7 @@ class DataProviderTestMonitorTest { fun testWaitForNextFailureResult_differentValues_consumed_returnsLatest() { val dataProvider = createDataProviderWithResultsQueue( - "test", AsyncResult.failed(Exception("First")), AsyncResult.failed(Exception("Second")) + "test", AsyncResult.Failure(Exception("First")), AsyncResult.Failure(Exception("Second")) ) monitorFactory.waitForNextFailureResult(dataProvider) @@ -831,7 +828,7 @@ class DataProviderTestMonitorTest { fun testWaitForNextFailureResult_twiceForChangedProvider_returnsCorrectValues() { val dataProvider = createDataProviderWithResultsQueue( - "test", AsyncResult.failed(Exception("First")), AsyncResult.failed(Exception("Second")) + "test", AsyncResult.Failure(Exception("First")), AsyncResult.Failure(Exception("Second")) ) val firstResult = monitorFactory.waitForNextFailureResult(dataProvider) diff --git a/testing/src/test/java/org/oppia/android/testing/lightweightcheckpointing/ExplorationCheckpointTestHelperTest.kt b/testing/src/test/java/org/oppia/android/testing/lightweightcheckpointing/ExplorationCheckpointTestHelperTest.kt index 603194058ba..0aeb0800f11 100644 --- a/testing/src/test/java/org/oppia/android/testing/lightweightcheckpointing/ExplorationCheckpointTestHelperTest.kt +++ b/testing/src/test/java/org/oppia/android/testing/lightweightcheckpointing/ExplorationCheckpointTestHelperTest.kt @@ -2,7 +2,6 @@ package org.oppia.android.testing.lightweightcheckpointing import android.app.Application import android.content.Context -import androidx.lifecycle.Observer import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat @@ -10,18 +9,11 @@ import dagger.BindsInstance import dagger.Component import dagger.Module import dagger.Provides +import javax.inject.Inject +import javax.inject.Singleton import org.junit.Before -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentCaptor -import org.mockito.Captor -import org.mockito.Mock -import org.mockito.Mockito.atLeastOnce -import org.mockito.Mockito.reset -import org.mockito.Mockito.verify -import org.mockito.junit.MockitoJUnit -import org.mockito.junit.MockitoRule import org.oppia.android.app.model.ExplorationCheckpoint import org.oppia.android.app.model.ProfileId import org.oppia.android.domain.exploration.lightweightcheckpointing.ExplorationCheckpointController @@ -31,6 +23,7 @@ import org.oppia.android.domain.topic.FRACTIONS_EXPLORATION_ID_0 import org.oppia.android.domain.topic.FRACTIONS_EXPLORATION_ID_1 import org.oppia.android.domain.topic.RATIOS_EXPLORATION_ID_0 import org.oppia.android.testing.TestLogReportingModule +import org.oppia.android.testing.data.DataProviderTestMonitor import org.oppia.android.testing.environment.TestEnvironmentConfig import org.oppia.android.testing.robolectric.RobolectricModule import org.oppia.android.testing.threading.TestCoroutineDispatchers @@ -41,8 +34,6 @@ import org.oppia.android.util.caching.AssetModule import org.oppia.android.util.caching.CacheAssetsLocally import org.oppia.android.util.caching.LoadLessonProtosFromAssets import org.oppia.android.util.caching.TopicListToCache -import org.oppia.android.util.data.AsyncResult -import org.oppia.android.util.data.DataProviders.Companion.toLiveData import org.oppia.android.util.data.DataProvidersInjector import org.oppia.android.util.data.DataProvidersInjectorProvider import org.oppia.android.util.locale.LocaleProdModule @@ -53,38 +44,20 @@ import org.oppia.android.util.logging.LogLevel import org.oppia.android.util.networking.NetworkConnectionUtilDebugModule import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode -import javax.inject.Inject -import javax.inject.Singleton /** Tests for [ExplorationCheckpointTestHelper]. */ +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) @Config(application = ExplorationCheckpointTestHelperTest.TestApplication::class) class ExplorationCheckpointTestHelperTest { - @Rule - @JvmField - val mockitoRule: MockitoRule = MockitoJUnit.rule() - - @Inject - lateinit var context: Context - - @Inject - lateinit var fakeOppiaClock: FakeOppiaClock - - @Inject - lateinit var explorationCheckpointTestHelper: ExplorationCheckpointTestHelper - - @Inject - lateinit var explorationCheckpointController: ExplorationCheckpointController - - @Inject - lateinit var testCoroutineDispatchers: TestCoroutineDispatchers - - @Mock - lateinit var mockExplorationCheckpointObserver: Observer> - - @Captor - lateinit var explorationCheckpointCaptor: ArgumentCaptor> + @Inject lateinit var context: Context + @Inject lateinit var fakeOppiaClock: FakeOppiaClock + @Inject lateinit var explorationCheckpointTestHelper: ExplorationCheckpointTestHelper + @Inject lateinit var explorationCheckpointController: ExplorationCheckpointController + @Inject lateinit var testCoroutineDispatchers: TestCoroutineDispatchers + @Inject lateinit var monitorFactory: DataProviderTestMonitor.Factory private val profileId = ProfileId.newBuilder().setInternalId(0).build() @@ -105,12 +78,11 @@ class ExplorationCheckpointTestHelperTest { version = FRACTIONS_STORY_0_EXPLORATION_0_CURRENT_VERSION ) - verifySavedCheckpointHasCorrectExplorationDetails( - profileId = profileId, - explorationId = FRACTIONS_EXPLORATION_ID_0, - explorationTitle = FRACTIONS_EXPLORATION_0_TITLE, - pendingStateName = FRACTIONS_STORY_0_EXPLORATION_0_FIRST_STATE_NAME - ) + // Verify saved checkpoint has correct exploration title and pending state name. + val checkpoint = retrieveCheckpoint(profileId, FRACTIONS_EXPLORATION_ID_0) + assertThat(checkpoint.explorationTitle).isEqualTo(FRACTIONS_EXPLORATION_0_TITLE) + assertThat(checkpoint.pendingStateName) + .isEqualTo(FRACTIONS_STORY_0_EXPLORATION_0_FIRST_STATE_NAME) } @Test @@ -120,24 +92,22 @@ class ExplorationCheckpointTestHelperTest { version = FRACTIONS_STORY_0_EXPLORATION_0_CURRENT_VERSION ) - verifySavedCheckpointHasCorrectExplorationDetails( - profileId = profileId, - explorationId = FRACTIONS_EXPLORATION_ID_0, - explorationTitle = FRACTIONS_EXPLORATION_0_TITLE, - pendingStateName = FRACTIONS_STORY_0_EXPLORATION_0_FIRST_STATE_NAME - ) + // Verify saved checkpoint has correct exploration title and pending state name. + val checkpoint = retrieveCheckpoint(profileId, FRACTIONS_EXPLORATION_ID_0) + assertThat(checkpoint.explorationTitle).isEqualTo(FRACTIONS_EXPLORATION_0_TITLE) + assertThat(checkpoint.pendingStateName) + .isEqualTo(FRACTIONS_STORY_0_EXPLORATION_0_FIRST_STATE_NAME) explorationCheckpointTestHelper.updateCheckpointForFractionsStory0Exploration0( profileId = profileId, version = FRACTIONS_STORY_0_EXPLORATION_0_CURRENT_VERSION ) - verifySavedCheckpointHasCorrectExplorationDetails( - profileId = profileId, - explorationId = FRACTIONS_EXPLORATION_ID_0, - explorationTitle = FRACTIONS_EXPLORATION_0_TITLE, - pendingStateName = FRACTIONS_STORY_0_EXPLORATION_0_SECOND_STATE_NAME - ) + // Verify saved checkpoint has correct exploration title and pending state name. + val checkpoint1 = retrieveCheckpoint(profileId, FRACTIONS_EXPLORATION_ID_0) + assertThat(checkpoint1.explorationTitle).isEqualTo(FRACTIONS_EXPLORATION_0_TITLE) + assertThat(checkpoint1.pendingStateName) + .isEqualTo(FRACTIONS_STORY_0_EXPLORATION_0_SECOND_STATE_NAME) } @Test @@ -147,12 +117,11 @@ class ExplorationCheckpointTestHelperTest { version = FRACTIONS_STORY_0_EXPLORATION_1_CURRENT_VERSION ) - verifySavedCheckpointHasCorrectExplorationDetails( - profileId = profileId, - explorationId = FRACTIONS_EXPLORATION_ID_1, - explorationTitle = FRACTIONS_EXPLORATION_1_TITLE, - pendingStateName = FRACTIONS_STORY_0_EXPLORATION_1_FIRST_STATE_NAME - ) + // Verify saved checkpoint has correct exploration title and pending state name. + val checkpoint = retrieveCheckpoint(profileId, FRACTIONS_EXPLORATION_ID_1) + assertThat(checkpoint.explorationTitle).isEqualTo(FRACTIONS_EXPLORATION_1_TITLE) + assertThat(checkpoint.pendingStateName) + .isEqualTo(FRACTIONS_STORY_0_EXPLORATION_1_FIRST_STATE_NAME) } @Test @@ -162,11 +131,11 @@ class ExplorationCheckpointTestHelperTest { version = FRACTIONS_STORY_0_EXPLORATION_1_CURRENT_VERSION ) - verifySavedCheckpointHasCorrectExplorationDetails( - profileId = profileId, - explorationId = FRACTIONS_EXPLORATION_ID_1, - explorationTitle = FRACTIONS_EXPLORATION_1_TITLE, - pendingStateName = FRACTIONS_STORY_0_EXPLORATION_1_FIRST_STATE_NAME + // Verify saved checkpoint has correct exploration title and pending state name. + val checkpoint = retrieveCheckpoint(profileId, FRACTIONS_EXPLORATION_ID_1) + assertThat(checkpoint.explorationTitle).isEqualTo(FRACTIONS_EXPLORATION_1_TITLE) + assertThat(checkpoint.pendingStateName) + .isEqualTo(FRACTIONS_STORY_0_EXPLORATION_1_FIRST_STATE_NAME ) explorationCheckpointTestHelper.updateCheckpointForFractionsStory0Exploration1( @@ -174,12 +143,11 @@ class ExplorationCheckpointTestHelperTest { version = FRACTIONS_STORY_0_EXPLORATION_1_CURRENT_VERSION, ) - verifySavedCheckpointHasCorrectExplorationDetails( - profileId = profileId, - explorationId = FRACTIONS_EXPLORATION_ID_1, - explorationTitle = FRACTIONS_EXPLORATION_1_TITLE, - pendingStateName = FRACTIONS_STORY_0_EXPLORATION_1_SECOND_STATE_NAME - ) + // Verify saved checkpoint has correct exploration title and pending state name. + val checkpoint1 = retrieveCheckpoint(profileId, FRACTIONS_EXPLORATION_ID_1) + assertThat(checkpoint1.explorationTitle).isEqualTo(FRACTIONS_EXPLORATION_1_TITLE) + assertThat(checkpoint1.pendingStateName) + .isEqualTo(FRACTIONS_STORY_0_EXPLORATION_1_SECOND_STATE_NAME) } @Test @@ -189,12 +157,10 @@ class ExplorationCheckpointTestHelperTest { version = RATIOS_STORY_0_EXPLORATION_0_CURRENT_VERSION ) - verifySavedCheckpointHasCorrectExplorationDetails( - profileId = profileId, - explorationId = RATIOS_EXPLORATION_ID_0, - explorationTitle = RATIOS_EXPLORATION_0_TITLE, - pendingStateName = RATIOS_STORY_0_EXPLORATION_0_FIRST_STATE_NAME - ) + // Verify saved checkpoint has correct exploration title and pending state name. + val checkpoint = retrieveCheckpoint(profileId, RATIOS_EXPLORATION_ID_0) + assertThat(checkpoint.explorationTitle).isEqualTo(RATIOS_EXPLORATION_0_TITLE) + assertThat(checkpoint.pendingStateName).isEqualTo(RATIOS_STORY_0_EXPLORATION_0_FIRST_STATE_NAME) } @Test @@ -204,49 +170,29 @@ class ExplorationCheckpointTestHelperTest { version = RATIOS_STORY_0_EXPLORATION_0_CURRENT_VERSION ) - verifySavedCheckpointHasCorrectExplorationDetails( - profileId = profileId, - explorationId = RATIOS_EXPLORATION_ID_0, - explorationTitle = RATIOS_EXPLORATION_0_TITLE, - pendingStateName = RATIOS_STORY_0_EXPLORATION_0_FIRST_STATE_NAME - ) + // Verify saved checkpoint has correct exploration title and pending state name. + val checkpoint = retrieveCheckpoint(profileId, RATIOS_EXPLORATION_ID_0) + assertThat(checkpoint.explorationTitle).isEqualTo(RATIOS_EXPLORATION_0_TITLE) + assertThat(checkpoint.pendingStateName).isEqualTo(RATIOS_STORY_0_EXPLORATION_0_FIRST_STATE_NAME) explorationCheckpointTestHelper.updateCheckpointForRatiosStory0Exploration0( profileId = profileId, version = RATIOS_STORY_0_EXPLORATION_0_CURRENT_VERSION ) - verifySavedCheckpointHasCorrectExplorationDetails( - profileId = profileId, - explorationId = RATIOS_EXPLORATION_ID_0, - explorationTitle = RATIOS_EXPLORATION_0_TITLE, - pendingStateName = RATIOS_STORY_0_EXPLORATION_0_SECOND_STATE_NAME - ) + // Verify saved checkpoint has correct exploration title and pending state name. + val checkpoint1 = retrieveCheckpoint(profileId, RATIOS_EXPLORATION_ID_0) + assertThat(checkpoint1.explorationTitle).isEqualTo(RATIOS_EXPLORATION_0_TITLE) + assertThat(checkpoint1.pendingStateName) + .isEqualTo(RATIOS_STORY_0_EXPLORATION_0_SECOND_STATE_NAME) } - private fun verifySavedCheckpointHasCorrectExplorationDetails( - profileId: ProfileId, - explorationId: String, - explorationTitle: String, - pendingStateName: String - ) { - reset(mockExplorationCheckpointObserver) - val retrieveFakeCheckpointLiveData = - explorationCheckpointController.retrieveExplorationCheckpoint( - profileId, - explorationId - ).toLiveData() - retrieveFakeCheckpointLiveData.observeForever(mockExplorationCheckpointObserver) - testCoroutineDispatchers.runCurrent() - - // Verify saved checkpoint has correct exploration title and pending state name. - verify(mockExplorationCheckpointObserver, atLeastOnce()) - .onChanged(explorationCheckpointCaptor.capture()) - assertThat(explorationCheckpointCaptor.value.isSuccess()).isTrue() - assertThat(explorationCheckpointCaptor.value.getOrThrow().explorationTitle) - .isEqualTo(explorationTitle) - assertThat(explorationCheckpointCaptor.value.getOrThrow().pendingStateName) - .isEqualTo(pendingStateName) + private fun retrieveCheckpoint( + profileId: ProfileId, explorationId: String + ): ExplorationCheckpoint { + val retrieveCheckpointProvider = + explorationCheckpointController.retrieveExplorationCheckpoint(profileId, explorationId) + return monitorFactory.waitForNextSuccessfulResult(retrieveCheckpointProvider) } // TODO(#89): Move this to a common test application component. diff --git a/testing/src/test/java/org/oppia/android/testing/profile/ProfileTestHelperTest.kt b/testing/src/test/java/org/oppia/android/testing/profile/ProfileTestHelperTest.kt index e831ced54df..5799ac7ac84 100644 --- a/testing/src/test/java/org/oppia/android/testing/profile/ProfileTestHelperTest.kt +++ b/testing/src/test/java/org/oppia/android/testing/profile/ProfileTestHelperTest.kt @@ -10,6 +10,8 @@ import dagger.BindsInstance import dagger.Component import dagger.Module import dagger.Provides +import javax.inject.Inject +import javax.inject.Singleton import org.junit.Before import org.junit.Rule import org.junit.Test @@ -21,16 +23,16 @@ import org.mockito.Mockito.atLeastOnce import org.mockito.Mockito.verify import org.mockito.junit.MockitoJUnit import org.mockito.junit.MockitoRule -import org.oppia.android.app.model.Profile import org.oppia.android.domain.oppialogger.LogStorageModule import org.oppia.android.domain.profile.ProfileManagementController import org.oppia.android.testing.TestLogReportingModule +import org.oppia.android.testing.data.AsyncResultSubject.Companion.assertThat +import org.oppia.android.testing.data.DataProviderTestMonitor import org.oppia.android.testing.robolectric.RobolectricModule import org.oppia.android.testing.threading.TestCoroutineDispatchers import org.oppia.android.testing.threading.TestDispatcherModule import org.oppia.android.testing.time.FakeOppiaClockModule import org.oppia.android.util.data.AsyncResult -import org.oppia.android.util.data.DataProviders.Companion.toLiveData import org.oppia.android.util.data.DataProvidersInjector import org.oppia.android.util.data.DataProvidersInjectorProvider import org.oppia.android.util.locale.LocaleProdModule @@ -41,41 +43,25 @@ import org.oppia.android.util.logging.LogLevel import org.oppia.android.util.networking.NetworkConnectionUtilDebugModule import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode -import javax.inject.Inject -import javax.inject.Singleton -/** Tests for [ProfileTestHelperTest]. */ +/** Tests for [ProfileTestHelper]. */ +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) @Config(application = ProfileTestHelperTest.TestApplication::class) class ProfileTestHelperTest { - @Rule - @JvmField - val mockitoRule: MockitoRule = MockitoJUnit.rule() - - @Inject - lateinit var context: Context - - @Inject - lateinit var profileTestHelper: ProfileTestHelper - - @Inject - lateinit var profileManagementController: ProfileManagementController - - @Inject - lateinit var testCoroutineDispatchers: TestCoroutineDispatchers - - @Mock - lateinit var mockProfilesObserver: Observer>> + @field:[Rule JvmField] val mockitoRule: MockitoRule = MockitoJUnit.rule() - @Captor - lateinit var profilesResultCaptor: ArgumentCaptor>> + @Inject lateinit var context: Context + @Inject lateinit var profileTestHelper: ProfileTestHelper + @Inject lateinit var profileManagementController: ProfileManagementController + @Inject lateinit var testCoroutineDispatchers: TestCoroutineDispatchers + @Inject lateinit var monitorFactory: DataProviderTestMonitor.Factory - @Mock - lateinit var mockUpdateResultObserver: Observer> + @Mock lateinit var mockUpdateResultObserver: Observer> - @Captor - lateinit var updateResultCaptor: ArgumentCaptor> + @Captor lateinit var updateResultCaptor: ArgumentCaptor> @Before fun setUp() { @@ -89,14 +75,12 @@ class ProfileTestHelperTest { @Test fun testInitializeProfiles_initializeProfiles_checkProfilesAreAddedAndCurrentIsSet() { profileTestHelper.initializeProfiles().observeForever(mockUpdateResultObserver) - profileManagementController.getProfiles().toLiveData().observeForever(mockProfilesObserver) + val profilesProvider = profileManagementController.getProfiles() testCoroutineDispatchers.runCurrent() - verify(mockProfilesObserver, atLeastOnce()).onChanged(profilesResultCaptor.capture()) verify(mockUpdateResultObserver, atLeastOnce()).onChanged(updateResultCaptor.capture()) - assertThat(profilesResultCaptor.value.isSuccess()).isTrue() - assertThat(updateResultCaptor.value.isSuccess()).isTrue() - val profiles = profilesResultCaptor.value.getOrThrow() + assertThat(updateResultCaptor.value).isSuccess() + val profiles = monitorFactory.waitForNextSuccessfulResult(profilesProvider) assertThat(profiles[0].name).isEqualTo("Admin") assertThat(profiles[0].isAdmin).isTrue() assertThat(profiles[1].name).isEqualTo("Ben") @@ -107,14 +91,12 @@ class ProfileTestHelperTest { @Test fun testInitializeProfiles_addOnlyAdminProfile_checkProfileIsAddedAndCurrentIsSet() { profileTestHelper.addOnlyAdminProfile().observeForever(mockUpdateResultObserver) - profileManagementController.getProfiles().toLiveData().observeForever(mockProfilesObserver) + val profilesProvider = profileManagementController.getProfiles() testCoroutineDispatchers.runCurrent() - verify(mockProfilesObserver, atLeastOnce()).onChanged(profilesResultCaptor.capture()) verify(mockUpdateResultObserver, atLeastOnce()).onChanged(updateResultCaptor.capture()) - assertThat(profilesResultCaptor.value.isSuccess()).isTrue() - assertThat(updateResultCaptor.value.isSuccess()).isTrue() - val profiles = profilesResultCaptor.value.getOrThrow() + assertThat(updateResultCaptor.value).isSuccess() + val profiles = monitorFactory.waitForNextSuccessfulResult(profilesProvider) assertThat(profiles.size).isEqualTo(1) assertThat(profiles[0].name).isEqualTo("Admin") assertThat(profiles[0].isAdmin).isTrue() @@ -125,12 +107,11 @@ class ProfileTestHelperTest { fun testAddMoreProfiles_addMoreProfiles_checkProfilesAreAdded() { profileTestHelper.addMoreProfiles(10) testCoroutineDispatchers.runCurrent() - profileManagementController.getProfiles().toLiveData().observeForever(mockProfilesObserver) + val profilesProvider = profileManagementController.getProfiles() testCoroutineDispatchers.runCurrent() - verify(mockProfilesObserver, atLeastOnce()).onChanged(profilesResultCaptor.capture()) - assertThat(profilesResultCaptor.value.isSuccess()).isTrue() - assertThat(profilesResultCaptor.value.getOrThrow().size).isEqualTo(10) + val profiles = monitorFactory.waitForNextSuccessfulResult(profilesProvider) + assertThat(profiles).hasSize(10) } @Test @@ -141,7 +122,7 @@ class ProfileTestHelperTest { testCoroutineDispatchers.runCurrent() verify(mockUpdateResultObserver, atLeastOnce()).onChanged(updateResultCaptor.capture()) - assertThat(updateResultCaptor.value.isSuccess()).isTrue() + assertThat(updateResultCaptor.value).isSuccess() assertThat(profileManagementController.getCurrentProfileId().internalId).isEqualTo(0) } @@ -153,7 +134,7 @@ class ProfileTestHelperTest { testCoroutineDispatchers.runCurrent() verify(mockUpdateResultObserver, atLeastOnce()).onChanged(updateResultCaptor.capture()) - assertThat(updateResultCaptor.value.isSuccess()).isTrue() + assertThat(updateResultCaptor.value).isSuccess() assertThat(profileManagementController.getCurrentProfileId().internalId).isEqualTo(1) } @@ -165,7 +146,7 @@ class ProfileTestHelperTest { testCoroutineDispatchers.runCurrent() verify(mockUpdateResultObserver, atLeastOnce()).onChanged(updateResultCaptor.capture()) - assertThat(updateResultCaptor.value.isSuccess()).isTrue() + assertThat(updateResultCaptor.value).isSuccess() assertThat(profileManagementController.getCurrentProfileId().internalId).isEqualTo(2) } diff --git a/testing/src/test/java/org/oppia/android/testing/story/StoryProgressTestHelperTest.kt b/testing/src/test/java/org/oppia/android/testing/story/StoryProgressTestHelperTest.kt index 10464c26f96..f2635f82ea2 100644 --- a/testing/src/test/java/org/oppia/android/testing/story/StoryProgressTestHelperTest.kt +++ b/testing/src/test/java/org/oppia/android/testing/story/StoryProgressTestHelperTest.kt @@ -2,7 +2,6 @@ package org.oppia.android.testing.story import android.app.Application import android.content.Context -import androidx.lifecycle.Observer import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat @@ -10,18 +9,12 @@ import dagger.BindsInstance import dagger.Component import dagger.Module import dagger.Provides +import java.util.concurrent.TimeUnit +import javax.inject.Inject +import javax.inject.Singleton import org.junit.Before -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentCaptor -import org.mockito.Captor -import org.mockito.Mock -import org.mockito.Mockito.atLeastOnce -import org.mockito.Mockito.reset -import org.mockito.Mockito.verify -import org.mockito.junit.MockitoJUnit -import org.mockito.junit.MockitoRule import org.oppia.android.app.model.ChapterPlayState import org.oppia.android.app.model.ChapterProgress import org.oppia.android.app.model.ChapterSummary @@ -53,6 +46,7 @@ import org.oppia.android.domain.topic.TEST_TOPIC_ID_0 import org.oppia.android.domain.topic.TEST_TOPIC_ID_1 import org.oppia.android.domain.topic.TopicController import org.oppia.android.testing.TestLogReportingModule +import org.oppia.android.testing.data.DataProviderTestMonitor import org.oppia.android.testing.environment.TestEnvironmentConfig import org.oppia.android.testing.robolectric.RobolectricModule import org.oppia.android.testing.threading.TestCoroutineDispatchers @@ -61,9 +55,6 @@ import org.oppia.android.testing.time.FakeOppiaClock import org.oppia.android.testing.time.FakeOppiaClockModule import org.oppia.android.util.caching.AssetModule import org.oppia.android.util.caching.LoadLessonProtosFromAssets -import org.oppia.android.util.data.AsyncResult -import org.oppia.android.util.data.DataProvider -import org.oppia.android.util.data.DataProviders.Companion.toLiveData import org.oppia.android.util.data.DataProvidersInjector import org.oppia.android.util.data.DataProvidersInjectorProvider import org.oppia.android.util.locale.LocaleProdModule @@ -72,49 +63,21 @@ import org.oppia.android.util.networking.NetworkConnectionUtilDebugModule import org.oppia.android.util.parser.image.ImageParsingModule import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode -import java.util.concurrent.TimeUnit -import javax.inject.Inject -import javax.inject.Singleton /** Tests for [StoryProgressTestHelper]. */ +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) @Config(application = StoryProgressTestHelperTest.TestApplication::class) class StoryProgressTestHelperTest { - - @Rule - @JvmField - val mockitoRule: MockitoRule = MockitoJUnit.rule() - - @Inject - lateinit var context: Context - - @Inject - lateinit var storyProgressTestHelper: StoryProgressTestHelper - - @Inject - lateinit var topicController: TopicController - - @Inject - lateinit var persistentCacheStoreFactory: PersistentCacheStore.Factory - - @Inject - lateinit var testCoroutineDispatchers: TestCoroutineDispatchers - - @Inject - lateinit var fakeOppiaClock: FakeOppiaClock - - @Mock - lateinit var mockTopicObserver: Observer> - - @Captor - lateinit var topicResultCaptor: ArgumentCaptor> - - @Mock - lateinit var mockTopicProgressDatabaseObserver: Observer> - - @Captor - lateinit var topicProgressDatabaseResultCaptor: ArgumentCaptor> + @Inject lateinit var context: Context + @Inject lateinit var storyProgressTestHelper: StoryProgressTestHelper + @Inject lateinit var topicController: TopicController + @Inject lateinit var persistentCacheStoreFactory: PersistentCacheStore.Factory + @Inject lateinit var testCoroutineDispatchers: TestCoroutineDispatchers + @Inject lateinit var fakeOppiaClock: FakeOppiaClock + @Inject lateinit var monitorFactory: DataProviderTestMonitor.Factory private val profileId0: ProfileId by lazy { ProfileId.newBuilder().setInternalId(0).build() } private val profileId1: ProfileId by lazy { ProfileId.newBuilder().setInternalId(1).build() } @@ -1652,11 +1615,8 @@ class StoryProgressTestHelperTest { assertThat(exp2.isStartedNotCompleted()).isFalse() } - private fun getTopic(profileId: ProfileId, topicId: String): Topic { - return retrieveSuccessfulResult( - topicController.getTopic(profileId, topicId), mockTopicObserver, topicResultCaptor - ) - } + private fun getTopic(profileId: ProfileId, topicId: String): Topic = + monitorFactory.waitForNextSuccessfulResult(topicController.getTopic(profileId, topicId)) private fun Topic.getStory(storyId: String): StorySummary { return storyList.find { it.storyId == storyId } ?: error("Failed to find story: $storyId") @@ -1664,8 +1624,6 @@ class StoryProgressTestHelperTest { private fun Topic.isNotStarted(): Boolean = storyList.all { it.isNotStarted() } - private fun Topic.isStartedNotCompleted(): Boolean = storyList.any { it.isStartedNotCompleted() } - private fun Topic.isInProgressSaved(): Boolean = storyList.any { it.isInProgressSaved() } private fun Topic.isInProgressNotSaved(): Boolean = storyList.any { it.isInProgressNotSaved() } @@ -1680,9 +1638,6 @@ class StoryProgressTestHelperTest { private fun StorySummary.isNotStarted(): Boolean = chapterList.all { it.isNotStarted() } - private fun StorySummary.isStartedNotCompleted(): Boolean = - chapterList.any { it.isStartedNotCompleted() } - private fun StorySummary.isInProgressSaved(): Boolean = chapterList.any { it.isInProgressSaved() } private fun StorySummary.isInProgressNotSaved(): Boolean = @@ -1714,9 +1669,7 @@ class StoryProgressTestHelperTest { TopicProgressDatabase.getDefaultInstance(), profileId ) - return retrieveSuccessfulResult( - persistentCacheStore, mockTopicProgressDatabaseObserver, topicProgressDatabaseResultCaptor - ) + return monitorFactory.waitForNextSuccessfulResult(persistentCacheStore) } private fun TopicProgressDatabase.getTopicProgress(topicId: String): TopicProgress { @@ -1731,25 +1684,6 @@ class StoryProgressTestHelperTest { return chapterProgressMap[expId] ?: error("Failed to get progress for chapter: $expId") } - private fun retrieveSuccessfulResult( - dataProvider: DataProvider, - mockObserver: Observer>, - mockResultCaptor: ArgumentCaptor> - ): T { - val requestLiveData = dataProvider.toLiveData() - reset(mockObserver) - requestLiveData.observeForever(mockObserver) - - // Provide time for the topic retrieval to complete. - testCoroutineDispatchers.runCurrent() - - verify(mockObserver, atLeastOnce()).onChanged(mockResultCaptor.capture()) - requestLiveData.removeObserver(mockObserver) - val result = mockResultCaptor.value - assertThat(result.isSuccess()).isTrue() - return result.getOrThrow() - } - // TODO(#89): Move this to a common test application component. @Module class TestModule { diff --git a/testing/src/test/java/org/oppia/android/testing/threading/CoroutineExecutorServiceTest.kt b/testing/src/test/java/org/oppia/android/testing/threading/CoroutineExecutorServiceTest.kt index 68e6f0b6188..a64af7b45c7 100644 --- a/testing/src/test/java/org/oppia/android/testing/threading/CoroutineExecutorServiceTest.kt +++ b/testing/src/test/java/org/oppia/android/testing/threading/CoroutineExecutorServiceTest.kt @@ -33,7 +33,6 @@ import org.oppia.android.testing.assertThrows import org.oppia.android.testing.robolectric.RobolectricModule import org.oppia.android.testing.time.FakeOppiaClockModule import org.oppia.android.testing.time.FakeSystemClock -import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.threading.BackgroundDispatcher import org.robolectric.annotation.LooperMode import java.util.concurrent.Callable @@ -46,6 +45,8 @@ import java.util.concurrent.TimeUnit import java.util.concurrent.TimeoutException import javax.inject.Inject import javax.inject.Singleton +import org.oppia.android.testing.data.AsyncResultSubject +import org.oppia.android.util.data.AsyncResult /** * Tests for [CoroutineExecutorService]. NOTE: significant care should be taken when modifying these @@ -339,20 +340,19 @@ class CoroutineExecutorServiceTest { val getResult = testDispatcherScope.async { try { - AsyncResult.success(callableFuture.get(/* timeout= */ 1, TimeUnit.SECONDS)) + AsyncResult.Success(callableFuture.get(/* timeout= */ 1, TimeUnit.SECONDS)) } catch (e: ExecutionException) { - AsyncResult.failed(e) + AsyncResult.Failure(e) } } testDispatcher.runUntilIdle() // The getter should return since the task has finished. assertThat(getResult.isCompleted).isTrue() - assertThat(getResult.getCompleted().isFailure()).isTrue() - assertThat(getResult.getCompleted().getErrorOrNull()) - .isInstanceOf(ExecutionException::class.java) - assertThat(getResult.getCompleted().getErrorOrNull()?.cause) - .isInstanceOf(TimeoutException::class.java) + AsyncResultSubject.assertThat(getResult.getCompleted()).isFailureThat().apply { + isInstanceOf(ExecutionException::class.java) + isInstanceOf(TimeoutException::class.java) + } } @Test diff --git a/utility/src/main/java/org/oppia/android/util/data/AsyncResult.kt b/utility/src/main/java/org/oppia/android/util/data/AsyncResult.kt index 7889986d6a1..75dbf8260a2 100644 --- a/utility/src/main/java/org/oppia/android/util/data/AsyncResult.kt +++ b/utility/src/main/java/org/oppia/android/util/data/AsyncResult.kt @@ -2,69 +2,16 @@ package org.oppia.android.util.data import android.os.SystemClock +// TODO: update documentation to explain how to create these, and how to check the result type. /** Represents the result from a single asynchronous function. */ -class AsyncResult private constructor( - private val status: Status, - private val resultTimeMillis: Long, - private val value: T? = null, - private val error: Throwable? = null -) { - /** Represents the status of an asynchronous result. */ - enum class Status { - /** Indicates that the asynchronous operation is not yet completed. */ - PENDING, - - /** Indicates that the asynchronous operation completed successfully and has a result. */ - SUCCEEDED, - - /** Indicates that the asynchronous operation failed and has an error. */ - FAILED - } - - /** Returns whether this result is still pending. */ - fun isPending(): Boolean { - return status == Status.PENDING - } - - /** Returns whether this result has completed successfully. */ - fun isSuccess(): Boolean { - return status == Status.SUCCEEDED - } - - /** Returns whether this result has completed unsuccessfully. */ - fun isFailure(): Boolean { - return status == Status.FAILED - } - - /** Returns whether this result has completed (successfully or unsuccessfully). */ - fun isCompleted(): Boolean { - return isSuccess() || isFailure() - } +sealed class AsyncResult { + protected abstract val resultTimeMillis: Long /** Returns whether this result is newer than, or the same age as, the specified result of the same type. */ fun isNewerThanOrSameAgeAs(otherResult: AsyncResult): Boolean { return resultTimeMillis >= otherResult.resultTimeMillis } - /** Returns the value of the result if it succeeded, otherwise the specified default value. */ - fun getOrDefault(defaultValue: T): T { - return if (isSuccess()) value!! else defaultValue - } - - /** - * Returns the value of the result if it succeeded, otherwise throws the underlying exception. Throws if this result - * is not yet completed. - */ - fun getOrThrow(): T { - check(isCompleted()) { "Result is not yet completed." } - if (isSuccess()) return value!! else throw error!! - } - - /** Returns the underlying exception if this result failed, otherwise null. */ - fun getErrorOrNull(): Throwable? { - return if (isFailure()) error else null - } - /** * Returns a version of this result that retains its pending and failed states, but transforms its success state * according to the specified transformation function. @@ -75,9 +22,7 @@ class AsyncResult private constructor( * Note also that the specified transformation function should have no side effects, and be non-blocking. */ fun transform(transformFunction: (T) -> O): AsyncResult { - return transformWithResult { value -> - success(transformFunction(value)) - } + return transformWithResult { value -> Success(transformFunction(value)) } } /** @@ -108,9 +53,7 @@ class AsyncResult private constructor( combineFunction: (T, T2) -> O ): AsyncResult { return transformWithResult { value1 -> - otherResult.transformWithResult { value2 -> - success(combineFunction(value1, value2)) - } + otherResult.transformWithResult { value2 -> Success(combineFunction(value1, value2)) } } } @@ -131,75 +74,36 @@ class AsyncResult private constructor( } private fun transformWithResult(transformFunction: (T) -> AsyncResult): AsyncResult { - return when (status) { - Status.PENDING -> pending() - Status.FAILED -> failed(ChainedFailureException(error!!)) - Status.SUCCEEDED -> transformFunction(value!!) + return when (this) { + is Pending -> Pending() + is Success -> transformFunction(value) + is Failure -> Failure(ChainedFailureException(error)) } } private suspend fun transformWithResultAsync( transformFunction: suspend (T) -> AsyncResult ): AsyncResult { - return when (status) { - Status.PENDING -> pending() - Status.FAILED -> failed(ChainedFailureException(error!!)) - Status.SUCCEEDED -> transformFunction(value!!) - } - } - - override fun equals(other: Any?): Boolean { - if (this === other) { - return true - } - if (other == null || other.javaClass != javaClass) { - return false - } - val otherResult = other as AsyncResult<*> - return otherResult.status == status && otherResult.error == error && otherResult.value == value - } - - override fun hashCode(): Int { - // Automatically generated hashCode() function that has parity with equals(). - var result = status.hashCode() - result = 31 * result + (value?.hashCode() ?: 0) - result = 31 * result + (error?.hashCode() ?: 0) - return result - } - - override fun toString(): String { - return when (status) { - Status.PENDING -> "AsyncResult[status=PENDING]" - Status.FAILED -> "AsyncResult[status=FAILED, error=$error]" - Status.SUCCEEDED -> "AsyncResult[status=SUCCESS, value=$value]" + return when (this) { + is Pending -> Pending() + is Success -> transformFunction(value) + is Failure -> Failure(ChainedFailureException(error)) } } - companion object { - /** Returns a pending result. */ - fun pending(): AsyncResult { - return AsyncResult(status = Status.PENDING, resultTimeMillis = SystemClock.uptimeMillis()) - } + /** A chained exception to preserve failure stacktraces for [transform] and [transformAsync]. */ + class ChainedFailureException(cause: Throwable) : Exception(cause) - /** Returns a successful result with the specified payload. */ - fun success(value: T): AsyncResult { - return AsyncResult( - status = Status.SUCCEEDED, - resultTimeMillis = SystemClock.uptimeMillis(), - value = value - ) - } + data class Pending( + override val resultTimeMillis: Long = SystemClock.uptimeMillis() + ) : AsyncResult() - /** Returns a failed result with the specified error. */ - fun failed(error: Throwable): AsyncResult { - return AsyncResult( - status = Status.FAILED, - resultTimeMillis = SystemClock.uptimeMillis(), - error = error - ) - } + data class Success( + val value: T, override val resultTimeMillis: Long = SystemClock.uptimeMillis() + ) : AsyncResult() { } - /** A chained exception to preserve failure stacktraces for [transform] and [transformAsync]. */ - class ChainedFailureException(cause: Throwable) : Exception(cause) + data class Failure( + val error: Throwable, override val resultTimeMillis: Long = SystemClock.uptimeMillis() + ) : AsyncResult() } diff --git a/utility/src/main/java/org/oppia/android/util/data/DataProviders.kt b/utility/src/main/java/org/oppia/android/util/data/DataProviders.kt index b812a442013..d53b58fd70c 100644 --- a/utility/src/main/java/org/oppia/android/util/data/DataProviders.kt +++ b/utility/src/main/java/org/oppia/android/util/data/DataProviders.kt @@ -12,6 +12,9 @@ import org.oppia.android.util.threading.BackgroundDispatcher import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicReference import javax.inject.Inject +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.transform /** * Various functions to create or manipulate [DataProvider]s. @@ -50,7 +53,7 @@ class DataProviders @Inject constructor( this@transform.retrieveData().transform(function) } catch (e: Exception) { dataProviders.exceptionLogger.logException(e) - AsyncResult.failed(e) + AsyncResult.Failure(e) } } } @@ -119,7 +122,7 @@ class DataProviders @Inject constructor( this@combineWith.retrieveData().combineWith(dataProvider.retrieveData(), function) } catch (e: Exception) { dataProviders.exceptionLogger.logException(e) - AsyncResult.failed(e) + AsyncResult.Failure(e) } } } @@ -187,10 +190,10 @@ class DataProviders @Inject constructor( override suspend fun retrieveData(): AsyncResult { return try { - AsyncResult.success(loadFromMemory()) + AsyncResult.Success(loadFromMemory()) } catch (e: Exception) { exceptionLogger.logException(e) - AsyncResult.failed(e) + AsyncResult.Failure(e) } } } diff --git a/utility/src/test/java/org/oppia/android/util/data/AsyncResultTest.kt b/utility/src/test/java/org/oppia/android/util/data/AsyncResultTest.kt index 8dcb09ec100..313ef401516 100644 --- a/utility/src/test/java/org/oppia/android/util/data/AsyncResultTest.kt +++ b/utility/src/test/java/org/oppia/android/util/data/AsyncResultTest.kt @@ -21,9 +21,12 @@ import org.oppia.android.testing.time.FakeSystemClock import org.robolectric.annotation.LooperMode import javax.inject.Inject import javax.inject.Singleton -import kotlin.test.assertFailsWith +import org.oppia.android.testing.data.AsyncResultSubject.Companion.assertThat +import org.oppia.android.util.data.AsyncResult.ChainedFailureException /** Tests for [AsyncResult]. */ +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) class AsyncResultTest { @@ -47,184 +50,134 @@ class AsyncResultTest { /* Pending tests. */ - @Test - fun testPendingAsyncResult_isPending() { - val result = AsyncResult.pending() - - assertThat(result.isPending()).isTrue() - } - - @Test - fun testPendingAsyncResult_isNotSuccess() { - val result = AsyncResult.pending() - - assertThat(result.isSuccess()).isFalse() - } - - @Test - fun testPendingAsyncResult_isNotFailure() { - val result = AsyncResult.pending() - - assertThat(result.isFailure()).isFalse() - } - - @Test - fun testPendingAsyncResult_isNotCompleted() { - val result = AsyncResult.pending() - - assertThat(result.isCompleted()).isFalse() - } - - @Test - fun testPendingAsyncResult_getOrDefault_returnsDefault() { - val result = AsyncResult.pending() - - assertThat(result.getOrDefault("default")).isEqualTo("default") - } - - @Test - fun testPendingAsyncResult_getOrThrow_throwsIllegalStateExceptionDueToIncompletion() { - val result = AsyncResult.pending() - - assertFailsWith { result.getOrThrow() } - } - - @Test - fun testPendingAsyncResult_getErrorOrNull_returnsNull() { - val result = AsyncResult.pending() - - assertThat(result.getErrorOrNull()).isNull() - } - @Test fun testPendingAsyncResult_transformed_isStillPending() { - val original = AsyncResult.pending() + val original = AsyncResult.Pending() val transformed = original.transform { 0 } - assertThat(transformed.isPending()).isTrue() + assertThat(transformed).isInstanceOf(AsyncResult.Pending::class.java) } @Test fun testPendingAsyncResult_transformedAsync_isStillPending() { - val original = AsyncResult.pending() + val original = AsyncResult.Pending() - val transformed = original.blockingTransformAsync { AsyncResult.success(0) } + val transformed = original.blockingTransformAsync { AsyncResult.Success(0) } - assertThat(transformed.isPending()).isTrue() + assertThat(transformed).isInstanceOf(AsyncResult.Pending::class.java) } @Test fun testPendingAsyncResult_combinedWithPending_isStillPending() { - val result1 = AsyncResult.pending() - val result2 = AsyncResult.pending() + val result1 = AsyncResult.Pending() + val result2 = AsyncResult.Pending() val combined = result1.combineWith(result2) { _, _ -> 0 } - assertThat(combined.isPending()).isTrue() + assertThat(combined).isInstanceOf(AsyncResult.Pending::class.java) } @Test fun testPendingAsyncResult_combinedWithFailure_isStillPending() { - val result1 = AsyncResult.pending() - val result2 = AsyncResult.failed(RuntimeException()) + val result1 = AsyncResult.Pending() + val result2 = AsyncResult.Failure(RuntimeException()) val combined = result1.combineWith(result2) { _, _ -> 0 } - assertThat(combined.isPending()).isTrue() + assertThat(combined).isInstanceOf(AsyncResult.Pending::class.java) } @Test fun testPendingAsyncResult_combinedWithSuccess_isStillPending() { - val result1 = AsyncResult.pending() - val result2 = AsyncResult.success(1.0f) + val result1 = AsyncResult.Pending() + val result2 = AsyncResult.Success(1.0f) val combined = result1.combineWith(result2) { _, _ -> 0 } - assertThat(combined.isPending()).isTrue() + assertThat(combined).isInstanceOf(AsyncResult.Pending::class.java) } @Test fun testPendingAsyncResult_combinedAsyncWithPending_isStillPending() { - val result1 = AsyncResult.pending() - val result2 = AsyncResult.pending() + val result1 = AsyncResult.Pending() + val result2 = AsyncResult.Pending() - val combined = result1.blockingCombineWithAsync(result2) { _, _ -> AsyncResult.success(0) } + val combined = result1.blockingCombineWithAsync(result2) { _, _ -> AsyncResult.Success(0) } - assertThat(combined.isPending()).isTrue() + assertThat(combined).isInstanceOf(AsyncResult.Pending::class.java) } @Test fun testPendingAsyncResult_combinedAsyncWithFailure_isStillPending() { - val result1 = AsyncResult.pending() - val result2 = AsyncResult.failed(RuntimeException()) + val result1 = AsyncResult.Pending() + val result2 = AsyncResult.Failure(RuntimeException()) - val combined = result1.blockingCombineWithAsync(result2) { _, _ -> AsyncResult.success(0) } + val combined = result1.blockingCombineWithAsync(result2) { _, _ -> AsyncResult.Success(0) } - assertThat(combined.isPending()).isTrue() + assertThat(combined).isInstanceOf(AsyncResult.Pending::class.java) } @Test fun testPendingAsyncResult_combinedAsyncWithSuccess_isStillPending() { - val result1 = AsyncResult.pending() - val result2 = AsyncResult.success(1.0f) + val result1 = AsyncResult.Pending() + val result2 = AsyncResult.Success(1.0f) - val combined = result1.blockingCombineWithAsync(result2) { _, _ -> AsyncResult.success(0) } + val combined = result1.blockingCombineWithAsync(result2) { _, _ -> AsyncResult.Success(0) } - assertThat(combined.isPending()).isTrue() + assertThat(combined).isInstanceOf(AsyncResult.Pending::class.java) } @Test fun testPendingResult_isEqualToAnotherPendingResult() { - val result = AsyncResult.pending() + val result = AsyncResult.Pending() // Two pending results are the same regardless of their types. - assertThat(result).isEqualTo(AsyncResult.pending()) + assertThat(result).isEqualTo(AsyncResult.Pending()) } @Test fun testPendingResult_isNotEqualToFailedResult() { - val result = AsyncResult.pending() + val result = AsyncResult.Pending() - assertThat(result).isNotEqualTo(AsyncResult.failed(UnsupportedOperationException())) + assertThat(result).isNotEqualTo(AsyncResult.Failure(UnsupportedOperationException())) } @Test fun testPendingResult_isNotEqualToSucceededResult() { - val result = AsyncResult.pending() + val result = AsyncResult.Pending() - assertThat(result).isNotEqualTo(AsyncResult.success("Success")) + assertThat(result).isNotEqualTo(AsyncResult.Success("Success")) } @Test fun testPendingResult_hashCode_isEqualToAnotherPendingResult() { - val resultHash = AsyncResult.pending().hashCode() + val resultHash = AsyncResult.Pending().hashCode() // Two pending results are the same regardless of their types. - assertThat(resultHash).isEqualTo(AsyncResult.pending().hashCode()) + assertThat(resultHash).isEqualTo(AsyncResult.Pending().hashCode()) } @Test fun testPendingResult_hashCode_isNotEqualToSucceededResult() { - val resultHash = AsyncResult.pending().hashCode() + val resultHash = AsyncResult.Pending().hashCode() - assertThat(resultHash).isNotEqualTo(AsyncResult.success("Success").hashCode()) + assertThat(resultHash).isNotEqualTo(AsyncResult.Success("Success").hashCode()) } @Test fun testPendingResult_hashCode_isNotEqualToFailedResult() { - val resultHash = AsyncResult.pending().hashCode() + val resultHash = AsyncResult.Pending().hashCode() assertThat(resultHash).isNotEqualTo( - AsyncResult.failed( - UnsupportedOperationException() + AsyncResult.Failure(UnsupportedOperationException() ).hashCode() ) } @Test fun testPendingResult_comparedWithItself_isTheSameAge() { - val result = AsyncResult.pending() + val result = AsyncResult.Pending() val areSameAge = result.isNewerThanOrSameAgeAs(result) @@ -233,8 +186,8 @@ class AsyncResultTest { @Test fun testPendingResult_comparedWithOtherPendingResult_createdAtTheSameTime_areTheSameAge() { - val result1 = AsyncResult.pending() - val result2 = AsyncResult.pending() + val result1 = AsyncResult.Pending() + val result2 = AsyncResult.Pending() val areSameAge = result1.isNewerThanOrSameAgeAs(result2) @@ -243,8 +196,8 @@ class AsyncResultTest { @Test fun testPendingResult_comparedWithSucceededResult_createdAtTheSameTime_areTheSameAge() { - val pendingResult = AsyncResult.pending() - val success = AsyncResult.success("value") + val pendingResult = AsyncResult.Pending() + val success = AsyncResult.Success("value") val areSameAge = pendingResult.isNewerThanOrSameAgeAs(success) @@ -253,8 +206,8 @@ class AsyncResultTest { @Test fun testPendingResult_comparedWithFailedResult_createdAtTheSameTime_areTheSameAge() { - val pendingResult = AsyncResult.pending() - val failure = AsyncResult.failed(RuntimeException()) + val pendingResult = AsyncResult.Pending() + val failure = AsyncResult.Failure(RuntimeException()) val areSameAge = pendingResult.isNewerThanOrSameAgeAs(failure) @@ -263,9 +216,9 @@ class AsyncResultTest { @Test fun testPendingResult_comparedWithOlderPendingResult_isNewer() { - val olderResult = AsyncResult.pending() + val olderResult = AsyncResult.Pending() fakeSystemClock.advanceTime(millis = 10) - val newerResult = AsyncResult.pending() + val newerResult = AsyncResult.Pending() val isNewer = newerResult.isNewerThanOrSameAgeAs(olderResult) @@ -274,9 +227,9 @@ class AsyncResultTest { @Test fun testPendingResult_comparedWithNewerPendingResult_isNotNewer() { - val olderResult = AsyncResult.pending() + val olderResult = AsyncResult.Pending() fakeSystemClock.advanceTime(millis = 10) - val newerResult = AsyncResult.pending() + val newerResult = AsyncResult.Pending() val isNewer = olderResult.isNewerThanOrSameAgeAs(newerResult) @@ -286,267 +239,220 @@ class AsyncResultTest { /* Success tests. */ @Test - fun testSucceededAsyncResult_isNotPending() { - val result = AsyncResult.success("value") - - assertThat(result.isPending()).isFalse() - } - - @Test - fun testSucceededAsyncResult_isSuccess() { - val result = AsyncResult.success("value") - - assertThat(result.isSuccess()).isTrue() - } - - @Test - fun testSucceededAsyncResult_isNotFailure() { - val result = AsyncResult.success("value") - - assertThat(result.isFailure()).isFalse() - } - - @Test - fun testSucceededAsyncResult_isCompleted() { - val result = AsyncResult.success("value") - - assertThat(result.isCompleted()).isTrue() - } - - @Test - fun testSucceededAsyncResult_getOrDefault_returnsValue() { - val result = AsyncResult.success("value") - - assertThat(result.getOrDefault("default")).isEqualTo("value") - } - - @Test - fun testSucceededAsyncResult_getOrThrow_returnsValue() { - val result = AsyncResult.success("value") + fun testSucceededAsyncResult_hasCorrectValue() { + val result = AsyncResult.Success("value") - assertThat(result.getOrThrow()).isEqualTo("value") - } - - @Test - fun testSucceededAsyncResult_getErrorOrNull_returnsNull() { - val result = AsyncResult.success("value") - - assertThat(result.getErrorOrNull()).isNull() + assertThat(result.value).isEqualTo("value") } @Test fun testSucceededAsyncResult_transformed_hasTransformedValue() { - val original = AsyncResult.success("value") + val original = AsyncResult.Success("value") val transformed = original.transform { 0 } - assertThat(transformed.getOrThrow()).isEqualTo(0) + assertThat(transformed).isIntSuccessThat().isEqualTo(0) } @Test fun testSucceededAsyncResult_transformedAsyncPending_isPending() { - val original = AsyncResult.success("value") + val original = AsyncResult.Success("value") - val transformed = original.blockingTransformAsync { AsyncResult.pending() } + val transformed = original.blockingTransformAsync { AsyncResult.Pending() } - assertThat(transformed.isPending()).isTrue() + assertThat(transformed).isInstanceOf(AsyncResult.Pending::class.java) } @Test fun testSucceededAsyncResult_transformedAsyncSuccess_hasTransformedValue() { - val original = AsyncResult.success("value") + val original = AsyncResult.Success("value") - val transformed = original.blockingTransformAsync { AsyncResult.success(0) } + val transformed = original.blockingTransformAsync { AsyncResult.Success(0) } - assertThat(transformed.getOrThrow()).isEqualTo(0) + assertThat(transformed).isIntSuccessThat().isEqualTo(0) } @Test fun testSucceededAsyncResult_transformedAsyncFailed_isFailure() { - val original = AsyncResult.success("value") + val original = AsyncResult.Success("value") val transformed = original.blockingTransformAsync { - AsyncResult.failed(UnsupportedOperationException()) + AsyncResult.Failure(UnsupportedOperationException()) } - // Note that the failure is not chained since the transform function was responsible for 'throwing' it. - assertThat(transformed.getErrorOrNull()).isInstanceOf(UnsupportedOperationException::class.java) + // Note that the failure is not chained since the transform function was responsible for + // 'throwing' it. + assertThat(transformed).isFailureThat().isInstanceOf(UnsupportedOperationException::class.java) } @Test fun testSucceededAsyncResult_combinedWithPending_isPending() { - val result1 = AsyncResult.success("value") - val result2 = AsyncResult.pending() + val result1 = AsyncResult.Success("value") + val result2 = AsyncResult.Pending() val combined = result1.combineWith(result2) { _, _ -> 0 } - assertThat(combined.isPending()).isTrue() + assertThat(combined).isInstanceOf(AsyncResult.Pending::class.java) } @Test fun testSucceededAsyncResult_combinedWithFailure_isFailedWithCorrectChainedFailure() { - val result1 = AsyncResult.success("value") - val result2 = AsyncResult.failed(RuntimeException()) + val result1 = AsyncResult.Success("value") + val result2 = AsyncResult.Failure(RuntimeException()) val combined = result1.combineWith(result2) { _, _ -> 0 } - assertThat(combined.isFailure()).isTrue() - assertThat(combined.getErrorOrNull()).isInstanceOf( - AsyncResult.ChainedFailureException::class.java - ) - assertThat(combined.getErrorOrNull()).hasCauseThat().isInstanceOf(RuntimeException::class.java) + assertThat(combined).isFailureThat().isInstanceOf(ChainedFailureException::class.java) + assertThat(combined).isFailureThat().hasCauseThat().isInstanceOf(RuntimeException::class.java) } @Test fun testSucceededAsyncResult_combinedWithSuccess_hasCombinedSuccessValue() { - val result1 = AsyncResult.success("value") - val result2 = AsyncResult.success(1.0) + val result1 = AsyncResult.Success("value") + val result2 = AsyncResult.Success(1.0) val combined = result1.combineWith(result2) { v1, v2 -> v1 + v2 } - assertThat(combined.getOrThrow()).contains("value") - assertThat(combined.getOrThrow()).contains("1.0") + assertThat(combined).isStringSuccessThat().contains("value") + assertThat(combined).isStringSuccessThat().contains("1.0") } @Test fun testSucceededAsyncResult_combinedAsyncWithPending_isPending() { - val result1 = AsyncResult.success("value") - val result2 = AsyncResult.pending() + val result1 = AsyncResult.Success("value") + val result2 = AsyncResult.Pending() - val combined = result1.blockingCombineWithAsync(result2) { _, _ -> AsyncResult.success(0) } + val combined = result1.blockingCombineWithAsync(result2) { _, _ -> AsyncResult.Success(0) } - assertThat(combined.isPending()).isTrue() + assertThat(combined).isInstanceOf(AsyncResult.Pending::class.java) } @Test fun testSucceededAsyncResult_combinedAsyncWithFailure_isFailedWithCorrectChainedFailure() { - val result1 = AsyncResult.success("value") - val result2 = AsyncResult.failed(RuntimeException()) + val result1 = AsyncResult.Success("value") + val result2 = AsyncResult.Failure(RuntimeException()) - val combined = result1.blockingCombineWithAsync(result2) { _, _ -> AsyncResult.success(0) } + val combined = result1.blockingCombineWithAsync(result2) { _, _ -> AsyncResult.Success(0) } - assertThat(combined.isFailure()).isTrue() - assertThat(combined.getErrorOrNull()).isInstanceOf( - AsyncResult.ChainedFailureException::class.java - ) - assertThat(combined.getErrorOrNull()).hasCauseThat().isInstanceOf( - RuntimeException::class.java - ) + assertThat(combined).isFailureThat().isInstanceOf(ChainedFailureException::class.java) + assertThat(combined).isFailureThat().hasCauseThat().isInstanceOf(RuntimeException::class.java) } @Test fun testSucceededAsyncResult_combinedAsyncWithSuccess_resultPending_isPending() { - val result1 = AsyncResult.success("value") - val result2 = AsyncResult.success(1.0) + val result1 = AsyncResult.Success("value") + val result2 = AsyncResult.Success(1.0) - val combined = result1.blockingCombineWithAsync(result2) { _, _ -> AsyncResult.pending() } + val combined = result1.blockingCombineWithAsync(result2) { _, _ -> + AsyncResult.Pending() + } - assertThat(combined.isPending()).isTrue() + assertThat(combined).isInstanceOf(AsyncResult.Pending::class.java) } @Test fun testSucceededAsyncResult_combinedAsyncWithSuccess_resultFailure_isFailed() { - val result1 = AsyncResult.success("value") - val result2 = AsyncResult.success(1.0) + val result1 = AsyncResult.Success("value") + val result2 = AsyncResult.Success(1.0) val combined = result1.blockingCombineWithAsync(result2) { _, _ -> - AsyncResult.failed(RuntimeException()) + AsyncResult.Failure(RuntimeException()) } - // Note that the failure is not chained since the transform function was responsible for 'throwing' it. - assertThat(combined.isFailure()).isTrue() - assertThat(combined.getErrorOrNull()).isInstanceOf(RuntimeException::class.java) + // Note that the failure is not chained since the transform function was responsible for + // 'throwing' it. + assertThat(combined).isFailureThat().isInstanceOf(RuntimeException::class.java) } @Test fun testSucceededAsyncResult_combinedAsyncWithSuccess_resultSuccess_hasCombinedSuccessValue() { - val result1 = AsyncResult.success("value") - val result2 = AsyncResult.success(1.0) + val result1 = AsyncResult.Success("value") + val result2 = AsyncResult.Success(1.0) val combined = result1.blockingCombineWithAsync(result2) { v1, v2 -> - AsyncResult.success(v1 + v2) + AsyncResult.Success(v1 + v2) } - assertThat(combined.getOrThrow()).contains("value") - assertThat(combined.getOrThrow()).contains("1.0") + assertThat(combined).isStringSuccessThat().contains("value") + assertThat(combined).isStringSuccessThat().contains("1.0") } @Test fun testSucceededResult_isNotEqualToPendingResult() { - val result = AsyncResult.success("Success") + val result = AsyncResult.Success("Success") - assertThat(result).isNotEqualTo(AsyncResult.pending()) + assertThat(result).isNotEqualTo(AsyncResult.Pending()) } @Test fun testSucceededResult_isEqualToSameSucceededResult() { - val result = AsyncResult.success("Success") + val result = AsyncResult.Success("Success") - assertThat(result).isEqualTo(AsyncResult.success("Success")) + assertThat(result).isEqualTo(AsyncResult.Success("Success")) } @Test fun testSucceededResult_isNotEqualToDifferentSucceededResult() { - val result = AsyncResult.success("Success") + val result = AsyncResult.Success("Success") - assertThat(result).isNotEqualTo(AsyncResult.success("Other value")) + assertThat(result).isNotEqualTo(AsyncResult.Success("Other value")) } @Test fun testSucceededResult_isNotEqualToDifferentTypedSucceededResult() { - val result = AsyncResult.success("0") + val result = AsyncResult.Success("0") - assertThat(result).isNotEqualTo(AsyncResult.success(0)) + assertThat(result).isNotEqualTo(AsyncResult.Success(0)) } @Test fun testSucceededResult_isNotEqualToFailedResult() { - val result = AsyncResult.success("Success") + val result = AsyncResult.Success("Success") - assertThat(result).isNotEqualTo(AsyncResult.failed(UnsupportedOperationException())) + assertThat(result).isNotEqualTo(AsyncResult.Failure(UnsupportedOperationException())) } @Test fun testSucceededResult_hashCode_isNotEqualToPendingResult() { - val resultHash = AsyncResult.success("Success").hashCode() + val resultHash = AsyncResult.Success("Success").hashCode() // Two pending results are the same regardless of their types. - assertThat(resultHash).isNotEqualTo(AsyncResult.pending().hashCode()) + assertThat(resultHash).isNotEqualTo(AsyncResult.Pending().hashCode()) } @Test fun testSucceededResult_hashCode_isEqualToSameSucceededResult() { - val resultHash = AsyncResult.success("Success").hashCode() + val resultHash = AsyncResult.Success("Success").hashCode() - assertThat(resultHash).isEqualTo(AsyncResult.success("Success").hashCode()) + assertThat(resultHash).isEqualTo(AsyncResult.Success("Success").hashCode()) } @Test fun testSucceededResult_hashCode_isNotEqualToDifferentSucceededResult() { - val resultHash = AsyncResult.success("Success").hashCode() + val resultHash = AsyncResult.Success("Success").hashCode() - assertThat(resultHash).isNotEqualTo(AsyncResult.success("Other value").hashCode()) + assertThat(resultHash).isNotEqualTo(AsyncResult.Success("Other value").hashCode()) } @Test fun testSucceededResult_hashCode_isNotEqualToDifferentTypedSucceededResult() { - val resultHash = AsyncResult.success("0").hashCode() + val resultHash = AsyncResult.Success("0").hashCode() - assertThat(resultHash).isNotEqualTo(AsyncResult.success(0)) + assertThat(resultHash).isNotEqualTo(AsyncResult.Success(0)) } @Test fun testSucceededResult_hashCode_isNotEqualToFailedResult() { - val resultHash = AsyncResult.success("Success").hashCode() + val resultHash = AsyncResult.Success("Success").hashCode() assertThat(resultHash).isNotEqualTo( - AsyncResult.failed(UnsupportedOperationException()).hashCode() + AsyncResult.Failure(UnsupportedOperationException()).hashCode() ) } @Test fun testSucceededResult_comparedWithItself_isTheSameAge() { - val result = AsyncResult.success("value") + val result = AsyncResult.Success("value") val areSameAge = result.isNewerThanOrSameAgeAs(result) @@ -555,8 +461,8 @@ class AsyncResultTest { @Test fun testSucceededResult_comparedWithPendingResult_createdAtTheSameTime_areTheSameAge() { - val pendingResult = AsyncResult.pending() - val success = AsyncResult.success("value") + val pendingResult = AsyncResult.Pending() + val success = AsyncResult.Success("value") val areSameAge = success.isNewerThanOrSameAgeAs(pendingResult) @@ -565,8 +471,8 @@ class AsyncResultTest { @Test fun testSucceededResult_comparedWithOtherSucceededResult_createdAtTheSameTime_areTheSameAge() { - val result1 = AsyncResult.success("value") - val result2 = AsyncResult.success("value") + val result1 = AsyncResult.Success("value") + val result2 = AsyncResult.Success("value") val areSameAge = result1.isNewerThanOrSameAgeAs(result2) @@ -575,8 +481,8 @@ class AsyncResultTest { @Test fun testSucceededResult_comparedWithFailedResult_createdAtTheSameTime_areTheSameAge() { - val success = AsyncResult.success("value") - val failure = AsyncResult.failed(RuntimeException()) + val success = AsyncResult.Success("value") + val failure = AsyncResult.Failure(RuntimeException()) val areSameAge = success.isNewerThanOrSameAgeAs(failure) @@ -585,9 +491,9 @@ class AsyncResultTest { @Test fun testSucceededResult_comparedWithOlderSucceededResult_isNewer() { - val olderResult = AsyncResult.success("value") + val olderResult = AsyncResult.Success("value") fakeSystemClock.advanceTime(millis = 10) - val newerResult = AsyncResult.success("value") + val newerResult = AsyncResult.Success("value") val isNewer = newerResult.isNewerThanOrSameAgeAs(olderResult) @@ -596,254 +502,233 @@ class AsyncResultTest { @Test fun testSucceededResult_comparedWithNewerSucceededResult_isNotNewer() { - val olderResult = AsyncResult.success("value") + val olderResult = AsyncResult.Success("value") fakeSystemClock.advanceTime(millis = 10) - val newerResult = AsyncResult.success("value") + val newerResult = AsyncResult.Success("value") val isNewer = olderResult.isNewerThanOrSameAgeAs(newerResult) assertThat(isNewer).isFalse() } - /* Failure tests. */ - @Test - fun testFailedAsyncResult_isNotPending() { - val result = AsyncResult.failed(UnsupportedOperationException()) + fun testSuccessfulResult_nullValue_canRetrieveNullValue() { + val result = AsyncResult.Success(null) - assertThat(result.isPending()).isFalse() + assertThat(result.value).isNull() } @Test - fun testFailedAsyncResult_isNotSuccess() { - val result = AsyncResult.failed(UnsupportedOperationException()) + fun testSuccessfulResult_nullValue_transformIntoString_createsResultWithCorrectValue() { + val result1 = AsyncResult.Success(null) - assertThat(result.isSuccess()).isFalse() - } + val result2 = result1.transform { "string" } - @Test - fun testFailedAsyncResult_isFailure() { - val result = AsyncResult.failed(UnsupportedOperationException()) - - assertThat(result.isFailure()).isTrue() + assertThat(result2).isStringSuccessThat().isEqualTo("string") } @Test - fun testFailedAsyncResult_isCompleted() { - val result = AsyncResult.failed(UnsupportedOperationException()) - - assertThat(result.isCompleted()).isTrue() - } + fun testSuccessfulResult_stringValue_transformIntoNull_createsResultWithCorrectValue() { + val result1 = AsyncResult.Success("string") - @Test - fun testFailedAsyncResult_getOrDefault_returnsDefault() { - val result = AsyncResult.failed(UnsupportedOperationException()) + val result2: AsyncResult = result1.transform { null } - assertThat(result.getOrDefault("default")).isEqualTo("default") + assertThat(result2).isSuccessThat().isNull() } @Test - fun testFailedAsyncResult_getOrThrow_throwsFailureException() { - val result = AsyncResult.failed(UnsupportedOperationException()) + fun testSuccessfulResult_combineStringAndNullResult_createsResultWithCorrectValue() { + val result1 = AsyncResult.Success("string") + val result2 = AsyncResult.Success(null) + + val combined = result1.combineWith(result2) { _, _ -> "combined" } - assertFailsWith { result.getOrThrow() } + assertThat(combined).isStringSuccessThat().isEqualTo("combined") } + /* Failure tests. */ + @Test - fun testFailedAsyncResult_getErrorOrNull_returnsFailureException() { - val result = AsyncResult.failed(UnsupportedOperationException()) + fun testFailedAsyncResult_containsFailureException() { + val result = AsyncResult.Failure(UnsupportedOperationException()) - assertThat(result.getErrorOrNull()).isInstanceOf(UnsupportedOperationException::class.java) + assertThat(result.error).isInstanceOf(UnsupportedOperationException::class.java) } @Test fun testFailedAsyncResult_transformed_throwsChainedFailureException_withCorrectRootCause() { - val result = AsyncResult.failed(UnsupportedOperationException()) + val result = AsyncResult.Failure(UnsupportedOperationException()) val transformed = result.transform { 0 } - assertThat(transformed.getErrorOrNull()).isInstanceOf( - AsyncResult.ChainedFailureException::class.java - ) - assertThat(transformed.getErrorOrNull()).hasCauseThat().isInstanceOf( - UnsupportedOperationException::class.java - ) + assertThat(transformed).isFailureThat().isInstanceOf(ChainedFailureException::class.java) + assertThat(transformed).isFailureThat() + .hasCauseThat() + .isInstanceOf(UnsupportedOperationException::class.java) } @Test fun testFailedAsyncResult_transformedAsync_throwsChainedFailureException_withCorrectRootCause() { - val result = AsyncResult.failed(UnsupportedOperationException()) + val result = AsyncResult.Failure(UnsupportedOperationException()) - val transformed = result.blockingTransformAsync { AsyncResult.success(0) } + val transformed = result.blockingTransformAsync { AsyncResult.Success(0) } - assertThat(transformed.getErrorOrNull()).isInstanceOf( - AsyncResult.ChainedFailureException::class.java - ) - assertThat(transformed.getErrorOrNull()).hasCauseThat().isInstanceOf( - UnsupportedOperationException::class.java - ) + assertThat(transformed).isFailureThat().isInstanceOf(ChainedFailureException::class.java) + assertThat(transformed).isFailureThat() + .hasCauseThat() + .isInstanceOf(UnsupportedOperationException::class.java) } @Test fun testFailedAsyncResult_combinedWithPending_isStillChainedFailure() { - val result1 = AsyncResult.failed(UnsupportedOperationException()) - val result2 = AsyncResult.pending() + val result1 = AsyncResult.Failure(UnsupportedOperationException()) + val result2 = AsyncResult.Pending() val combined = result1.combineWith(result2) { _, _ -> 0 } - assertThat(combined.getErrorOrNull()).isInstanceOf( - AsyncResult.ChainedFailureException::class.java - ) - assertThat(combined.getErrorOrNull()).hasCauseThat().isInstanceOf( - UnsupportedOperationException::class.java - ) + assertThat(combined).isFailureThat().isInstanceOf(ChainedFailureException::class.java) + assertThat(combined).isFailureThat() + .hasCauseThat() + .isInstanceOf(UnsupportedOperationException::class.java) } @Test fun testFailedAsyncResult_combinedWithFailure_hasFirstFailureChained() { - val result1 = AsyncResult.failed(UnsupportedOperationException()) - val result2 = AsyncResult.failed(RuntimeException()) + val result1 = AsyncResult.Failure(UnsupportedOperationException()) + val result2 = AsyncResult.Failure(RuntimeException()) val combined = result1.combineWith(result2) { _, _ -> 0 } - assertThat(combined.getErrorOrNull()).isInstanceOf( - AsyncResult.ChainedFailureException::class.java - ) - assertThat(combined.getErrorOrNull()).hasCauseThat().isInstanceOf( - UnsupportedOperationException::class.java - ) + assertThat(combined).isFailureThat().isInstanceOf(ChainedFailureException::class.java) + assertThat(combined).isFailureThat() + .hasCauseThat() + .isInstanceOf(UnsupportedOperationException::class.java) } @Test fun testFailedAsyncResult_combinedWithSuccess_isStillChainedFailure() { - val result1 = AsyncResult.failed(UnsupportedOperationException()) - val result2 = AsyncResult.success(1.0f) + val result1 = AsyncResult.Failure(UnsupportedOperationException()) + val result2 = AsyncResult.Success(1.0f) val combined = result1.combineWith(result2) { _, _ -> 0 } - assertThat(combined.getErrorOrNull()).isInstanceOf( - AsyncResult.ChainedFailureException::class.java - ) - assertThat(combined.getErrorOrNull()).hasCauseThat().isInstanceOf( - UnsupportedOperationException::class.java - ) + assertThat(combined).isFailureThat().isInstanceOf(ChainedFailureException::class.java) + assertThat(combined).isFailureThat() + .hasCauseThat() + .isInstanceOf(UnsupportedOperationException::class.java) } @Test fun testFailedAsyncResult_combinedAsyncWithPending_isStillChainedFailure() { - val result1 = AsyncResult.failed(UnsupportedOperationException()) - val result2 = AsyncResult.pending() + val result1 = AsyncResult.Failure(UnsupportedOperationException()) + val result2 = AsyncResult.Pending() - val combined = result1.blockingCombineWithAsync(result2) { _, _ -> AsyncResult.success(0) } + val combined = result1.blockingCombineWithAsync(result2) { _, _ -> AsyncResult.Success(0) } - assertThat(combined.getErrorOrNull()).isInstanceOf( - AsyncResult.ChainedFailureException::class.java - ) - assertThat(combined.getErrorOrNull()).hasCauseThat().isInstanceOf( - UnsupportedOperationException::class.java - ) + assertThat(combined).isFailureThat().isInstanceOf(ChainedFailureException::class.java) + assertThat(combined).isFailureThat() + .hasCauseThat() + .isInstanceOf(UnsupportedOperationException::class.java) } @Test fun testFailedAsyncResult_combinedAsyncWithFailure_isStillChainedFailure() { - val result1 = AsyncResult.failed(UnsupportedOperationException()) - val result2 = AsyncResult.failed(RuntimeException()) + val result1 = AsyncResult.Failure(UnsupportedOperationException()) + val result2 = AsyncResult.Failure(RuntimeException()) - val combined = result1.blockingCombineWithAsync(result2) { _, _ -> AsyncResult.success(0) } + val combined = result1.blockingCombineWithAsync(result2) { _, _ -> AsyncResult.Success(0) } - assertThat(combined.getErrorOrNull()).isInstanceOf( - AsyncResult.ChainedFailureException::class.java - ) - assertThat(combined.getErrorOrNull()).hasCauseThat().isInstanceOf( - UnsupportedOperationException::class.java - ) + assertThat(combined).isFailureThat().isInstanceOf(ChainedFailureException::class.java) + assertThat(combined).isFailureThat() + .hasCauseThat() + .isInstanceOf(UnsupportedOperationException::class.java) } @Test fun testFailedAsyncResult_combinedAsyncWithSuccess_isStillChainedFailure() { - val result1 = AsyncResult.failed(UnsupportedOperationException()) - val result2 = AsyncResult.success(1.0f) + val result1 = AsyncResult.Failure(UnsupportedOperationException()) + val result2 = AsyncResult.Success(1.0f) - val combined = result1.blockingCombineWithAsync(result2) { _, _ -> AsyncResult.success(0) } + val combined = result1.blockingCombineWithAsync(result2) { _, _ -> AsyncResult.Success(0) } - assertThat(combined.getErrorOrNull()).isInstanceOf( - AsyncResult.ChainedFailureException::class.java - ) - assertThat(combined.getErrorOrNull()).hasCauseThat().isInstanceOf( - UnsupportedOperationException::class.java - ) + assertThat(combined).isFailureThat().isInstanceOf(ChainedFailureException::class.java) + assertThat(combined).isFailureThat() + .hasCauseThat() + .isInstanceOf(UnsupportedOperationException::class.java) } @Test fun testFailedResult_isNotEqualToPendingResult() { - val result = AsyncResult.failed(UnsupportedOperationException("Reason")) + val result = AsyncResult.Failure(UnsupportedOperationException("Reason")) - assertThat(result).isNotEqualTo(AsyncResult.pending()) + assertThat(result).isNotEqualTo(AsyncResult.Pending()) } @Test fun testFailedResult_isNotEqualToSucceededResult() { - val result = AsyncResult.failed(UnsupportedOperationException("Reason")) + val result = AsyncResult.Failure(UnsupportedOperationException("Reason")) - assertThat(result).isNotEqualTo(AsyncResult.success("Success")) + assertThat(result).isNotEqualTo(AsyncResult.Success("Success")) } @Test fun testFailedResult_isEqualToFailedResultWithSameExceptionObject() { val failure = UnsupportedOperationException("Reason") - val result = AsyncResult.failed(failure) + val result = AsyncResult.Failure(failure) - assertThat(result).isEqualTo(AsyncResult.failed(failure)) + assertThat(result).isEqualTo(AsyncResult.Failure(failure)) } @Test fun testFailedResult_isNotEqualToFailedResultWithDifferentInstanceOfSameExceptionType() { - val result = AsyncResult.failed(UnsupportedOperationException("Reason")) + val result = AsyncResult.Failure(UnsupportedOperationException("Reason")) - // Different exceptions have different stack traces, so they can't be equal despite similar constructions. + // Different exceptions have different stack traces, so they can't be equal despite similar + // constructions. assertThat(result).isNotEqualTo( - AsyncResult.failed(UnsupportedOperationException("Reason")) + AsyncResult.Failure(UnsupportedOperationException("Reason")) ) } @Test fun testFailedResult_hashCode_isNotEqualToPendingResult() { - val resultHash = AsyncResult.failed(UnsupportedOperationException("Reason")).hashCode() + val resultHash = AsyncResult.Failure(UnsupportedOperationException("Reason")).hashCode() // Two pending results are the same regardless of their types. - assertThat(resultHash).isNotEqualTo(AsyncResult.pending().hashCode()) + assertThat(resultHash).isNotEqualTo(AsyncResult.Pending().hashCode()) } @Test fun testFailedResult_hashCode_isNotEqualToSucceededResult() { - val resultHash = AsyncResult.failed(UnsupportedOperationException("Reason")).hashCode() + val resultHash = AsyncResult.Failure(UnsupportedOperationException("Reason")).hashCode() - assertThat(resultHash).isNotEqualTo(AsyncResult.success("Success").hashCode()) + assertThat(resultHash).isNotEqualTo(AsyncResult.Success("Success").hashCode()) } @Test fun testFailedResult_hashCode_isEqualToFailedResultWithSameExceptionObject() { val failure = UnsupportedOperationException("Reason") - val resultHash = AsyncResult.failed(failure).hashCode() + val resultHash = AsyncResult.Failure(failure).hashCode() - assertThat(resultHash).isEqualTo(AsyncResult.failed(failure).hashCode()) + assertThat(resultHash).isEqualTo(AsyncResult.Failure(failure).hashCode()) } @Test fun testFailedResult_hashCode_isNotEqualToFailedResultWithDifferentInstanceOfSameExceptionType() { - val resultHash = AsyncResult.failed(UnsupportedOperationException("Reason")).hashCode() + val resultHash = AsyncResult.Failure(UnsupportedOperationException("Reason")).hashCode() - // Different exceptions have different stack traces, so they can't be equal despite similar constructions. + // Different exceptions have different stack traces, so they can't be equal despite similar + // constructions. assertThat(resultHash).isNotEqualTo( - AsyncResult.failed(UnsupportedOperationException("Reason")).hashCode() + AsyncResult.Failure(UnsupportedOperationException("Reason")).hashCode() ) } @Test fun testFailedResult_comparedWithItself_isTheSameAge() { - val result = AsyncResult.failed(RuntimeException()) + val result = AsyncResult.Failure(RuntimeException()) val areSameAge = result.isNewerThanOrSameAgeAs(result) @@ -852,8 +737,8 @@ class AsyncResultTest { @Test fun testFailedResult_comparedWithPendingResult_createdAtTheSameTime_areTheSameAge() { - val failure = AsyncResult.failed(RuntimeException()) - val pendingResult = AsyncResult.pending() + val failure = AsyncResult.Failure(RuntimeException()) + val pendingResult = AsyncResult.Pending() val areSameAge = failure.isNewerThanOrSameAgeAs(pendingResult) @@ -862,8 +747,8 @@ class AsyncResultTest { @Test fun testFailedResult_comparedWithSucceededResult_createdAtTheSameTime_areTheSameAge() { - val failure = AsyncResult.failed(RuntimeException()) - val success = AsyncResult.success("value") + val failure = AsyncResult.Failure(RuntimeException()) + val success = AsyncResult.Success("value") val areSameAge = failure.isNewerThanOrSameAgeAs(success) @@ -872,8 +757,8 @@ class AsyncResultTest { @Test fun testFailedResult_comparedWithOtherFailedResult_createdAtTheSameTime_areTheSameAge() { - val result1 = AsyncResult.failed(RuntimeException()) - val result2 = AsyncResult.failed(RuntimeException()) + val result1 = AsyncResult.Failure(RuntimeException()) + val result2 = AsyncResult.Failure(RuntimeException()) val areSameAge = result1.isNewerThanOrSameAgeAs(result2) @@ -882,9 +767,9 @@ class AsyncResultTest { @Test fun testFailedResult_comparedWithOlderFailedResult_isNewer() { - val olderResult = AsyncResult.failed(RuntimeException()) + val olderResult = AsyncResult.Failure(RuntimeException()) fakeSystemClock.advanceTime(millis = 10) - val newerResult = AsyncResult.failed(RuntimeException()) + val newerResult = AsyncResult.Failure(RuntimeException()) val isNewer = newerResult.isNewerThanOrSameAgeAs(olderResult) @@ -893,9 +778,9 @@ class AsyncResultTest { @Test fun testFailedResult_comparedWithNewerFailedResult_isNotNewer() { - val olderResult = AsyncResult.failed(RuntimeException()) + val olderResult = AsyncResult.Failure(RuntimeException()) fakeSystemClock.advanceTime(millis = 10) - val newerResult = AsyncResult.failed(RuntimeException()) + val newerResult = AsyncResult.Failure(RuntimeException()) val isNewer = olderResult.isNewerThanOrSameAgeAs(newerResult) diff --git a/utility/src/test/java/org/oppia/android/util/data/BUILD.bazel b/utility/src/test/java/org/oppia/android/util/data/BUILD.bazel new file mode 100644 index 00000000000..ba600e1b24d --- /dev/null +++ b/utility/src/test/java/org/oppia/android/util/data/BUILD.bazel @@ -0,0 +1,86 @@ +""" +Tests for lightweight exploration player domain components. +""" + +load("@dagger//:workspace_defs.bzl", "dagger_rules") +load("//:oppia_android_test.bzl", "oppia_android_test") + +oppia_android_test( + name = "AsyncDataSubscriptionManagerTest", + srcs = ["AsyncDataSubscriptionManagerTest.kt"], + custom_package = "org.oppia.android.util.data", + test_class = "org.oppia.android.util.data.AsyncDataSubscriptionManagerTest", + test_manifest = "//utility:test_manifest", + deps = [ + ":dagger", + "//testing", + "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module", + "//testing/src/main/java/org/oppia/android/testing/threading:test_module", + "//third_party:androidx_test_ext_junit", + "//third_party:com_google_truth_truth", + "//third_party:org_mockito_mockito-core", + "//third_party:org_robolectric_robolectric", + "//third_party:robolectric_android-all", + ], +) + +oppia_android_test( + name = "AsyncResultTest", + srcs = ["AsyncResultTest.kt"], + custom_package = "org.oppia.android.util.data", + test_class = "org.oppia.android.util.data.AsyncResultTest", + test_manifest = "//utility:test_manifest", + deps = [ + ":dagger", + "//testing", + "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module", + "//testing/src/main/java/org/oppia/android/testing/threading:test_module", + "//testing/src/main/java/org/oppia/android/testing/time:test_module", + "//third_party:androidx_test_ext_junit", + "//third_party:com_google_truth_truth", + "//third_party:org_robolectric_robolectric", + "//third_party:robolectric_android-all", + "//utility/src/main/java/org/oppia/android/util/data:async_result", + ], +) + +oppia_android_test( + name = "DataProvidersTest", + srcs = ["DataProvidersTest.kt"], + custom_package = "org.oppia.android.util.data", + test_class = "org.oppia.android.util.data.DataProvidersTest", + test_manifest = "//utility:test_manifest", + deps = [ + ":dagger", + "//testing", + "//testing/src/main/java/org/oppia/android/testing/data:async_result_subject", + "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module", + "//testing/src/main/java/org/oppia/android/testing/threading:test_module", + "//testing/src/main/java/org/oppia/android/testing/time:test_module", + "//third_party:androidx_test_ext_junit", + "//third_party:com_google_truth_truth", + "//third_party:org_robolectric_robolectric", + "//third_party:robolectric_android-all", + ], +) + +oppia_android_test( + name = "InMemoryBlockingCacheTest", + srcs = ["InMemoryBlockingCacheTest.kt"], + custom_package = "org.oppia.android.util.data", + test_class = "org.oppia.android.util.data.InMemoryBlockingCacheTest", + test_manifest = "//utility:test_manifest", + deps = [ + ":dagger", + "//testing", + "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module", + "//testing/src/main/java/org/oppia/android/testing/threading:test_module", + "//testing/src/main/java/org/oppia/android/testing/time:test_module", + "//third_party:androidx_test_ext_junit", + "//third_party:com_google_truth_truth", + "//third_party:org_robolectric_robolectric", + "//third_party:robolectric_android-all", + ], +) + +dagger_rules() diff --git a/utility/src/test/java/org/oppia/android/util/data/DataProvidersTest.kt b/utility/src/test/java/org/oppia/android/util/data/DataProvidersTest.kt index f15d1d20ad3..7d0ad8eef7f 100644 --- a/utility/src/test/java/org/oppia/android/util/data/DataProvidersTest.kt +++ b/utility/src/test/java/org/oppia/android/util/data/DataProvidersTest.kt @@ -5,7 +5,11 @@ import android.content.Context import androidx.lifecycle.Observer import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.FailureMetadata +import com.google.common.truth.Subject +import com.google.common.truth.Truth.assertAbout import com.google.common.truth.Truth.assertThat +import com.google.common.truth.Truth.assertWithMessage import dagger.BindsInstance import dagger.Component import dagger.Module @@ -43,6 +47,8 @@ import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode import javax.inject.Inject import javax.inject.Singleton +import org.oppia.android.testing.data.AsyncResultSubject +import org.oppia.android.testing.data.AsyncResultSubject.Companion.assertThat private const val BASE_PROVIDER_ID_0 = "base_id_0" private const val BASE_PROVIDER_ID_1 = "base_id_1" @@ -61,6 +67,8 @@ private const val COMBINED_STR_VALUE_21 = "At least I thought I was. Now I'm not private const val COMBINED_STR_VALUE_02 = "I used to be indecisive. At least I thought I was." /** Tests for [DataProviders], [DataProvider]s, and [AsyncDataSubscriptionManager]. */ +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) @Config(application = DataProvidersTest.TestApplication::class) @@ -126,7 +134,7 @@ class DataProvidersTest { override suspend fun retrieveData(): AsyncResult { hasRetrieveBeenCalled = true - return AsyncResult.pending() + return AsyncResult.Pending() } } @@ -145,7 +153,7 @@ class DataProvidersTest { override suspend fun retrieveData(): AsyncResult { hasRetrieveBeenCalled = true - return AsyncResult.pending() + return AsyncResult.Pending() } } @@ -160,15 +168,14 @@ class DataProvidersTest { val simpleDataProvider = object : DataProvider(context) { override fun getId(): Any = "simple_data_provider" - override suspend fun retrieveData(): AsyncResult = AsyncResult.success(123) + override suspend fun retrieveData(): AsyncResult = AsyncResult.Success(123) } simpleDataProvider.toLiveData().observeForever(mockIntLiveDataObserver) testCoroutineDispatchers.advanceUntilIdle() verify(mockIntLiveDataObserver).onChanged(intResultCaptor.capture()) - assertThat(intResultCaptor.value.isSuccess()).isTrue() - assertThat(intResultCaptor.value.getOrThrow()).isEqualTo(123) + assertThat(intResultCaptor.value).isIntSuccessThat().isEqualTo(123) } @Test @@ -177,7 +184,7 @@ class DataProvidersTest { val simpleDataProvider = object : DataProvider(context) { override fun getId(): Any = "simple_data_provider" - override suspend fun retrieveData(): AsyncResult = AsyncResult.success(providerValue) + override suspend fun retrieveData(): AsyncResult = AsyncResult.Success(providerValue) } simpleDataProvider.toLiveData().observeForever(mockIntLiveDataObserver) @@ -186,8 +193,7 @@ class DataProvidersTest { testCoroutineDispatchers.advanceUntilIdle() verify(mockIntLiveDataObserver).onChanged(intResultCaptor.capture()) - assertThat(intResultCaptor.value.isSuccess()).isTrue() - assertThat(intResultCaptor.value.getOrThrow()).isEqualTo(456) + assertThat(intResultCaptor.value).isIntSuccessThat().isEqualTo(456) } @Test @@ -196,7 +202,7 @@ class DataProvidersTest { val simpleDataProvider = object : DataProvider(context) { override fun getId(): Any = "simple_data_provider" - override suspend fun retrieveData(): AsyncResult = AsyncResult.success(providerValue) + override suspend fun retrieveData(): AsyncResult = AsyncResult.Success(providerValue) } providerValue = 456 asyncDataSubscriptionManager.notifyChangeAsync(simpleDataProvider.getId()) @@ -208,8 +214,7 @@ class DataProvidersTest { // The newer value should be observed. verify(mockIntLiveDataObserver).onChanged(intResultCaptor.capture()) - assertThat(intResultCaptor.value.isSuccess()).isTrue() - assertThat(intResultCaptor.value.getOrThrow()).isEqualTo(456) + assertThat(intResultCaptor.value).isIntSuccessThat().isEqualTo(456) } @Test @@ -217,7 +222,7 @@ class DataProvidersTest { val simpleDataProvider = object : DataProvider(context) { override fun getId(): Any = "simple_data_provider" - override suspend fun retrieveData(): AsyncResult = AsyncResult.success(123) + override suspend fun retrieveData(): AsyncResult = AsyncResult.Success(123) } simpleDataProvider.toLiveData().observeForever(mockIntLiveDataObserver) testCoroutineDispatchers.advanceUntilIdle() @@ -233,9 +238,9 @@ class DataProvidersTest { @Test fun testConvertToLiveData_multipleUpdatesNoObserver_newObserver_observerReceivesLatest() { - val providerOldResult = AsyncResult.success(123) + val providerOldResult = AsyncResult.Success(123) testCoroutineDispatchers.advanceTimeBy(10) - val providerNewResult = AsyncResult.success(456) + val providerNewResult = AsyncResult.Success(456) val simpleDataProvider = object : DataProvider(context) { var callCount = 0 @@ -249,7 +254,7 @@ class DataProvidersTest { return when (++callCount) { 1 -> providerNewResult 2 -> providerOldResult - else -> AsyncResult.failed(AssertionError("Invalid test case")) + else -> AsyncResult.Failure(AssertionError("Invalid test case")) } } } @@ -262,8 +267,7 @@ class DataProvidersTest { // The more recent value should be observed despite it being retrieved first. verify(mockIntLiveDataObserver, atLeastOnce()).onChanged(intResultCaptor.capture()) - assertThat(intResultCaptor.value.isSuccess()).isTrue() - assertThat(intResultCaptor.value.getOrThrow()).isEqualTo(456) + assertThat(intResultCaptor.value).isIntSuccessThat().isEqualTo(456) assertThat(simpleDataProvider.callCount).isEqualTo(2) // Sanity check for the test logic itself. } @@ -275,8 +279,7 @@ class DataProvidersTest { testCoroutineDispatchers.advanceUntilIdle() verify(mockStringLiveDataObserver).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isSuccess()).isTrue() - assertThat(stringResultCaptor.value.getOrThrow()).isEqualTo(STR_VALUE_0) + assertThat(stringResultCaptor.value).isStringSuccessThat().isEqualTo(STR_VALUE_0) } @Test @@ -304,8 +307,7 @@ class DataProvidersTest { testCoroutineDispatchers.advanceUntilIdle() verify(mockStringLiveDataObserver).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isSuccess()).isTrue() - assertThat(stringResultCaptor.value.getOrThrow()).isEqualTo(STR_VALUE_1) + assertThat(stringResultCaptor.value).isStringSuccessThat().isEqualTo(STR_VALUE_1) } @Test @@ -322,8 +324,7 @@ class DataProvidersTest { testCoroutineDispatchers.advanceUntilIdle() verify(mockStringLiveDataObserver).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isSuccess()).isTrue() - assertThat(stringResultCaptor.value.getOrThrow()).isEqualTo(STR_VALUE_0) + assertThat(stringResultCaptor.value).isStringSuccessThat().isEqualTo(STR_VALUE_0) } @Test @@ -338,8 +339,7 @@ class DataProvidersTest { testCoroutineDispatchers.advanceUntilIdle() verify(mockStringLiveDataObserver, atLeastOnce()).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isSuccess()).isTrue() - assertThat(stringResultCaptor.value.getOrThrow()).isEqualTo(STR_VALUE_1) + assertThat(stringResultCaptor.value).isStringSuccessThat().isEqualTo(STR_VALUE_1) } @Test @@ -358,8 +358,7 @@ class DataProvidersTest { // The first value should be observed since a completely different provider was notified. verify(mockStringLiveDataObserver, atLeastOnce()).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isSuccess()).isTrue() - assertThat(stringResultCaptor.value.getOrThrow()).isEqualTo(STR_VALUE_0) + assertThat(stringResultCaptor.value).isStringSuccessThat().isEqualTo(STR_VALUE_0) } @Test @@ -407,31 +406,29 @@ class DataProvidersTest { testCoroutineDispatchers.advanceUntilIdle() verify(mockStringLiveDataObserver).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isFailure()).isTrue() - assertThat(stringResultCaptor.value.getErrorOrNull()).isInstanceOf( - IllegalStateException::class.java - ) + assertThat(stringResultCaptor.value) + .isFailureThat() + .isInstanceOf(IllegalStateException::class.java) } @Test fun testAsyncInMemoryDataProvider_toLiveData_deliversInMemoryValue() { val dataProvider = dataProviders.createInMemoryDataProviderAsync(BASE_PROVIDER_ID_0) { - AsyncResult.success(STR_VALUE_0) + AsyncResult.Success(STR_VALUE_0) } dataProvider.toLiveData().observeForever(mockStringLiveDataObserver) testCoroutineDispatchers.advanceUntilIdle() verify(mockStringLiveDataObserver).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isSuccess()).isTrue() - assertThat(stringResultCaptor.value.getOrThrow()).isEqualTo(STR_VALUE_0) + assertThat(stringResultCaptor.value).isStringSuccessThat().isEqualTo(STR_VALUE_0) } @Test fun testAsyncInMemoryDataProvider_toLiveData_withChangedValue_beforeReg_deliversSecondValue() { inMemoryCachedStr = STR_VALUE_0 val dataProvider = dataProviders.createInMemoryDataProviderAsync(BASE_PROVIDER_ID_0) { - AsyncResult.success(inMemoryCachedStr!!) + AsyncResult.Success(inMemoryCachedStr!!) } inMemoryCachedStr = STR_VALUE_1 @@ -439,15 +436,14 @@ class DataProvidersTest { testCoroutineDispatchers.advanceUntilIdle() verify(mockStringLiveDataObserver).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isSuccess()).isTrue() - assertThat(stringResultCaptor.value.getOrThrow()).isEqualTo(STR_VALUE_1) + assertThat(stringResultCaptor.value).isStringSuccessThat().isEqualTo(STR_VALUE_1) } @Test fun testAsyncInMemoryDataProvider_toLiveData_withChangedValue_afterReg_deliversFirstValue() { inMemoryCachedStr = STR_VALUE_0 val dataProvider = dataProviders.createInMemoryDataProviderAsync(BASE_PROVIDER_ID_0) { - AsyncResult.success(inMemoryCachedStr!!) + AsyncResult.Success(inMemoryCachedStr!!) } // Ensure the initial state is sent before changing the cache. @@ -458,15 +454,14 @@ class DataProvidersTest { testCoroutineDispatchers.advanceUntilIdle() verify(mockStringLiveDataObserver).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isSuccess()).isTrue() - assertThat(stringResultCaptor.value.getOrThrow()).isEqualTo(STR_VALUE_0) + assertThat(stringResultCaptor.value).isStringSuccessThat().isEqualTo(STR_VALUE_0) } @Test fun testAsyncInMemoryDataProvider_changedValueAfterReg_notified_deliversValueTwo() { inMemoryCachedStr = STR_VALUE_0 val dataProvider = dataProviders.createInMemoryDataProviderAsync(BASE_PROVIDER_ID_0) { - AsyncResult.success(inMemoryCachedStr!!) + AsyncResult.Success(inMemoryCachedStr!!) } dataProvider.toLiveData().observeForever(mockStringLiveDataObserver) @@ -475,8 +470,7 @@ class DataProvidersTest { testCoroutineDispatchers.advanceUntilIdle() verify(mockStringLiveDataObserver, atLeastOnce()).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isSuccess()).isTrue() - assertThat(stringResultCaptor.value.getOrThrow()).isEqualTo(STR_VALUE_1) + assertThat(stringResultCaptor.value).isStringSuccessThat().isEqualTo(STR_VALUE_1) } @Test @@ -484,7 +478,7 @@ class DataProvidersTest { // Ensure the suspend operation is initially blocked. val blockingOperation = backgroundCoroutineScope.async { STR_VALUE_0 } val dataProvider = dataProviders.createInMemoryDataProviderAsync(BASE_PROVIDER_ID_0) { - AsyncResult.success(blockingOperation.await()) + AsyncResult.Success(blockingOperation.await()) } dataProvider.toLiveData().observeForever(mockStringLiveDataObserver) @@ -498,7 +492,7 @@ class DataProvidersTest { // Ensure the suspend operation is initially blocked. val blockingOperation = backgroundCoroutineScope.async { STR_VALUE_0 } val dataProvider = dataProviders.createInMemoryDataProviderAsync(BASE_PROVIDER_ID_0) { - AsyncResult.success(blockingOperation.await()) + AsyncResult.Success(blockingOperation.await()) } // Start observing the provider, then complete its suspend function. @@ -509,8 +503,7 @@ class DataProvidersTest { // The provider will deliver a value immediately when the suspend function completes (no // additional notification is needed). verify(mockStringLiveDataObserver).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isSuccess()).isTrue() - assertThat(stringResultCaptor.value.getOrThrow()).isEqualTo(STR_VALUE_0) + assertThat(stringResultCaptor.value).isStringSuccessThat().isEqualTo(STR_VALUE_0) } @Test @@ -518,7 +511,7 @@ class DataProvidersTest { var fakeLoadMemoryCallbackCalled = false val fakeLoadMemoryCallback: suspend () -> AsyncResult = { fakeLoadMemoryCallbackCalled = true - AsyncResult.success(STR_VALUE_0) + AsyncResult.Success(STR_VALUE_0) } val dataProvider = dataProviders.createInMemoryDataProviderAsync(BASE_PROVIDER_ID_0, fakeLoadMemoryCallback) @@ -535,7 +528,7 @@ class DataProvidersTest { var fakeLoadMemoryCallbackCalled = false val fakeLoadMemoryCallback: suspend () -> AsyncResult = { fakeLoadMemoryCallbackCalled = true - AsyncResult.success(STR_VALUE_0) + AsyncResult.Success(STR_VALUE_0) } val dataProvider = dataProviders.createInMemoryDataProviderAsync(BASE_PROVIDER_ID_0, fakeLoadMemoryCallback) @@ -555,7 +548,7 @@ class DataProvidersTest { testCoroutineDispatchers.advanceUntilIdle() verify(mockStringLiveDataObserver).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isPending()).isTrue() + assertThat(stringResultCaptor.value).isPending() } @Test @@ -567,10 +560,9 @@ class DataProvidersTest { testCoroutineDispatchers.advanceUntilIdle() verify(mockStringLiveDataObserver).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isFailure()).isTrue() - assertThat(stringResultCaptor.value.getErrorOrNull()).isInstanceOf( - IllegalStateException::class.java - ) + assertThat(stringResultCaptor.value) + .isFailureThat() + .isInstanceOf(IllegalStateException::class.java) } @Test @@ -582,8 +574,7 @@ class DataProvidersTest { testCoroutineDispatchers.advanceUntilIdle() verify(mockIntLiveDataObserver).onChanged(intResultCaptor.capture()) - assertThat(intResultCaptor.value.isSuccess()).isTrue() - assertThat(intResultCaptor.value.getOrThrow()).isEqualTo(INT_XFORMED_STR_VALUE_0) + assertThat(intResultCaptor.value).isIntSuccessThat().isEqualTo(INT_XFORMED_STR_VALUE_0) } @Test @@ -600,8 +591,7 @@ class DataProvidersTest { // Notifying the base results in observers of the transformed provider also being called. verify(mockIntLiveDataObserver, atLeastOnce()).onChanged(intResultCaptor.capture()) - assertThat(intResultCaptor.value.isSuccess()).isTrue() - assertThat(intResultCaptor.value.getOrThrow()).isEqualTo(INT_XFORMED_STR_VALUE_1) + assertThat(intResultCaptor.value).isIntSuccessThat().isEqualTo(INT_XFORMED_STR_VALUE_1) } @Test @@ -618,8 +608,7 @@ class DataProvidersTest { // Notifying the transformed provider has the same result as notifying the base provider. verify(mockIntLiveDataObserver, atLeastOnce()).onChanged(intResultCaptor.capture()) - assertThat(intResultCaptor.value.isSuccess()).isTrue() - assertThat(intResultCaptor.value.getOrThrow()).isEqualTo(INT_XFORMED_STR_VALUE_1) + assertThat(intResultCaptor.value).isIntSuccessThat().isEqualTo(INT_XFORMED_STR_VALUE_1) } @Test @@ -638,8 +627,7 @@ class DataProvidersTest { // Having a transformed data provider with an observer does not change the base's notification // behavior. verify(mockStringLiveDataObserver, atLeastOnce()).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isSuccess()).isTrue() - assertThat(stringResultCaptor.value.getOrThrow()).isEqualTo(STR_VALUE_1) + assertThat(stringResultCaptor.value).isStringSuccessThat().isEqualTo(STR_VALUE_1) } @Test @@ -661,8 +649,7 @@ class DataProvidersTest { // However, notifying that the transformed provider has changed should not affect base // subscriptions even if the base has changed. verify(mockStringLiveDataObserver, atLeastOnce()).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isSuccess()).isTrue() - assertThat(stringResultCaptor.value.getOrThrow()).isEqualTo(STR_VALUE_0) + assertThat(stringResultCaptor.value).isStringSuccessThat().isEqualTo(STR_VALUE_0) } @Test @@ -674,7 +661,7 @@ class DataProvidersTest { testCoroutineDispatchers.advanceUntilIdle() verify(mockIntLiveDataObserver).onChanged(intResultCaptor.capture()) - assertThat(intResultCaptor.value.isPending()).isTrue() + assertThat(intResultCaptor.value).isPending() } @Test @@ -687,12 +674,10 @@ class DataProvidersTest { testCoroutineDispatchers.advanceUntilIdle() verify(mockIntLiveDataObserver).onChanged(intResultCaptor.capture()) - assertThat(intResultCaptor.value.isFailure()).isTrue() - assertThat(intResultCaptor.value.getErrorOrNull()).isInstanceOf( - AsyncResult.ChainedFailureException::class.java - ) - assertThat(intResultCaptor.value.getErrorOrNull()).hasCauseThat() - .isInstanceOf(IllegalStateException::class.java) + assertThat(intResultCaptor.value).isFailureThat().apply { + isInstanceOf(AsyncResult.ChainedFailureException::class.java) + hasCauseThat().isInstanceOf(IllegalStateException::class.java) + } } @Test @@ -778,12 +763,10 @@ class DataProvidersTest { // Note that the exception type here is not chained since the failure occurred in the transform // function. verify(mockIntLiveDataObserver).onChanged(intResultCaptor.capture()) - assertThat(intResultCaptor.value.isFailure()).isTrue() - assertThat(intResultCaptor.value.getErrorOrNull()).isInstanceOf( - IllegalStateException::class.java - ) - assertThat(intResultCaptor.value.getErrorOrNull()).hasMessageThat() - .contains("Transform failure") + assertThat(intResultCaptor.value).isFailureThat().apply { + isInstanceOf(IllegalStateException::class.java) + hasMessageThat().contains("Transform failure") + } } @Test @@ -796,14 +779,11 @@ class DataProvidersTest { testCoroutineDispatchers.advanceUntilIdle() verify(mockIntLiveDataObserver).onChanged(intResultCaptor.capture()) - assertThat(intResultCaptor.value.isFailure()).isTrue() - assertThat(intResultCaptor.value.getErrorOrNull()).isInstanceOf( - AsyncResult.ChainedFailureException::class.java - ) - assertThat(intResultCaptor.value.getErrorOrNull()).hasCauseThat() - .isInstanceOf(IllegalStateException::class.java) - assertThat(intResultCaptor.value.getErrorOrNull()).hasCauseThat().hasMessageThat() - .contains("Base failure") + assertThat(intResultCaptor.value).isFailureThat().apply { + isInstanceOf(AsyncResult.ChainedFailureException::class.java) + hasCauseThat().isInstanceOf(IllegalStateException::class.java) + hasCauseThat().hasMessageThat().contains("Base failure") + } } @Test @@ -817,8 +797,7 @@ class DataProvidersTest { testCoroutineDispatchers.advanceUntilIdle() verify(mockIntLiveDataObserver).onChanged(intResultCaptor.capture()) - assertThat(intResultCaptor.value.isSuccess()).isTrue() - assertThat(intResultCaptor.value.getOrThrow()).isEqualTo(INT_XFORMED_STR_VALUE_0) + assertThat(intResultCaptor.value).isIntSuccessThat().isEqualTo(INT_XFORMED_STR_VALUE_0) } @Test @@ -837,8 +816,7 @@ class DataProvidersTest { // Notifying the base results in observers of the transformed provider also being called. verify(mockIntLiveDataObserver, atLeastOnce()).onChanged(intResultCaptor.capture()) - assertThat(intResultCaptor.value.isSuccess()).isTrue() - assertThat(intResultCaptor.value.getOrThrow()).isEqualTo(INT_XFORMED_STR_VALUE_1) + assertThat(intResultCaptor.value).isIntSuccessThat().isEqualTo(INT_XFORMED_STR_VALUE_1) } @Test @@ -857,8 +835,7 @@ class DataProvidersTest { // Notifying the transformed provider has the same result as notifying the base provider. verify(mockIntLiveDataObserver, atLeastOnce()).onChanged(intResultCaptor.capture()) - assertThat(intResultCaptor.value.isSuccess()).isTrue() - assertThat(intResultCaptor.value.getOrThrow()).isEqualTo(INT_XFORMED_STR_VALUE_1) + assertThat(intResultCaptor.value).isIntSuccessThat().isEqualTo(INT_XFORMED_STR_VALUE_1) } @Test @@ -879,8 +856,7 @@ class DataProvidersTest { // Having a transformed data provider with an observer does not change the base's notification // behavior. verify(mockStringLiveDataObserver, atLeastOnce()).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isSuccess()).isTrue() - assertThat(stringResultCaptor.value.getOrThrow()).isEqualTo(STR_VALUE_1) + assertThat(stringResultCaptor.value).isStringSuccessThat().isEqualTo(STR_VALUE_1) } @Test @@ -904,8 +880,7 @@ class DataProvidersTest { // However, notifying that the transformed provider has changed should not affect base // subscriptions even if the base has changed. verify(mockStringLiveDataObserver, atLeastOnce()).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isSuccess()).isTrue() - assertThat(stringResultCaptor.value.getOrThrow()).isEqualTo(STR_VALUE_0) + assertThat(stringResultCaptor.value).isStringSuccessThat().isEqualTo(STR_VALUE_0) } @Test @@ -935,8 +910,7 @@ class DataProvidersTest { // The value should now be delivered since the async function was unblocked. verify(mockIntLiveDataObserver).onChanged(intResultCaptor.capture()) - assertThat(intResultCaptor.value.isSuccess()).isTrue() - assertThat(intResultCaptor.value.getOrThrow()).isEqualTo(INT_XFORMED_STR_VALUE_0) + assertThat(intResultCaptor.value).isIntSuccessThat().isEqualTo(INT_XFORMED_STR_VALUE_0) } @Test @@ -955,15 +929,14 @@ class DataProvidersTest { // Verify that even though the transformed provider is blocked, the base can still properly // publish changes. verify(mockStringLiveDataObserver).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isSuccess()).isTrue() - assertThat(stringResultCaptor.value.getOrThrow()).isEqualTo(STR_VALUE_0) + assertThat(stringResultCaptor.value).isStringSuccessThat().isEqualTo(STR_VALUE_0) } @Test fun testTransformAsync_toLiveData_transformedPending_deliversPending() { val baseProvider = createSuccessfulDataProvider(BASE_PROVIDER_ID_0, STR_VALUE_0) val dataProvider = baseProvider.transformAsync(TRANSFORMED_PROVIDER_ID) { - AsyncResult.pending() + AsyncResult.Pending() } dataProvider.toLiveData().observeForever(mockIntLiveDataObserver) @@ -971,14 +944,14 @@ class DataProvidersTest { // The transformation result yields a pending delivered result. verify(mockIntLiveDataObserver).onChanged(intResultCaptor.capture()) - assertThat(intResultCaptor.value.isPending()).isTrue() + assertThat(intResultCaptor.value).isPending() } @Test fun testTransformAsync_toLiveData_transformedFailure_deliversFailure() { val baseProvider = createSuccessfulDataProvider(BASE_PROVIDER_ID_0, STR_VALUE_0) val dataProvider = baseProvider.transformAsync(TRANSFORMED_PROVIDER_ID) { - AsyncResult.failed(IllegalStateException("Transform failure")) + AsyncResult.Failure(IllegalStateException("Transform failure")) } dataProvider.toLiveData().observeForever(mockIntLiveDataObserver) @@ -987,12 +960,10 @@ class DataProvidersTest { // Note that the failure exception in this case is not chained since the failure occurred in the // transform function. verify(mockIntLiveDataObserver).onChanged(intResultCaptor.capture()) - assertThat(intResultCaptor.value.isFailure()).isTrue() - assertThat(intResultCaptor.value.getErrorOrNull()).isInstanceOf( - IllegalStateException::class.java - ) - assertThat(intResultCaptor.value.getErrorOrNull()).hasMessageThat() - .contains("Transform failure") + assertThat(intResultCaptor.value).isFailureThat().apply { + isInstanceOf(IllegalStateException::class.java) + hasMessageThat().contains("Transform failure") + } } @Test @@ -1007,7 +978,7 @@ class DataProvidersTest { // Since the base provider is pending, so is the transformed provider. verify(mockIntLiveDataObserver).onChanged(intResultCaptor.capture()) - assertThat(intResultCaptor.value.isPending()).isTrue() + assertThat(intResultCaptor.value).isPending() } @Test @@ -1024,14 +995,11 @@ class DataProvidersTest { // Note that the failure exception in this case is not chained since the failure occurred in the // transform function. verify(mockIntLiveDataObserver).onChanged(intResultCaptor.capture()) - assertThat(intResultCaptor.value.isFailure()).isTrue() - assertThat(intResultCaptor.value.getErrorOrNull()).isInstanceOf( - AsyncResult.ChainedFailureException::class.java - ) - assertThat(intResultCaptor.value.getErrorOrNull()).hasCauseThat() - .isInstanceOf(IllegalStateException::class.java) - assertThat(intResultCaptor.value.getErrorOrNull()).hasCauseThat().hasMessageThat() - .contains("Base failure") + assertThat(intResultCaptor.value).isFailureThat().apply { + isInstanceOf(AsyncResult.ChainedFailureException::class.java) + hasCauseThat().isInstanceOf(IllegalStateException::class.java) + hasCauseThat().hasMessageThat().contains("Base failure") + } } @Test @@ -1115,8 +1083,7 @@ class DataProvidersTest { testCoroutineDispatchers.advanceUntilIdle() verify(mockStringLiveDataObserver).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isSuccess()).isTrue() - assertThat(stringResultCaptor.value.getOrThrow()).isEqualTo(COMBINED_STR_VALUE_01) + assertThat(stringResultCaptor.value).isStringSuccessThat().isEqualTo(COMBINED_STR_VALUE_01) } @Test @@ -1137,8 +1104,7 @@ class DataProvidersTest { // Notifying the first base provider results in observers of the combined provider also being // called. verify(mockStringLiveDataObserver, atLeastOnce()).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isSuccess()).isTrue() - assertThat(stringResultCaptor.value.getOrThrow()).isEqualTo(COMBINED_STR_VALUE_21) + assertThat(stringResultCaptor.value).isStringSuccessThat().isEqualTo(COMBINED_STR_VALUE_21) } @Test @@ -1159,8 +1125,7 @@ class DataProvidersTest { // Notifying the combined provider results in observers of the combined provider also being // called. verify(mockStringLiveDataObserver, atLeastOnce()).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isSuccess()).isTrue() - assertThat(stringResultCaptor.value.getOrThrow()).isEqualTo(COMBINED_STR_VALUE_21) + assertThat(stringResultCaptor.value).isStringSuccessThat().isEqualTo(COMBINED_STR_VALUE_21) } @Test @@ -1180,8 +1145,7 @@ class DataProvidersTest { // The combined data provider is irrelevant; the base provider's change should be observed. verify(mockStringLiveDataObserver, atLeastOnce()).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isSuccess()).isTrue() - assertThat(stringResultCaptor.value.getOrThrow()).isEqualTo(STR_VALUE_2) + assertThat(stringResultCaptor.value).isStringSuccessThat().isEqualTo(STR_VALUE_2) } @Test @@ -1205,8 +1169,7 @@ class DataProvidersTest { // Notifying the combined data provider will not trigger observers of the changed provider // becoming aware of the change. verify(mockStringLiveDataObserver, atLeastOnce()).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isSuccess()).isTrue() - assertThat(stringResultCaptor.value.getOrThrow()).isEqualTo(STR_VALUE_0) + assertThat(stringResultCaptor.value).isStringSuccessThat().isEqualTo(STR_VALUE_0) } @Test @@ -1227,8 +1190,7 @@ class DataProvidersTest { // Notifying the second base provider results in observers of the combined provider also being // called. verify(mockStringLiveDataObserver, atLeastOnce()).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isSuccess()).isTrue() - assertThat(stringResultCaptor.value.getOrThrow()).isEqualTo(COMBINED_STR_VALUE_02) + assertThat(stringResultCaptor.value).isStringSuccessThat().isEqualTo(COMBINED_STR_VALUE_02) } @Test @@ -1249,8 +1211,7 @@ class DataProvidersTest { // Notifying the combined provider results in observers of the combined provider also being // called. verify(mockStringLiveDataObserver, atLeastOnce()).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isSuccess()).isTrue() - assertThat(stringResultCaptor.value.getOrThrow()).isEqualTo(COMBINED_STR_VALUE_02) + assertThat(stringResultCaptor.value).isStringSuccessThat().isEqualTo(COMBINED_STR_VALUE_02) } @Test @@ -1270,8 +1231,7 @@ class DataProvidersTest { // The combined data provider is irrelevant; the base provider's change should be observed. verify(mockStringLiveDataObserver, atLeastOnce()).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isSuccess()).isTrue() - assertThat(stringResultCaptor.value.getOrThrow()).isEqualTo(STR_VALUE_2) + assertThat(stringResultCaptor.value).isStringSuccessThat().isEqualTo(STR_VALUE_2) } @Test @@ -1295,8 +1255,7 @@ class DataProvidersTest { // Notifying the combined data provider will not trigger observers of the changed provider // becoming aware of the change. verify(mockStringLiveDataObserver, atLeastOnce()).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isSuccess()).isTrue() - assertThat(stringResultCaptor.value.getOrThrow()).isEqualTo(STR_VALUE_1) + assertThat(stringResultCaptor.value).isStringSuccessThat().isEqualTo(STR_VALUE_1) } @Test @@ -1311,7 +1270,7 @@ class DataProvidersTest { testCoroutineDispatchers.advanceUntilIdle() verify(mockStringLiveDataObserver).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isPending()).isTrue() + assertThat(stringResultCaptor.value).isPending() } @Test @@ -1326,7 +1285,7 @@ class DataProvidersTest { testCoroutineDispatchers.advanceUntilIdle() verify(mockStringLiveDataObserver).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isPending()).isTrue() + assertThat(stringResultCaptor.value).isPending() } @Test @@ -1341,7 +1300,7 @@ class DataProvidersTest { testCoroutineDispatchers.advanceUntilIdle() verify(mockStringLiveDataObserver).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isPending()).isTrue() + assertThat(stringResultCaptor.value).isPending() } @Test @@ -1357,12 +1316,10 @@ class DataProvidersTest { testCoroutineDispatchers.advanceUntilIdle() verify(mockStringLiveDataObserver).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isFailure()).isTrue() - assertThat(stringResultCaptor.value.getErrorOrNull()).isInstanceOf( - AsyncResult.ChainedFailureException::class.java - ) - assertThat(stringResultCaptor.value.getErrorOrNull()).hasCauseThat() - .isInstanceOf(IllegalStateException::class.java) + assertThat(stringResultCaptor.value).isFailureThat().apply { + isInstanceOf(AsyncResult.ChainedFailureException::class.java) + hasCauseThat().isInstanceOf(IllegalStateException::class.java) + } } @Test @@ -1378,12 +1335,10 @@ class DataProvidersTest { testCoroutineDispatchers.advanceUntilIdle() verify(mockStringLiveDataObserver).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isFailure()).isTrue() - assertThat(stringResultCaptor.value.getErrorOrNull()).isInstanceOf( - AsyncResult.ChainedFailureException::class.java - ) - assertThat(stringResultCaptor.value.getErrorOrNull()).hasCauseThat() - .isInstanceOf(IllegalStateException::class.java) + assertThat(stringResultCaptor.value).isFailureThat().apply { + isInstanceOf(AsyncResult.ChainedFailureException::class.java) + hasCauseThat().isInstanceOf(IllegalStateException::class.java) + } } @Test @@ -1400,12 +1355,10 @@ class DataProvidersTest { testCoroutineDispatchers.advanceUntilIdle() verify(mockStringLiveDataObserver).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isFailure()).isTrue() - assertThat(stringResultCaptor.value.getErrorOrNull()).isInstanceOf( - AsyncResult.ChainedFailureException::class.java - ) - assertThat(stringResultCaptor.value.getErrorOrNull()).hasCauseThat() - .isInstanceOf(IllegalStateException::class.java) + assertThat(stringResultCaptor.value).isFailureThat().apply { + isInstanceOf(AsyncResult.ChainedFailureException::class.java) + hasCauseThat().isInstanceOf(IllegalStateException::class.java) + } } @Test @@ -1567,12 +1520,10 @@ class DataProvidersTest { testCoroutineDispatchers.advanceUntilIdle() verify(mockStringLiveDataObserver).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isFailure()).isTrue() - assertThat(stringResultCaptor.value.getErrorOrNull()).isInstanceOf( - AsyncResult.ChainedFailureException::class.java - ) - assertThat(stringResultCaptor.value.getErrorOrNull()).hasCauseThat() - .isInstanceOf(IllegalStateException::class.java) + assertThat(stringResultCaptor.value).isFailureThat().apply { + isInstanceOf(AsyncResult.ChainedFailureException::class.java) + hasCauseThat().isInstanceOf(IllegalStateException::class.java) + } } @Test @@ -1590,12 +1541,10 @@ class DataProvidersTest { testCoroutineDispatchers.advanceUntilIdle() verify(mockStringLiveDataObserver).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isFailure()).isTrue() - assertThat(stringResultCaptor.value.getErrorOrNull()).isInstanceOf( - AsyncResult.ChainedFailureException::class.java - ) - assertThat(stringResultCaptor.value.getErrorOrNull()).hasCauseThat() - .isInstanceOf(IllegalStateException::class.java) + assertThat(stringResultCaptor.value).isFailureThat().apply { + isInstanceOf(AsyncResult.ChainedFailureException::class.java) + hasCauseThat().isInstanceOf(IllegalStateException::class.java) + } } @Test @@ -1616,12 +1565,10 @@ class DataProvidersTest { testCoroutineDispatchers.advanceUntilIdle() verify(mockStringLiveDataObserver).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isFailure()).isTrue() - assertThat(stringResultCaptor.value.getErrorOrNull()).isInstanceOf( - AsyncResult.ChainedFailureException::class.java - ) - assertThat(stringResultCaptor.value.getErrorOrNull()).hasCauseThat() - .isInstanceOf(IllegalStateException::class.java) + assertThat(stringResultCaptor.value).isFailureThat().apply { + isInstanceOf(AsyncResult.ChainedFailureException::class.java) + hasCauseThat().isInstanceOf(IllegalStateException::class.java) + } } @Test @@ -1639,10 +1586,9 @@ class DataProvidersTest { testCoroutineDispatchers.advanceUntilIdle() verify(mockStringLiveDataObserver).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isFailure()).isTrue() - assertThat(stringResultCaptor.value.getErrorOrNull()).isInstanceOf( - IllegalStateException::class.java - ) + assertThat(stringResultCaptor.value) + .isFailureThat() + .isInstanceOf(IllegalStateException::class.java) } @Test @@ -1658,8 +1604,7 @@ class DataProvidersTest { testCoroutineDispatchers.advanceUntilIdle() verify(mockStringLiveDataObserver).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isSuccess()).isTrue() - assertThat(stringResultCaptor.value.getOrThrow()).isEqualTo(COMBINED_STR_VALUE_01) + assertThat(stringResultCaptor.value).isStringSuccessThat().isEqualTo(COMBINED_STR_VALUE_01) } @Test @@ -1681,8 +1626,7 @@ class DataProvidersTest { // Notifying the first base provider results in observers of the combined provider also being // called. verify(mockStringLiveDataObserver, atLeastOnce()).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isSuccess()).isTrue() - assertThat(stringResultCaptor.value.getOrThrow()).isEqualTo(COMBINED_STR_VALUE_21) + assertThat(stringResultCaptor.value).isStringSuccessThat().isEqualTo(COMBINED_STR_VALUE_21) } @Test @@ -1704,8 +1648,7 @@ class DataProvidersTest { // Notifying the combined provider results in observers of the combined provider also being // called. verify(mockStringLiveDataObserver, atLeastOnce()).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isSuccess()).isTrue() - assertThat(stringResultCaptor.value.getOrThrow()).isEqualTo(COMBINED_STR_VALUE_21) + assertThat(stringResultCaptor.value).isStringSuccessThat().isEqualTo(COMBINED_STR_VALUE_21) } @Test @@ -1725,8 +1668,7 @@ class DataProvidersTest { // The combined data provider is irrelevant; the base provider's change should be observed. verify(mockStringLiveDataObserver, atLeastOnce()).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isSuccess()).isTrue() - assertThat(stringResultCaptor.value.getOrThrow()).isEqualTo(STR_VALUE_2) + assertThat(stringResultCaptor.value).isStringSuccessThat().isEqualTo(STR_VALUE_2) } @Test @@ -1750,8 +1692,7 @@ class DataProvidersTest { // Notifying the combined data provider will not trigger observers of the changed provider // becoming aware of the change. verify(mockStringLiveDataObserver, atLeastOnce()).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isSuccess()).isTrue() - assertThat(stringResultCaptor.value.getOrThrow()).isEqualTo(STR_VALUE_0) + assertThat(stringResultCaptor.value).isStringSuccessThat().isEqualTo(STR_VALUE_0) } @Test @@ -1773,8 +1714,7 @@ class DataProvidersTest { // Notifying the second base provider results in observers of the combined provider also being // called. verify(mockStringLiveDataObserver, atLeastOnce()).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isSuccess()).isTrue() - assertThat(stringResultCaptor.value.getOrThrow()).isEqualTo(COMBINED_STR_VALUE_02) + assertThat(stringResultCaptor.value).isStringSuccessThat().isEqualTo(COMBINED_STR_VALUE_02) } @Test @@ -1796,8 +1736,7 @@ class DataProvidersTest { // Notifying the combined provider results in observers of the combined provider also being // called. verify(mockStringLiveDataObserver, atLeastOnce()).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isSuccess()).isTrue() - assertThat(stringResultCaptor.value.getOrThrow()).isEqualTo(COMBINED_STR_VALUE_02) + assertThat(stringResultCaptor.value).isStringSuccessThat().isEqualTo(COMBINED_STR_VALUE_02) } @Test @@ -1817,8 +1756,7 @@ class DataProvidersTest { // The combined data provider is irrelevant; the base provider's change should be observed. verify(mockStringLiveDataObserver, atLeastOnce()).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isSuccess()).isTrue() - assertThat(stringResultCaptor.value.getOrThrow()).isEqualTo(STR_VALUE_2) + assertThat(stringResultCaptor.value).isStringSuccessThat().isEqualTo(STR_VALUE_2) } @Test @@ -1842,8 +1780,7 @@ class DataProvidersTest { // Notifying the combined data provider will not trigger observers of the changed provider // becoming aware of the change. verify(mockStringLiveDataObserver, atLeastOnce()).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isSuccess()).isTrue() - assertThat(stringResultCaptor.value.getOrThrow()).isEqualTo(STR_VALUE_1) + assertThat(stringResultCaptor.value).isStringSuccessThat().isEqualTo(STR_VALUE_1) } @Test @@ -1859,7 +1796,7 @@ class DataProvidersTest { testCoroutineDispatchers.advanceUntilIdle() verify(mockStringLiveDataObserver).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isPending()).isTrue() + assertThat(stringResultCaptor.value).isPending() } @Test @@ -1875,7 +1812,7 @@ class DataProvidersTest { testCoroutineDispatchers.advanceUntilIdle() verify(mockStringLiveDataObserver).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isPending()).isTrue() + assertThat(stringResultCaptor.value).isPending() } @Test @@ -1891,7 +1828,7 @@ class DataProvidersTest { testCoroutineDispatchers.advanceUntilIdle() verify(mockStringLiveDataObserver).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isPending()).isTrue() + assertThat(stringResultCaptor.value).isPending() } @Test @@ -1908,12 +1845,10 @@ class DataProvidersTest { testCoroutineDispatchers.advanceUntilIdle() verify(mockStringLiveDataObserver).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isFailure()).isTrue() - assertThat(stringResultCaptor.value.getErrorOrNull()).isInstanceOf( - AsyncResult.ChainedFailureException::class.java - ) - assertThat(stringResultCaptor.value.getErrorOrNull()).hasCauseThat() - .isInstanceOf(IllegalStateException::class.java) + assertThat(stringResultCaptor.value).isFailureThat().apply { + isInstanceOf(AsyncResult.ChainedFailureException::class.java) + hasCauseThat().isInstanceOf(IllegalStateException::class.java) + } } @Test @@ -1930,12 +1865,10 @@ class DataProvidersTest { testCoroutineDispatchers.advanceUntilIdle() verify(mockStringLiveDataObserver).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isFailure()).isTrue() - assertThat(stringResultCaptor.value.getErrorOrNull()).isInstanceOf( - AsyncResult.ChainedFailureException::class.java - ) - assertThat(stringResultCaptor.value.getErrorOrNull()).hasCauseThat() - .isInstanceOf(IllegalStateException::class.java) + assertThat(stringResultCaptor.value).isFailureThat().apply { + isInstanceOf(AsyncResult.ChainedFailureException::class.java) + hasCauseThat().isInstanceOf(IllegalStateException::class.java) + } } @Test @@ -1953,12 +1886,10 @@ class DataProvidersTest { testCoroutineDispatchers.advanceUntilIdle() verify(mockStringLiveDataObserver).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isFailure()).isTrue() - assertThat(stringResultCaptor.value.getErrorOrNull()).isInstanceOf( - AsyncResult.ChainedFailureException::class.java - ) - assertThat(stringResultCaptor.value.getErrorOrNull()).hasCauseThat() - .isInstanceOf(IllegalStateException::class.java) + assertThat(stringResultCaptor.value).isFailureThat().apply { + isInstanceOf(AsyncResult.ChainedFailureException::class.java) + hasCauseThat().isInstanceOf(IllegalStateException::class.java) + } } @Test @@ -2121,12 +2052,10 @@ class DataProvidersTest { testCoroutineDispatchers.advanceUntilIdle() verify(mockStringLiveDataObserver).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isFailure()).isTrue() - assertThat(stringResultCaptor.value.getErrorOrNull()).isInstanceOf( - AsyncResult.ChainedFailureException::class.java - ) - assertThat(stringResultCaptor.value.getErrorOrNull()).hasCauseThat() - .isInstanceOf(IllegalStateException::class.java) + assertThat(stringResultCaptor.value).isFailureThat().apply { + isInstanceOf(AsyncResult.ChainedFailureException::class.java) + hasCauseThat().isInstanceOf(IllegalStateException::class.java) + } } @Test @@ -2145,12 +2074,10 @@ class DataProvidersTest { testCoroutineDispatchers.advanceUntilIdle() verify(mockStringLiveDataObserver).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isFailure()).isTrue() - assertThat(stringResultCaptor.value.getErrorOrNull()).isInstanceOf( - AsyncResult.ChainedFailureException::class.java - ) - assertThat(stringResultCaptor.value.getErrorOrNull()).hasCauseThat() - .isInstanceOf(IllegalStateException::class.java) + assertThat(stringResultCaptor.value).isFailureThat().apply { + isInstanceOf(AsyncResult.ChainedFailureException::class.java) + hasCauseThat().isInstanceOf(IllegalStateException::class.java) + } } @Test @@ -2172,12 +2099,10 @@ class DataProvidersTest { testCoroutineDispatchers.advanceUntilIdle() verify(mockStringLiveDataObserver).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isFailure()).isTrue() - assertThat(stringResultCaptor.value.getErrorOrNull()).isInstanceOf( - AsyncResult.ChainedFailureException::class.java - ) - assertThat(stringResultCaptor.value.getErrorOrNull()).hasCauseThat() - .isInstanceOf(IllegalStateException::class.java) + assertThat(stringResultCaptor.value).isFailureThat().apply { + isInstanceOf(AsyncResult.ChainedFailureException::class.java) + hasCauseThat().isInstanceOf(IllegalStateException::class.java) + } } @Test @@ -2189,7 +2114,7 @@ class DataProvidersTest { baseProvider1.combineWithAsync(baseProvider2, COMBINED_PROVIDER_ID) { v1, v2 -> // Note that this doesn't use combineStringsAsync since that relies on the blocked // backgroundTestCoroutineDispatcher. - AsyncResult.success(combineStrings(v1, v2)) + AsyncResult.Success(combineStrings(v1, v2)) } dataProvider.toLiveData().observeForever(mockStringLiveDataObserver) @@ -2207,7 +2132,7 @@ class DataProvidersTest { baseProvider1.combineWithAsync(baseProvider2, COMBINED_PROVIDER_ID) { v1, v2 -> // Note that this doesn't use combineStringsAsync since that relies on the blocked // backgroundTestCoroutineDispatcher. - AsyncResult.success(combineStrings(v1, v2)) + AsyncResult.Success(combineStrings(v1, v2)) } dataProvider.toLiveData().observeForever(mockStringLiveDataObserver) @@ -2216,8 +2141,7 @@ class DataProvidersTest { // The value should now be delivered since the provider was allowed to finish. verify(mockStringLiveDataObserver).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isSuccess()).isTrue() - assertThat(stringResultCaptor.value.getOrThrow()).isEqualTo(COMBINED_STR_VALUE_01) + assertThat(stringResultCaptor.value).isStringSuccessThat().isEqualTo(COMBINED_STR_VALUE_01) } @Test @@ -2229,7 +2153,7 @@ class DataProvidersTest { baseProvider1.combineWithAsync(baseProvider2, COMBINED_PROVIDER_ID) { v1, v2 -> // Note that this doesn't use combineStringsAsync since that relies on the blocked // backgroundTestCoroutineDispatcher. - AsyncResult.success(combineStrings(v1, v2)) + AsyncResult.Success(combineStrings(v1, v2)) } dataProvider.toLiveData().observeForever(mockStringLiveDataObserver) @@ -2247,7 +2171,7 @@ class DataProvidersTest { baseProvider1.combineWithAsync(baseProvider2, COMBINED_PROVIDER_ID) { v1, v2 -> // Note that this doesn't use combineStringsAsync since that relies on the blocked // backgroundTestCoroutineDispatcher. - AsyncResult.success(combineStrings(v1, v2)) + AsyncResult.Success(combineStrings(v1, v2)) } dataProvider.toLiveData().observeForever(mockStringLiveDataObserver) @@ -2256,8 +2180,7 @@ class DataProvidersTest { // The value should now be delivered since the provider was allowed to finish. verify(mockStringLiveDataObserver).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isSuccess()).isTrue() - assertThat(stringResultCaptor.value.getOrThrow()).isEqualTo(COMBINED_STR_VALUE_01) + assertThat(stringResultCaptor.value).isStringSuccessThat().isEqualTo(COMBINED_STR_VALUE_01) } @Test @@ -2292,8 +2215,7 @@ class DataProvidersTest { // The value should be delivered since the async function was allowed to finish. verify(mockStringLiveDataObserver).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isSuccess()).isTrue() - assertThat(stringResultCaptor.value.getOrThrow()).isEqualTo(COMBINED_STR_VALUE_01) + assertThat(stringResultCaptor.value).isStringSuccessThat().isEqualTo(COMBINED_STR_VALUE_01) } @Test @@ -2304,14 +2226,14 @@ class DataProvidersTest { baseProvider2, COMBINED_PROVIDER_ID ) { _: String, _: String -> - AsyncResult.pending() + AsyncResult.Pending() } dataProvider.toLiveData().observeForever(mockStringLiveDataObserver) testCoroutineDispatchers.advanceUntilIdle() verify(mockStringLiveDataObserver).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isPending()).isTrue() + assertThat(stringResultCaptor.value).isPending() } @Test @@ -2322,17 +2244,16 @@ class DataProvidersTest { baseProvider2, COMBINED_PROVIDER_ID ) { _: String, _: String -> - AsyncResult.failed(IllegalStateException("Combine failure")) + AsyncResult.Failure(IllegalStateException("Combine failure")) } dataProvider.toLiveData().observeForever(mockStringLiveDataObserver) testCoroutineDispatchers.advanceUntilIdle() verify(mockStringLiveDataObserver).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isFailure()).isTrue() - assertThat(stringResultCaptor.value.getErrorOrNull()).isInstanceOf( - IllegalStateException::class.java - ) + assertThat(stringResultCaptor.value) + .isFailureThat() + .isInstanceOf(IllegalStateException::class.java) } @Test @@ -2346,8 +2267,7 @@ class DataProvidersTest { testCoroutineDispatchers.advanceUntilIdle() verify(mockIntLiveDataObserver).onChanged(intResultCaptor.capture()) - assertThat(intResultCaptor.value.isSuccess()).isTrue() - assertThat(intResultCaptor.value.getOrThrow()).isEqualTo(INT_XFORMED_STR_VALUE_0) + assertThat(intResultCaptor.value).isIntSuccessThat().isEqualTo(INT_XFORMED_STR_VALUE_0) } @Test @@ -2366,8 +2286,7 @@ class DataProvidersTest { // Notifying the base results in observers of the transformed provider also being called. verify(mockIntLiveDataObserver, atLeastOnce()).onChanged(intResultCaptor.capture()) - assertThat(intResultCaptor.value.isSuccess()).isTrue() - assertThat(intResultCaptor.value.getOrThrow()).isEqualTo(INT_XFORMED_STR_VALUE_1) + assertThat(intResultCaptor.value).isIntSuccessThat().isEqualTo(INT_XFORMED_STR_VALUE_1) } @Test @@ -2386,8 +2305,7 @@ class DataProvidersTest { // Notifying the transformed provider has the same result as notifying the base provider. verify(mockIntLiveDataObserver, atLeastOnce()).onChanged(intResultCaptor.capture()) - assertThat(intResultCaptor.value.isSuccess()).isTrue() - assertThat(intResultCaptor.value.getOrThrow()).isEqualTo(INT_XFORMED_STR_VALUE_1) + assertThat(intResultCaptor.value).isIntSuccessThat().isEqualTo(INT_XFORMED_STR_VALUE_1) } @Test @@ -2408,8 +2326,7 @@ class DataProvidersTest { // Having a transformed data provider with an observer does not change the base's // notification behavior. verify(mockStringLiveDataObserver, atLeastOnce()).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isSuccess()).isTrue() - assertThat(stringResultCaptor.value.getOrThrow()).isEqualTo(STR_VALUE_1) + assertThat(stringResultCaptor.value).isStringSuccessThat().isEqualTo(STR_VALUE_1) } @Test @@ -2433,8 +2350,7 @@ class DataProvidersTest { // However, notifying that the transformed provider has changed should not affect base // subscriptions even if the base has changed. verify(mockStringLiveDataObserver, atLeastOnce()).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isSuccess()).isTrue() - assertThat(stringResultCaptor.value.getOrThrow()).isEqualTo(STR_VALUE_0) + assertThat(stringResultCaptor.value).isStringSuccessThat().isEqualTo(STR_VALUE_0) } @Test @@ -2464,8 +2380,7 @@ class DataProvidersTest { // The value should now be delivered since the async function was unblocked. verify(mockIntLiveDataObserver).onChanged(intResultCaptor.capture()) - assertThat(intResultCaptor.value.isSuccess()).isTrue() - assertThat(intResultCaptor.value.getOrThrow()).isEqualTo(INT_XFORMED_STR_VALUE_0) + assertThat(intResultCaptor.value).isIntSuccessThat().isEqualTo(INT_XFORMED_STR_VALUE_0) } @Test @@ -2484,15 +2399,14 @@ class DataProvidersTest { // Verify that even though the transformed provider is blocked, the base can still properly // publish changes. verify(mockStringLiveDataObserver).onChanged(stringResultCaptor.capture()) - assertThat(stringResultCaptor.value.isSuccess()).isTrue() - assertThat(stringResultCaptor.value.getOrThrow()).isEqualTo(STR_VALUE_0) + assertThat(stringResultCaptor.value).isStringSuccessThat().isEqualTo(STR_VALUE_0) } @Test fun testNestedXformedProvider_toLiveData_transformedPending_deliversPending() { val baseProvider = createSuccessfulDataProvider(BASE_PROVIDER_ID_0, STR_VALUE_0) val dataProvider = baseProvider.transformNested(TRANSFORMED_PROVIDER_ID) { - AsyncResult.pending() + AsyncResult.Pending() } dataProvider.toLiveData().observeForever(mockIntLiveDataObserver) @@ -2500,14 +2414,14 @@ class DataProvidersTest { // The transformation result yields a pending delivered result. verify(mockIntLiveDataObserver).onChanged(intResultCaptor.capture()) - assertThat(intResultCaptor.value.isPending()).isTrue() + assertThat(intResultCaptor.value).isPending() } @Test fun testNestedXformedProvider_toLiveData_transformedFailure_deliversFailure() { val baseProvider = createSuccessfulDataProvider(BASE_PROVIDER_ID_0, STR_VALUE_0) val dataProvider = baseProvider.transformNested(TRANSFORMED_PROVIDER_ID) { - AsyncResult.failed(IllegalStateException("Transform failure")) + AsyncResult.Failure(IllegalStateException("Transform failure")) } dataProvider.toLiveData().observeForever(mockIntLiveDataObserver) @@ -2516,11 +2430,10 @@ class DataProvidersTest { // Note that the failure exception in this case is not chained since the failure occurred in the // transform function. verify(mockIntLiveDataObserver).onChanged(intResultCaptor.capture()) - assertThat(intResultCaptor.value.isFailure()).isTrue() - assertThat(intResultCaptor.value.getErrorOrNull()) - .isInstanceOf(IllegalStateException::class.java) - assertThat(intResultCaptor.value.getErrorOrNull()) - .hasMessageThat().contains("Transform failure") + assertThat(intResultCaptor.value).isFailureThat().apply { + isInstanceOf(IllegalStateException::class.java) + hasMessageThat().contains("Transform failure") + } } @Test @@ -2535,7 +2448,7 @@ class DataProvidersTest { // Since the base provider is pending, so is the transformed provider. verify(mockIntLiveDataObserver).onChanged(intResultCaptor.capture()) - assertThat(intResultCaptor.value.isPending()).isTrue() + assertThat(intResultCaptor.value).isPending() } @Test @@ -2552,13 +2465,11 @@ class DataProvidersTest { // Note that the failure exception in this case is not chained since the failure occurred in the // transform function. verify(mockIntLiveDataObserver).onChanged(intResultCaptor.capture()) - assertThat(intResultCaptor.value.isFailure()).isTrue() - assertThat(intResultCaptor.value.getErrorOrNull()) - .isInstanceOf(AsyncResult.ChainedFailureException::class.java) - assertThat(intResultCaptor.value.getErrorOrNull()) - .hasCauseThat().isInstanceOf(IllegalStateException::class.java) - assertThat(intResultCaptor.value.getErrorOrNull()) - .hasCauseThat().hasMessageThat().contains("Base failure") + assertThat(intResultCaptor.value).isFailureThat().apply { + isInstanceOf(AsyncResult.ChainedFailureException::class.java) + hasCauseThat().isInstanceOf(IllegalStateException::class.java) + hasCauseThat().hasMessageThat().contains("Base failure") + } } @Test @@ -2645,8 +2556,7 @@ class DataProvidersTest { // The observer should get the newest value immediately. verify(mockIntLiveDataObserver).onChanged(intResultCaptor.capture()) - assertThat(intResultCaptor.value.isSuccess()).isTrue() - assertThat(intResultCaptor.value.getOrThrow()).isEqualTo(INT_XFORMED_STR_VALUE_1) + assertThat(intResultCaptor.value).isIntSuccessThat().isEqualTo(INT_XFORMED_STR_VALUE_1) } @Test @@ -2664,8 +2574,7 @@ class DataProvidersTest { // The observer should get the newest value immediately. verify(mockIntLiveDataObserver).onChanged(intResultCaptor.capture()) - assertThat(intResultCaptor.value.isSuccess()).isTrue() - assertThat(intResultCaptor.value.getOrThrow()).isEqualTo(INT_XFORMED_STR_VALUE_0_DOUBLED) + assertThat(intResultCaptor.value).isIntSuccessThat().isEqualTo(INT_XFORMED_STR_VALUE_0_DOUBLED) } @Test @@ -2683,8 +2592,7 @@ class DataProvidersTest { testCoroutineDispatchers.advanceUntilIdle() verify(mockIntLiveDataObserver, atLeastOnce()).onChanged(intResultCaptor.capture()) - assertThat(intResultCaptor.value.isSuccess()).isTrue() - assertThat(intResultCaptor.value.getOrThrow()).isEqualTo(INT_XFORMED_STR_VALUE_1) + assertThat(intResultCaptor.value).isIntSuccessThat().isEqualTo(INT_XFORMED_STR_VALUE_1) } @Test @@ -2707,8 +2615,7 @@ class DataProvidersTest { testCoroutineDispatchers.advanceUntilIdle() verify(mockIntLiveDataObserver, atLeastOnce()).onChanged(intResultCaptor.capture()) - assertThat(intResultCaptor.value.isSuccess()).isTrue() - assertThat(intResultCaptor.value.getOrThrow()).isEqualTo(INT_XFORMED_STR_VALUE_2) + assertThat(intResultCaptor.value).isIntSuccessThat().isEqualTo(INT_XFORMED_STR_VALUE_2) } @Test @@ -2731,8 +2638,7 @@ class DataProvidersTest { // Since the base provider was replaced, it shouldn't result in any observed change. verify(mockIntLiveDataObserver, atLeastOnce()).onChanged(intResultCaptor.capture()) - assertThat(intResultCaptor.value.isSuccess()).isTrue() - assertThat(intResultCaptor.value.getOrThrow()).isEqualTo(INT_XFORMED_STR_VALUE_2) + assertThat(intResultCaptor.value).isIntSuccessThat().isEqualTo(INT_XFORMED_STR_VALUE_2) } @Test @@ -2758,8 +2664,7 @@ class DataProvidersTest { // Since the base provider was replaced, the old notification should not trigger a newly // change even though the new base technically did change (but it wasn't notified yet). verify(mockIntLiveDataObserver, atLeastOnce()).onChanged(intResultCaptor.capture()) - assertThat(intResultCaptor.value.isSuccess()).isTrue() - assertThat(intResultCaptor.value.getOrThrow()).isEqualTo(INT_XFORMED_STR_VALUE_1) + assertThat(intResultCaptor.value).isIntSuccessThat().isEqualTo(INT_XFORMED_STR_VALUE_1) } @Test @@ -2823,7 +2728,7 @@ class DataProvidersTest { private suspend fun transformStringAsync(str: String): AsyncResult { val deferred = backgroundCoroutineScope.async { transformString(str) } deferred.await() - return AsyncResult.success(deferred.getCompleted()) + return AsyncResult.Success(deferred.getCompleted()) } /** @@ -2833,7 +2738,7 @@ class DataProvidersTest { private suspend fun transformStringDoubledAsync(str: String): AsyncResult { val deferred = backgroundCoroutineScope.async { transformString(str) * 2 } deferred.await() - return AsyncResult.success(deferred.getCompleted()) + return AsyncResult.Success(deferred.getCompleted()) } private fun combineStrings(str1: String, str2: String): String { @@ -2847,7 +2752,7 @@ class DataProvidersTest { private suspend fun combineStringsAsync(str1: String, str2: String): AsyncResult { val deferred = backgroundCoroutineScope.async { combineStrings(str1, str2) } deferred.await() - return AsyncResult.success(deferred.getCompleted()) + return AsyncResult.Success(deferred.getCompleted()) } private fun createSuccessfulDataProvider(id: Any, value: T): DataProvider { @@ -2858,7 +2763,7 @@ class DataProvidersTest { return dataProviders.createInMemoryDataProviderAsync(id) { // Android Studio incorrectly suggests to remove the explicit argument. @Suppress("RemoveExplicitTypeArguments") - AsyncResult.pending() + (AsyncResult.Pending()) } } @@ -2866,7 +2771,7 @@ class DataProvidersTest { return dataProviders.createInMemoryDataProviderAsync(id) { // Android Studio incorrectly suggests to remove the explicit argument. @Suppress("RemoveExplicitTypeArguments") - AsyncResult.failed(failure) + (AsyncResult.Failure(failure)) } } @@ -2879,7 +2784,7 @@ class DataProvidersTest { return dataProviders.createInMemoryDataProviderAsync(id) { val deferred = backgroundCoroutineScope.async { value } deferred.await() - AsyncResult.success(deferred.getCompleted()) + AsyncResult.Success(deferred.getCompleted()) } } diff --git a/utility/src/test/java/org/oppia/android/util/data/InMemoryBlockingCacheTest.kt b/utility/src/test/java/org/oppia/android/util/data/InMemoryBlockingCacheTest.kt index 3f355805ac9..e5e2a69b59b 100644 --- a/utility/src/test/java/org/oppia/android/util/data/InMemoryBlockingCacheTest.kt +++ b/utility/src/test/java/org/oppia/android/util/data/InMemoryBlockingCacheTest.kt @@ -35,6 +35,8 @@ private const val CREATED_ASYNC_VALUE = "created async value" private const val UPDATED_ASYNC_VALUE = "updated async value" /** Tests for [InMemoryBlockingCache]. */ +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) @Config(manifest = Config.NONE) From c515106a156ee74f129653ea1d1c944bef03022d Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Mon, 7 Mar 2022 14:55:24 -0800 Subject: [PATCH 150/162] Post-merge fixes and updates for consistency. --- .../RecentlyPlayedFragmentPresenter.kt | 2 +- .../ExplorationActivityPresenter.kt | 2 +- .../player/state/StateFragmentPresenter.kt | 9 +- .../state/StatePlayerRecyclerViewAssembler.kt | 20 +- .../StateFragmentTestActivityPresenter.kt | 3 +- .../ResumeLessonFragmentPresenter.kt | 2 +- .../app/story/StoryFragmentPresenter.kt | 3 +- .../ExplorationTestActivityPresenter.kt | 3 +- .../lessons/TopicLessonsFragmentPresenter.kt | 2 +- .../QuestionPlayerFragmentPresenter.kt | 7 +- .../app/player/state/StateFragmentTest.kt | 1 + .../exploration/ExplorationDataController.kt | 35 +- .../ExplorationProgressController.kt | 156 +++-- .../QuestionAssessmentProgressController.kt | 157 +++-- .../question/QuestionTrainingController.kt | 34 +- .../ExplorationDataControllerTest.kt | 141 +--- .../ExplorationProgressControllerTest.kt | 636 +++++------------- .../AppStartupStateControllerTest.kt | 6 +- ...uestionAssessmentProgressControllerTest.kt | 332 ++------- .../QuestionTrainingControllerTest.kt | 83 +-- model/src/main/proto/exploration.proto | 3 + .../oppia/android/util/data/DataProviders.kt | 10 +- 22 files changed, 550 insertions(+), 1097 deletions(-) diff --git a/app/src/main/java/org/oppia/android/app/home/recentlyplayed/RecentlyPlayedFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/home/recentlyplayed/RecentlyPlayedFragmentPresenter.kt index 33554540ec3..1a2647ff393 100755 --- a/app/src/main/java/org/oppia/android/app/home/recentlyplayed/RecentlyPlayedFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/home/recentlyplayed/RecentlyPlayedFragmentPresenter.kt @@ -299,7 +299,7 @@ class RecentlyPlayedFragmentPresenter @Inject constructor( shouldSavePartialProgress, // Pass an empty checkpoint if the exploration does not have to be resumed. ExplorationCheckpoint.getDefaultInstance() - ).observe( + ).toLiveData().observe( fragment, Observer> { result -> when (result) { diff --git a/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationActivityPresenter.kt index e0cceec462f..164e3a6716a 100755 --- a/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationActivityPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationActivityPresenter.kt @@ -239,7 +239,7 @@ class ExplorationActivityPresenter @Inject constructor( fun stopExploration() { fontScaleConfigurationUtil.adjustFontScale(activity, ReadingTextSize.MEDIUM_TEXT_SIZE.name) - explorationDataController.stopPlayingExploration() + explorationDataController.stopPlayingExploration().toLiveData() .observe( activity, Observer> { diff --git a/app/src/main/java/org/oppia/android/app/player/state/StateFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/player/state/StateFragmentPresenter.kt index 06824276dd9..c110827d88a 100755 --- a/app/src/main/java/org/oppia/android/app/player/state/StateFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/player/state/StateFragmentPresenter.kt @@ -44,6 +44,7 @@ import org.oppia.android.util.gcsresource.DefaultResourceBucketName import org.oppia.android.util.parser.html.ExplorationHtmlParserEntityType import org.oppia.android.util.system.OppiaClock import javax.inject.Inject +import org.oppia.android.util.data.DataProvider const val STATE_FRAGMENT_PROFILE_ID_ARGUMENT_KEY = "StateFragmentPresenter.state_fragment_profile_id" @@ -329,8 +330,8 @@ class StateFragmentPresenter @Inject constructor( } /** Subscribes to the result of requesting to show a hint or solution. */ - private fun subscribeToHintSolution(resultLiveData: LiveData>) { - resultLiveData.observe( + private fun subscribeToHintSolution(resultDataProvider: DataProvider) { + resultDataProvider.toLiveData().observe( fragment, { result -> if (result is AsyncResult.Failure) { @@ -396,7 +397,7 @@ class StateFragmentPresenter @Inject constructor( } private fun handleSubmitAnswer(answer: UserAnswer) { - subscribeToAnswerOutcome(explorationProgressController.submitAnswer(answer)) + subscribeToAnswerOutcome(explorationProgressController.submitAnswer(answer).toLiveData()) } fun dismissConceptCard() { @@ -409,7 +410,7 @@ class StateFragmentPresenter @Inject constructor( private fun moveToNextState() { viewModel.setCanSubmitAnswer(canSubmitAnswer = false) - explorationProgressController.moveToNextState().observe( + explorationProgressController.moveToNextState().toLiveData().observe( fragment, Observer { recyclerViewAssembler.collapsePreviousResponses() diff --git a/app/src/main/java/org/oppia/android/app/player/state/StatePlayerRecyclerViewAssembler.kt b/app/src/main/java/org/oppia/android/app/player/state/StatePlayerRecyclerViewAssembler.kt index 8375b12f475..9d32680b3e0 100644 --- a/app/src/main/java/org/oppia/android/app/player/state/StatePlayerRecyclerViewAssembler.kt +++ b/app/src/main/java/org/oppia/android/app/player/state/StatePlayerRecyclerViewAssembler.kt @@ -220,7 +220,7 @@ class StatePlayerRecyclerViewAssembler private constructor( conversationPendingItemList, extraInteractionPendingItemList, ephemeralState.pendingState.wrongAnswerList, - /* isCorrectAnswer= */ false, + isLastAnswerCorrect = false, gcsEntityId, ephemeralState.writtenTranslationContext ) @@ -251,7 +251,7 @@ class StatePlayerRecyclerViewAssembler private constructor( conversationPendingItemList, extraInteractionPendingItemList, ephemeralState.completedState.answerList, - /* isCorrectAnswer= */ true, + isLastAnswerCorrect = true, gcsEntityId, ephemeralState.writtenTranslationContext ) @@ -346,7 +346,7 @@ class StatePlayerRecyclerViewAssembler private constructor( pendingItemList: MutableList, rightPendingItemList: MutableList, answersAndResponses: List, - isCorrectAnswer: Boolean, + isLastAnswerCorrect: Boolean, gcsEntityId: String, writtenTranslationContext: WrittenTranslationContext ) { @@ -369,6 +369,8 @@ class StatePlayerRecyclerViewAssembler private constructor( hasPreviousResponsesExpanded for (answerAndResponse in answersAndResponses.take(answersAndResponses.size - 1)) { if (playerFeatureSet.pastAnswerSupport) { + // Earlier answers can't be correct (since otherwise new answers wouldn't be able to be + // submitted), hence the assumption that these aren't. createSubmittedAnswer( answerAndResponse.userAnswer, gcsEntityId, @@ -396,17 +398,17 @@ class StatePlayerRecyclerViewAssembler private constructor( } answersAndResponses.lastOrNull()?.let { answerAndResponse -> if (playerFeatureSet.pastAnswerSupport) { - if (isCorrectAnswer && isSplitView.get()!!) { + if (isLastAnswerCorrect && isSplitView.get()!!) { rightPendingItemList += createSubmittedAnswer( answerAndResponse.userAnswer, gcsEntityId, - /* isAnswerCorrect= */ true + isAnswerCorrect = true ) } else { pendingItemList += createSubmittedAnswer( answerAndResponse.userAnswer, gcsEntityId, - this.isCorrectAnswer.get()!! + isLastAnswerCorrect || answerAndResponse.isCorrectAnswer ) } } @@ -1055,6 +1057,9 @@ class StatePlayerRecyclerViewAssembler private constructor( when (userAnswer.textualAnswerCase) { UserAnswer.TextualAnswerCase.HTML_ANSWER -> { showSingleAnswer(binding) + val accessibleAnswer = if (userAnswer.contentDescription.isNotEmpty()) { + userAnswer.contentDescription + } else null val htmlParser = htmlParserFactory.create( resourceBucketName, entityType, @@ -1067,7 +1072,8 @@ class StatePlayerRecyclerViewAssembler private constructor( userAnswer.htmlAnswer, binding.submittedAnswerTextView, supportsConceptCards = submittedAnswerViewModel.supportsConceptCards - ) + ), + accessibleAnswer ) } UserAnswer.TextualAnswerCase.LIST_OF_HTML_ANSWERS -> { diff --git a/app/src/main/java/org/oppia/android/app/player/state/testing/StateFragmentTestActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/player/state/testing/StateFragmentTestActivityPresenter.kt index 3f5e8f6238e..365452a5e7d 100644 --- a/app/src/main/java/org/oppia/android/app/player/state/testing/StateFragmentTestActivityPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/player/state/testing/StateFragmentTestActivityPresenter.kt @@ -20,6 +20,7 @@ import org.oppia.android.domain.topic.TEST_STORY_ID_0 import org.oppia.android.domain.topic.TEST_TOPIC_ID_0 import org.oppia.android.util.data.AsyncResult import javax.inject.Inject +import org.oppia.android.util.data.DataProviders.Companion.toLiveData private const val TEST_ACTIVITY_TAG = "TestActivity" @@ -96,7 +97,7 @@ class StateFragmentTestActivityPresenter @Inject constructor( explorationId, shouldSavePartialProgress, explorationCheckpoint = ExplorationCheckpoint.getDefaultInstance() - ).observe( + ).toLiveData().observe( activity, Observer> { result -> when (result) { diff --git a/app/src/main/java/org/oppia/android/app/resumelesson/ResumeLessonFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/resumelesson/ResumeLessonFragmentPresenter.kt index be20d0102d5..f49c2081f62 100644 --- a/app/src/main/java/org/oppia/android/app/resumelesson/ResumeLessonFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/resumelesson/ResumeLessonFragmentPresenter.kt @@ -170,7 +170,7 @@ class ResumeLessonFragmentPresenter @Inject constructor( // ResumeLessonFragment implies that learner has not completed the lesson. shouldSavePartialProgress = true, explorationCheckpoint - ).observe( + ).toLiveData().observe( fragment, Observer> { result -> when (result) { diff --git a/app/src/main/java/org/oppia/android/app/story/StoryFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/story/StoryFragmentPresenter.kt index 247bd9cef3c..37f16e09391 100644 --- a/app/src/main/java/org/oppia/android/app/story/StoryFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/story/StoryFragmentPresenter.kt @@ -41,6 +41,7 @@ import org.oppia.android.util.parser.html.HtmlParser import org.oppia.android.util.parser.html.TopicHtmlParserEntityType import org.oppia.android.util.system.OppiaClock import javax.inject.Inject +import org.oppia.android.util.data.DataProviders.Companion.toLiveData /** The presenter for [StoryFragment]. */ class StoryFragmentPresenter @Inject constructor( @@ -275,7 +276,7 @@ class StoryFragmentPresenter @Inject constructor( shouldSavePartialProgress = shouldSavePartialProgress, // Pass an empty checkpoint if the exploration does not have to be resumed. ExplorationCheckpoint.getDefaultInstance() - ).observe( + ).toLiveData().observe( fragment, Observer> { result -> when (result) { diff --git a/app/src/main/java/org/oppia/android/app/testing/ExplorationTestActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/testing/ExplorationTestActivityPresenter.kt index 7542941bbb3..e6b9ce27943 100644 --- a/app/src/main/java/org/oppia/android/app/testing/ExplorationTestActivityPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/testing/ExplorationTestActivityPresenter.kt @@ -14,6 +14,7 @@ import org.oppia.android.domain.topic.TEST_STORY_ID_0 import org.oppia.android.domain.topic.TEST_TOPIC_ID_0 import org.oppia.android.util.data.AsyncResult import javax.inject.Inject +import org.oppia.android.util.data.DataProviders.Companion.toLiveData private const val INTERNAL_PROFILE_ID = 0 private const val TOPIC_ID = TEST_TOPIC_ID_0 @@ -47,7 +48,7 @@ class ExplorationTestActivityPresenter @Inject constructor( EXPLORATION_ID, shouldSavePartialProgress = false, explorationCheckpoint = ExplorationCheckpoint.getDefaultInstance() - ).observe( + ).toLiveData().observe( activity, Observer> { result -> when (result) { diff --git a/app/src/main/java/org/oppia/android/app/topic/lessons/TopicLessonsFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/topic/lessons/TopicLessonsFragmentPresenter.kt index 4b22edfbd18..c8c8e4c218a 100644 --- a/app/src/main/java/org/oppia/android/app/topic/lessons/TopicLessonsFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/topic/lessons/TopicLessonsFragmentPresenter.kt @@ -292,7 +292,7 @@ class TopicLessonsFragmentPresenter @Inject constructor( shouldSavePartialProgress, // Pass an empty checkpoint if the exploration does not have to be resumed. ExplorationCheckpoint.getDefaultInstance() - ).observe( + ).toLiveData().observe( fragment, Observer> { result -> when (result) { diff --git a/app/src/main/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerFragmentPresenter.kt index e12366009cc..f3329a3e45b 100644 --- a/app/src/main/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerFragmentPresenter.kt @@ -37,6 +37,7 @@ import org.oppia.android.util.data.DataProviders.Companion.toLiveData import org.oppia.android.util.gcsresource.QuestionResourceBucketName import org.oppia.android.util.system.OppiaClock import javax.inject.Inject +import org.oppia.android.util.data.DataProvider /** The presenter for [QuestionPlayerFragment]. */ @FragmentScope @@ -255,7 +256,7 @@ class QuestionPlayerFragmentPresenter @Inject constructor( } private fun handleSubmitAnswer(answer: UserAnswer) { - subscribeToAnswerOutcome(questionAssessmentProgressController.submitAnswer(answer)) + subscribeToAnswerOutcome(questionAssessmentProgressController.submitAnswer(answer).toLiveData()) } /** This function listens to and processes the result of submitAnswer from QuestionAssessmentProgressController. */ @@ -279,8 +280,8 @@ class QuestionPlayerFragmentPresenter @Inject constructor( } /** Subscribes to the result of requesting to show a hint or solution. */ - private fun subscribeToHintSolution(resultLiveData: LiveData>) { - resultLiveData.observe( + private fun subscribeToHintSolution(resultDataProvider: DataProvider) { + resultDataProvider.toLiveData().observe( fragment, { result -> if (result is AsyncResult.Failure) { diff --git a/app/src/sharedTest/java/org/oppia/android/app/player/state/StateFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/player/state/StateFragmentTest.kt index c21655ab1d4..f6b4854b624 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/player/state/StateFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/player/state/StateFragmentTest.kt @@ -614,6 +614,7 @@ class StateFragmentTest { } @Test + @RunOn(TestPlatform.ESPRESSO) // TODO(#1612): Enable for Robolectric. fun testStateFragment_loadDragDropExp_correctAnswer_contentDescriptionIsCorrect() { launchForExploration(TEST_EXPLORATION_ID_2, shouldSavePartialProgress = false).use { startPlayingExploration() diff --git a/domain/src/main/java/org/oppia/android/domain/exploration/ExplorationDataController.kt b/domain/src/main/java/org/oppia/android/domain/exploration/ExplorationDataController.kt index e4780f88f30..f5eea90e12a 100644 --- a/domain/src/main/java/org/oppia/android/domain/exploration/ExplorationDataController.kt +++ b/domain/src/main/java/org/oppia/android/domain/exploration/ExplorationDataController.kt @@ -63,36 +63,23 @@ class ExplorationDataController @Inject constructor( explorationId: String, shouldSavePartialProgress: Boolean, explorationCheckpoint: ExplorationCheckpoint - ): LiveData> { - return try { - explorationProgressController.beginExplorationAsync( - internalProfileId, - topicId, - storyId, - explorationId, - shouldSavePartialProgress, - explorationCheckpoint - ) - MutableLiveData(AsyncResult.success(null)) - } catch (e: Exception) { - exceptionsController.logNonFatalException(e) - MutableLiveData(AsyncResult.failed(e)) - } + ): DataProvider { + return explorationProgressController.beginExplorationAsync( + internalProfileId, + topicId, + storyId, + explorationId, + shouldSavePartialProgress, + explorationCheckpoint + ) } /** * Finishes the most recent exploration started by [startPlayingExploration]. This method should only be called if an * active exploration is being played, otherwise an exception will be thrown. */ - fun stopPlayingExploration(): LiveData> { - return try { - explorationProgressController.finishExplorationAsync() - MutableLiveData(AsyncResult.success(null)) - } catch (e: Exception) { - exceptionsController.logNonFatalException(e) - MutableLiveData(AsyncResult.failed(e)) - } - } + fun stopPlayingExploration(): DataProvider = + explorationProgressController.finishExplorationAsync() /** * Fetches the details of the oldest saved exploration for a specified profileId. diff --git a/domain/src/main/java/org/oppia/android/domain/exploration/ExplorationProgressController.kt b/domain/src/main/java/org/oppia/android/domain/exploration/ExplorationProgressController.kt index 7935afb9155..6b1a4207dbd 100644 --- a/domain/src/main/java/org/oppia/android/domain/exploration/ExplorationProgressController.kt +++ b/domain/src/main/java/org/oppia/android/domain/exploration/ExplorationProgressController.kt @@ -1,7 +1,5 @@ package org.oppia.android.domain.exploration -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData import org.oppia.android.app.model.AnswerOutcome import org.oppia.android.app.model.CheckpointState import org.oppia.android.app.model.EphemeralState @@ -27,8 +25,23 @@ import java.util.concurrent.locks.ReentrantLock import javax.inject.Inject import javax.inject.Singleton import kotlin.concurrent.withLock - -private const val CURRENT_STATE_DATA_PROVIDER_ID = "current_state_data_provider_id" +import org.oppia.android.util.data.DataProviders + +private const val BEGIN_EXPLORATION_RESULT_PROVIDER_ID = + "ExplorationProgressController.begin_exploration_result" +private const val FINISH_EXPLORATION_RESULT_PROVIDER_ID = + "ExplorationProgressController.finish_exploration_result" +private const val SUBMIT_ANSWER_RESULT_PROVIDER_ID = + "ExplorationProgressController.submit_answer_result" +private const val SUBMIT_HINT_REVEALED_RESULT_PROVIDER_ID = + "ExplorationProgressController.submit_hint_revealed_result" +private const val SUBMIT_SOLUTION_REVEALED_RESULT_PROVIDER_ID = + "ExplorationProgressController.submit_solution_revealed_result" +private const val MOVE_TO_PREVIOUS_STATE_RESULT_PROVIDER_ID = + "ExplorationProgressController.move_to_previous_state_result" +private const val MOVE_TO_NEXT_STATE_RESULT_PROVIDER_ID = + "ExplorationProgressController.move_to_next_state_result" +private const val CURRENT_STATE_PROVIDER_ID = "ExplorationProgressController.current_state" /** * Controller that tracks and reports the learner's ephemeral/non-persisted progress through an @@ -50,7 +63,8 @@ class ExplorationProgressController @Inject constructor( private val oppiaClock: OppiaClock, private val oppiaLogger: OppiaLogger, private val hintHandlerFactory: HintHandler.Factory, - private val translationController: TranslationController + private val translationController: TranslationController, + private val dataProviders: DataProviders ) : HintHandler.HintMonitor { // TODO(#179): Add support for parameters. // TODO(#3622): Update the internal locking of this controller to use something like an in-memory @@ -65,6 +79,8 @@ class ExplorationProgressController @Inject constructor( // callback on the deferred returned on saving checkpoints. In this case ExplorationActivity will // make decisions based on a value of the checkpointState which might not be up-to date. + // TODO: update documentation here & elsewhere that references LiveData instead of data providers (also check for 'live data'). + private val explorationProgress = ExplorationProgress() private val explorationProgressLock = ReentrantLock() private lateinit var hintHandler: HintHandler @@ -77,34 +93,54 @@ class ExplorationProgressController @Inject constructor( explorationId: String, shouldSavePartialProgress: Boolean, explorationCheckpoint: ExplorationCheckpoint - ) { - explorationProgressLock.withLock { - check(explorationProgress.playStage == ExplorationProgress.PlayStage.NOT_PLAYING) { - "Expected to finish previous exploration before starting a new one." - } + ): DataProvider { + return explorationProgressLock.withLock { + try { + check(explorationProgress.playStage == ExplorationProgress.PlayStage.NOT_PLAYING) { + "Expected to finish previous exploration before starting a new one." + } - explorationProgress.apply { - currentProfileId = ProfileId.newBuilder().setInternalId(internalProfileId).build() - currentTopicId = topicId - currentStoryId = storyId - currentExplorationId = explorationId - this.shouldSavePartialProgress = shouldSavePartialProgress - checkpointState = CheckpointState.CHECKPOINT_UNSAVED - this.explorationCheckpoint = explorationCheckpoint + explorationProgress.apply { + currentProfileId = ProfileId.newBuilder().setInternalId(internalProfileId).build() + currentTopicId = topicId + currentStoryId = storyId + currentExplorationId = explorationId + this.shouldSavePartialProgress = shouldSavePartialProgress + checkpointState = CheckpointState.CHECKPOINT_UNSAVED + this.explorationCheckpoint = explorationCheckpoint + } + hintHandler = hintHandlerFactory.create(this) + explorationProgress.advancePlayStageTo(ExplorationProgress.PlayStage.LOADING_EXPLORATION) + asyncDataSubscriptionManager.notifyChangeAsync(CURRENT_STATE_PROVIDER_ID) + return@withLock dataProviders.createInMemoryDataProvider( + BEGIN_EXPLORATION_RESULT_PROVIDER_ID + ) { null } + } catch (e: Exception) { + exceptionsController.logNonFatalException(e) + return@withLock dataProviders.createInMemoryDataProviderAsync( + BEGIN_EXPLORATION_RESULT_PROVIDER_ID + ) { AsyncResult.Failure(e) } } - hintHandler = hintHandlerFactory.create(this) - explorationProgress.advancePlayStageTo(ExplorationProgress.PlayStage.LOADING_EXPLORATION) - asyncDataSubscriptionManager.notifyChangeAsync(CURRENT_STATE_DATA_PROVIDER_ID) } } /** Indicates that the current exploration being played is now completed. */ - internal fun finishExplorationAsync() { - explorationProgressLock.withLock { - check(explorationProgress.playStage != ExplorationProgress.PlayStage.NOT_PLAYING) { - "Cannot finish playing an exploration that hasn't yet been started" + internal fun finishExplorationAsync(): DataProvider { + return explorationProgressLock.withLock { + try { + check(explorationProgress.playStage != ExplorationProgress.PlayStage.NOT_PLAYING) { + "Cannot finish playing an exploration that hasn't yet been started" + } + explorationProgress.advancePlayStageTo(ExplorationProgress.PlayStage.NOT_PLAYING) + return@withLock dataProviders.createInMemoryDataProvider( + FINISH_EXPLORATION_RESULT_PROVIDER_ID + ) { null } + } catch (e: Exception) { + exceptionsController.logNonFatalException(e) + return@withLock dataProviders.createInMemoryDataProviderAsync( + FINISH_EXPLORATION_RESULT_PROVIDER_ID + ) { AsyncResult.Failure(e) } } - explorationProgress.advancePlayStageTo(ExplorationProgress.PlayStage.NOT_PLAYING) } } @@ -112,7 +148,7 @@ class ExplorationProgressController @Inject constructor( explorationProgressLock.withLock { saveExplorationCheckpoint() } - asyncDataSubscriptionManager.notifyChangeAsync(CURRENT_STATE_DATA_PROVIDER_ID) + asyncDataSubscriptionManager.notifyChangeAsync(CURRENT_STATE_PROVIDER_ID) } /** @@ -145,7 +181,7 @@ class ExplorationProgressController @Inject constructor( * [LiveData] from [getCurrentState]. Also note that the returned [LiveData] will only have a * single value and not be reused after that point. */ - fun submitAnswer(userAnswer: UserAnswer): LiveData> { + fun submitAnswer(userAnswer: UserAnswer): DataProvider { try { explorationProgressLock.withLock { check( @@ -169,7 +205,7 @@ class ExplorationProgressController @Inject constructor( // Notify observers that the submitted answer is currently pending. explorationProgress.advancePlayStageTo(ExplorationProgress.PlayStage.SUBMITTING_ANSWER) - asyncDataSubscriptionManager.notifyChangeAsync(CURRENT_STATE_DATA_PROVIDER_ID) + asyncDataSubscriptionManager.notifyChangeAsync(CURRENT_STATE_PROVIDER_ID) lateinit var answerOutcome: AnswerOutcome try { @@ -212,13 +248,17 @@ class ExplorationProgressController @Inject constructor( explorationProgress.advancePlayStageTo(ExplorationProgress.PlayStage.VIEWING_STATE) } - asyncDataSubscriptionManager.notifyChangeAsync(CURRENT_STATE_DATA_PROVIDER_ID) + asyncDataSubscriptionManager.notifyChangeAsync(CURRENT_STATE_PROVIDER_ID) - return MutableLiveData(AsyncResult.Success(answerOutcome)) + return dataProviders.createInMemoryDataProvider(SUBMIT_ANSWER_RESULT_PROVIDER_ID) { + answerOutcome + } } } catch (e: Exception) { exceptionsController.logNonFatalException(e) - return MutableLiveData(AsyncResult.Failure(e)) + return dataProviders.createInMemoryDataProviderAsync(SUBMIT_ANSWER_RESULT_PROVIDER_ID) { + AsyncResult.Failure(e) + } } } @@ -230,7 +270,7 @@ class ExplorationProgressController @Inject constructor( * @return a one-time [LiveData] that indicates success/failure of the operation (the actual * payload of the result isn't relevant) */ - fun submitHintIsRevealed(hintIndex: Int): LiveData> { + fun submitHintIsRevealed(hintIndex: Int): DataProvider { try { explorationProgressLock.withLock { check( @@ -259,12 +299,16 @@ class ExplorationProgressController @Inject constructor( // exception. explorationProgress.advancePlayStageTo(ExplorationProgress.PlayStage.VIEWING_STATE) } - asyncDataSubscriptionManager.notifyChangeAsync(CURRENT_STATE_DATA_PROVIDER_ID) - return MutableLiveData(AsyncResult.Success(null)) + asyncDataSubscriptionManager.notifyChangeAsync(CURRENT_STATE_PROVIDER_ID) + return dataProviders.createInMemoryDataProvider(SUBMIT_HINT_REVEALED_RESULT_PROVIDER_ID) { + null + } } } catch (e: Exception) { exceptionsController.logNonFatalException(e) - return MutableLiveData(AsyncResult.Failure(e)) + return dataProviders.createInMemoryDataProviderAsync( + SUBMIT_HINT_REVEALED_RESULT_PROVIDER_ID + ) { AsyncResult.Failure(e) } } } @@ -274,7 +318,7 @@ class ExplorationProgressController @Inject constructor( * @return a one-time [LiveData] that indicates success/failure of the operation (the actual * payload of the result isn't relevant) */ - fun submitSolutionIsRevealed(): LiveData> { + fun submitSolutionIsRevealed(): DataProvider { try { explorationProgressLock.withLock { check( @@ -304,12 +348,16 @@ class ExplorationProgressController @Inject constructor( explorationProgress.advancePlayStageTo(ExplorationProgress.PlayStage.VIEWING_STATE) } - asyncDataSubscriptionManager.notifyChangeAsync(CURRENT_STATE_DATA_PROVIDER_ID) - return MutableLiveData(AsyncResult.Success(null)) + asyncDataSubscriptionManager.notifyChangeAsync(CURRENT_STATE_PROVIDER_ID) + return dataProviders.createInMemoryDataProvider( + SUBMIT_SOLUTION_REVEALED_RESULT_PROVIDER_ID + ) { null } } } catch (e: Exception) { exceptionsController.logNonFatalException(e) - return MutableLiveData(AsyncResult.Failure(e)) + return dataProviders.createInMemoryDataProviderAsync( + SUBMIT_SOLUTION_REVEALED_RESULT_PROVIDER_ID + ) { AsyncResult.Failure(e) } } } @@ -324,7 +372,7 @@ class ExplorationProgressController @Inject constructor( * that calling code only listen to this result for failures, and instead rely on * [getCurrentState] for observing a successful transition to another state. */ - fun moveToPreviousState(): LiveData> { + fun moveToPreviousState(): DataProvider { try { explorationProgressLock.withLock { check( @@ -347,12 +395,16 @@ class ExplorationProgressController @Inject constructor( } hintHandler.navigateToPreviousState() explorationProgress.stateDeck.navigateToPreviousState() - asyncDataSubscriptionManager.notifyChangeAsync(CURRENT_STATE_DATA_PROVIDER_ID) + asyncDataSubscriptionManager.notifyChangeAsync(CURRENT_STATE_PROVIDER_ID) + } + return dataProviders.createInMemoryDataProvider(MOVE_TO_PREVIOUS_STATE_RESULT_PROVIDER_ID) { + null } - return MutableLiveData(AsyncResult.Success(null)) } catch (e: Exception) { exceptionsController.logNonFatalException(e) - return MutableLiveData(AsyncResult.Failure(e)) + return dataProviders.createInMemoryDataProviderAsync( + MOVE_TO_PREVIOUS_STATE_RESULT_PROVIDER_ID + ) { AsyncResult.Failure(e) } } } @@ -371,7 +423,7 @@ class ExplorationProgressController @Inject constructor( * listen to this result for failures, and instead rely on [getCurrentState] for observing a * successful transition to another state. */ - fun moveToNextState(): LiveData> { + fun moveToNextState(): DataProvider { try { explorationProgressLock.withLock { check( @@ -401,12 +453,16 @@ class ExplorationProgressController @Inject constructor( // will not be marked on any of the completed states. saveExplorationCheckpoint() } - asyncDataSubscriptionManager.notifyChangeAsync(CURRENT_STATE_DATA_PROVIDER_ID) + asyncDataSubscriptionManager.notifyChangeAsync(CURRENT_STATE_PROVIDER_ID) + } + return dataProviders.createInMemoryDataProvider(MOVE_TO_NEXT_STATE_RESULT_PROVIDER_ID) { + null } - return MutableLiveData(AsyncResult.Success(null)) } catch (e: Exception) { exceptionsController.logNonFatalException(e) - return MutableLiveData(AsyncResult.Failure(e)) + return dataProviders.createInMemoryDataProviderAsync(MOVE_TO_NEXT_STATE_RESULT_PROVIDER_ID) { + AsyncResult.Failure(e) + } } } @@ -434,7 +490,7 @@ class ExplorationProgressController @Inject constructor( fun getCurrentState(): DataProvider { return translationController.getWrittenTranslationContentLocale( explorationProgress.currentProfileId - ).transformAsync(CURRENT_STATE_DATA_PROVIDER_ID) { contentLocale -> + ).transformAsync(CURRENT_STATE_PROVIDER_ID) { contentLocale -> return@transformAsync retrieveCurrentStateAsync(contentLocale) } } @@ -645,7 +701,7 @@ class ExplorationProgressController @Inject constructor( } explorationProgress.updateCheckpointState(newCheckpointState) // Notify observers that the checkpoint state has changed. - asyncDataSubscriptionManager.notifyChangeAsync(CURRENT_STATE_DATA_PROVIDER_ID) + asyncDataSubscriptionManager.notifyChangeAsync(CURRENT_STATE_PROVIDER_ID) } } } diff --git a/domain/src/main/java/org/oppia/android/domain/question/QuestionAssessmentProgressController.kt b/domain/src/main/java/org/oppia/android/domain/question/QuestionAssessmentProgressController.kt index e90fa5286e0..97e69c42a9e 100644 --- a/domain/src/main/java/org/oppia/android/domain/question/QuestionAssessmentProgressController.kt +++ b/domain/src/main/java/org/oppia/android/domain/question/QuestionAssessmentProgressController.kt @@ -1,7 +1,5 @@ package org.oppia.android.domain.question -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData import org.oppia.android.app.model.AnsweredQuestionOutcome import org.oppia.android.app.model.EphemeralQuestion import org.oppia.android.app.model.EphemeralState @@ -29,15 +27,26 @@ import javax.inject.Inject import javax.inject.Singleton import kotlin.concurrent.withLock -private const val CREATE_CURRENT_QUESTION_DATA_PROVIDER_ID = - "create_current_question_data_provider_id" -private const val CREATE_CURRENT_QUESTION_DATA_WITH_TRANSLATION_CONTEXT_PROVIDER_ID = - "create_current_question_data_with_translation_context_provider_id" -private const val BEGIN_QUESTION_TRAINING_SESSION_PROVIDER_ID = - "begin_question_training_session_provider_id" -private const val CREATE_EMPTY_QUESTIONS_LIST_DATA_PROVIDER_ID = - "create_empty_questions_list_data_provider_id" -private const val SUBMIT_ANSWER_PROVIDER_ID = "submit_answer_provider_id" +private const val BEGIN_SESSION_RESULT_PROVIDER_ID = + "QuestionAssessmentProgressController.begin_session_result" +private const val FINISH_SESSION_RESULT_PROVIDER_ID = + "QuestionAssessmentProgressController.finish_session_result" +private const val SUBMIT_ANSWER_RESULT_PROVIDER_ID = + "QuestionAssessmentProgressController.submit_answer_result" +private const val SUBMIT_HINT_REVEALED_RESULT_PROVIDER_ID = + "QuestionAssessmentProgressController.submit_hint_revealed_result" +private const val SUBMIT_SOLUTION_REVEALED_RESULT_PROVIDER_ID = + "QuestionAssessmentProgressController.submit_solution_revealed_result" +private const val MOVE_TO_NEXT_QUESTION_RESULT_PROVIDER_ID = + "QuestionAssessmentProgressController.move_to_next_question_result" +private const val CURRENT_QUESTION_PROVIDER_ID = + "QuestionAssessmentProgressController.current_question" +private const val CALCULATE_SCORES_PROVIDER_ID = + "QuestionAssessmentProgressController.calculate_scores" +private const val LOCALIZED_QUESTION_PROVIDER_ID = + "QuestionAssessmentProgressController.localized_question" +private const val EMPTY_QUESTIONS_LIST_DATA_PROVIDER_ID = + "QuestionAssessmentProgressController.create_empty_questions_list_data_provider_id" /** * Controller that tracks and reports the learner's ephemeral/non-persisted progress through a @@ -75,37 +84,57 @@ class QuestionAssessmentProgressController @Inject constructor( internal fun beginQuestionTrainingSession( questionsListDataProvider: DataProvider>, profileId: ProfileId - ) { - progressLock.withLock { - check(progress.trainStage == TrainStage.NOT_IN_TRAINING_SESSION) { - "Cannot start a new training session until the previous one is completed." - } - progress.currentProfileId = profileId + ): DataProvider { + return progressLock.withLock { + try { + check(progress.trainStage == TrainStage.NOT_IN_TRAINING_SESSION) { + "Cannot start a new training session until the previous one is completed." + } + progress.currentProfileId = profileId - hintHandler = hintHandlerFactory.create(this) - progress.advancePlayStageTo(TrainStage.LOADING_TRAINING_SESSION) - currentQuestionDataProvider.setBaseDataProvider( - questionsListDataProvider, - this::retrieveCurrentQuestionAsync - ) - asyncDataSubscriptionManager.notifyChangeAsync(BEGIN_QUESTION_TRAINING_SESSION_PROVIDER_ID) + hintHandler = hintHandlerFactory.create(this) + progress.advancePlayStageTo(TrainStage.LOADING_TRAINING_SESSION) + currentQuestionDataProvider.setBaseDataProvider( + questionsListDataProvider, + this::retrieveCurrentQuestionAsync + ) + asyncDataSubscriptionManager.notifyChangeAsync(CURRENT_QUESTION_PROVIDER_ID) + return@withLock dataProviders.createInMemoryDataProvider(BEGIN_SESSION_RESULT_PROVIDER_ID) { + null + } + } catch (e: Exception) { + exceptionsController.logNonFatalException(e) + return@withLock dataProviders.createInMemoryDataProviderAsync( + BEGIN_SESSION_RESULT_PROVIDER_ID + ) { AsyncResult.Failure(e) } + } } } - internal fun finishQuestionTrainingSession() { - progressLock.withLock { - check(progress.trainStage != TrainStage.NOT_IN_TRAINING_SESSION) { - "Cannot stop a new training session which wasn't started." + internal fun finishQuestionTrainingSession(): DataProvider { + return progressLock.withLock { + try { + check(progress.trainStage != TrainStage.NOT_IN_TRAINING_SESSION) { + "Cannot stop a new training session which wasn't started." + } + progress.advancePlayStageTo(TrainStage.NOT_IN_TRAINING_SESSION) + currentQuestionDataProvider.setBaseDataProvider( + createEmptyQuestionsListDataProvider(), this::retrieveCurrentQuestionAsync + ) + return@withLock dataProviders.createInMemoryDataProvider( + FINISH_SESSION_RESULT_PROVIDER_ID + ) { null } + } catch (e: Exception) { + exceptionsController.logNonFatalException(e) + return@withLock dataProviders.createInMemoryDataProviderAsync( + FINISH_SESSION_RESULT_PROVIDER_ID + ) { AsyncResult.Failure(e) } } - progress.advancePlayStageTo(TrainStage.NOT_IN_TRAINING_SESSION) - currentQuestionDataProvider.setBaseDataProvider( - createEmptyQuestionsListDataProvider(), this::retrieveCurrentQuestionAsync - ) } } override fun onHelpIndexChanged() { - asyncDataSubscriptionManager.notifyChangeAsync(CREATE_CURRENT_QUESTION_DATA_PROVIDER_ID) + asyncDataSubscriptionManager.notifyChangeAsync(CURRENT_QUESTION_PROVIDER_ID) } /** @@ -137,7 +166,7 @@ class QuestionAssessmentProgressController @Inject constructor( * [LiveData] from [getCurrentQuestion]. Also note that the returned [LiveData] will only have a * single value and not be reused after that point. */ - fun submitAnswer(answer: UserAnswer): LiveData> { + fun submitAnswer(answer: UserAnswer): DataProvider { try { progressLock.withLock { check(progress.trainStage != TrainStage.NOT_IN_TRAINING_SESSION) { @@ -152,7 +181,7 @@ class QuestionAssessmentProgressController @Inject constructor( // Notify observers that the submitted answer is currently pending. progress.advancePlayStageTo(TrainStage.SUBMITTING_ANSWER) - asyncDataSubscriptionManager.notifyChangeAsync(SUBMIT_ANSWER_PROVIDER_ID) + asyncDataSubscriptionManager.notifyChangeAsync(CURRENT_QUESTION_PROVIDER_ID) lateinit var answeredQuestionOutcome: AnsweredQuestionOutcome try { @@ -198,13 +227,17 @@ class QuestionAssessmentProgressController @Inject constructor( progress.advancePlayStageTo(TrainStage.VIEWING_STATE) } - asyncDataSubscriptionManager.notifyChangeAsync(CREATE_CURRENT_QUESTION_DATA_PROVIDER_ID) + asyncDataSubscriptionManager.notifyChangeAsync(CURRENT_QUESTION_PROVIDER_ID) - return MutableLiveData(AsyncResult.Success(answeredQuestionOutcome)) + return dataProviders.createInMemoryDataProvider(SUBMIT_ANSWER_RESULT_PROVIDER_ID) { + answeredQuestionOutcome + } } } catch (e: Exception) { exceptionsController.logNonFatalException(e) - return MutableLiveData(AsyncResult.Failure(e)) + return dataProviders.createInMemoryDataProviderAsync(SUBMIT_ANSWER_RESULT_PROVIDER_ID) { + AsyncResult.Failure(e) + } } } @@ -216,7 +249,7 @@ class QuestionAssessmentProgressController @Inject constructor( * @return a one-time [LiveData] that indicates success/failure of the operation (the actual * payload of the result isn't relevant) */ - fun submitHintIsRevealed(hintIndex: Int): LiveData> { + fun submitHintIsRevealed(hintIndex: Int): DataProvider { try { progressLock.withLock { check(progress.trainStage != TrainStage.NOT_IN_TRAINING_SESSION) { @@ -237,12 +270,16 @@ class QuestionAssessmentProgressController @Inject constructor( // exception. progress.advancePlayStageTo(TrainStage.VIEWING_STATE) } - asyncDataSubscriptionManager.notifyChangeAsync(CREATE_CURRENT_QUESTION_DATA_PROVIDER_ID) - return MutableLiveData(AsyncResult.Success(null)) + asyncDataSubscriptionManager.notifyChangeAsync(CURRENT_QUESTION_PROVIDER_ID) + return dataProviders.createInMemoryDataProvider(SUBMIT_HINT_REVEALED_RESULT_PROVIDER_ID) { + null + } } } catch (e: Exception) { exceptionsController.logNonFatalException(e) - return MutableLiveData(AsyncResult.Failure(e)) + return dataProviders.createInMemoryDataProviderAsync( + SUBMIT_HINT_REVEALED_RESULT_PROVIDER_ID + ) { AsyncResult.Failure(e) } } } @@ -252,7 +289,7 @@ class QuestionAssessmentProgressController @Inject constructor( * @return a one-time [LiveData] that indicates success/failure of the operation (the actual * payload of the result isn't relevant) */ - fun submitSolutionIsRevealed(): LiveData> { + fun submitSolutionIsRevealed(): DataProvider { try { progressLock.withLock { check(progress.trainStage != TrainStage.NOT_IN_TRAINING_SESSION) { @@ -274,12 +311,16 @@ class QuestionAssessmentProgressController @Inject constructor( progress.advancePlayStageTo(TrainStage.VIEWING_STATE) } - asyncDataSubscriptionManager.notifyChangeAsync(CREATE_CURRENT_QUESTION_DATA_PROVIDER_ID) - return MutableLiveData(AsyncResult.Success(null)) + asyncDataSubscriptionManager.notifyChangeAsync(CURRENT_QUESTION_PROVIDER_ID) + return dataProviders.createInMemoryDataProvider( + SUBMIT_SOLUTION_REVEALED_RESULT_PROVIDER_ID + ) { null } } } catch (e: Exception) { exceptionsController.logNonFatalException(e) - return MutableLiveData(AsyncResult.Failure(e)) + return dataProviders.createInMemoryDataProviderAsync( + SUBMIT_SOLUTION_REVEALED_RESULT_PROVIDER_ID + ) { AsyncResult.Failure(e) } } } @@ -297,7 +338,7 @@ class QuestionAssessmentProgressController @Inject constructor( * listen to this result for failures, and instead rely on [getCurrentQuestion] for observing * a successful transition to another question. */ - fun moveToNextQuestion(): LiveData> { + fun moveToNextQuestion(): DataProvider { try { progressLock.withLock { check(progress.trainStage != TrainStage.NOT_IN_TRAINING_SESSION) { @@ -314,12 +355,16 @@ class QuestionAssessmentProgressController @Inject constructor( hintHandler.navigateBackToLatestPendingState() progress.processNavigationToNewQuestion() } - asyncDataSubscriptionManager.notifyChangeAsync(CREATE_CURRENT_QUESTION_DATA_PROVIDER_ID) + asyncDataSubscriptionManager.notifyChangeAsync(CURRENT_QUESTION_PROVIDER_ID) + } + return dataProviders.createInMemoryDataProvider(MOVE_TO_NEXT_QUESTION_RESULT_PROVIDER_ID) { + null } - return MutableLiveData(AsyncResult.Success(null)) } catch (e: Exception) { exceptionsController.logNonFatalException(e) - return MutableLiveData(AsyncResult.Failure(e)) + return dataProviders.createInMemoryDataProviderAsync( + MOVE_TO_NEXT_QUESTION_RESULT_PROVIDER_ID + ) { AsyncResult.Failure(e) } } } @@ -346,7 +391,7 @@ class QuestionAssessmentProgressController @Inject constructor( * success or failure state back to pending. */ fun getCurrentQuestion(): DataProvider = progressLock.withLock { - val providerId = CREATE_CURRENT_QUESTION_DATA_WITH_TRANSLATION_CONTEXT_PROVIDER_ID + val providerId = LOCALIZED_QUESTION_PROVIDER_ID return translationController.getWrittenTranslationContentLocale( progress.currentProfileId ).combineWith(currentQuestionDataProvider, providerId) { contentLocale, currentQuestion -> @@ -363,9 +408,7 @@ class QuestionAssessmentProgressController @Inject constructor( */ fun calculateScores(skillIdList: List): DataProvider = progressLock.withLock { - return dataProviders.createInMemoryDataProviderAsync( - "user_assessment_performance" - ) { + dataProviders.createInMemoryDataProviderAsync(CALCULATE_SCORES_PROVIDER_ID) { retrieveUserAssessmentPerformanceAsync(skillIdList) } } @@ -388,7 +431,7 @@ class QuestionAssessmentProgressController @Inject constructor( questionsListDataProvider: DataProvider> ): NestedTransformedDataProvider { return questionsListDataProvider.transformNested( - CREATE_CURRENT_QUESTION_DATA_PROVIDER_ID, + CURRENT_QUESTION_PROVIDER_ID, this::retrieveCurrentQuestionAsync ) } @@ -464,8 +507,8 @@ class QuestionAssessmentProgressController @Inject constructor( /** Returns a temporary [DataProvider] that always provides an empty list of [Question]s. */ private fun createEmptyQuestionsListDataProvider(): DataProvider> { - return dataProviders.createInMemoryDataProvider(CREATE_EMPTY_QUESTIONS_LIST_DATA_PROVIDER_ID) { - listOf() + return dataProviders.createInMemoryDataProvider(EMPTY_QUESTIONS_LIST_DATA_PROVIDER_ID) { + listOf() } } } diff --git a/domain/src/main/java/org/oppia/android/domain/question/QuestionTrainingController.kt b/domain/src/main/java/org/oppia/android/domain/question/QuestionTrainingController.kt index 5fcab8a4455..e0fe4660ac8 100644 --- a/domain/src/main/java/org/oppia/android/domain/question/QuestionTrainingController.kt +++ b/domain/src/main/java/org/oppia/android/domain/question/QuestionTrainingController.kt @@ -11,13 +11,13 @@ import javax.inject.Inject import javax.inject.Singleton import kotlin.random.Random import org.oppia.android.util.data.AsyncResult +import org.oppia.android.util.data.DataProviders.Companion.combineWith +import org.oppia.android.util.data.DataProviders.Companion.combineWithAsync private const val RETRIEVE_QUESTION_FOR_SKILLS_ID_PROVIDER_ID = "retrieve_question_for_skills_id_provider_id" private const val START_QUESTION_TRAINING_SESSION_PROVIDER_ID = "start_question_training_session_provider_id" -private const val STOP_QUESTION_TRAINING_SESSION_PROVIDER_ID = - "stop_question_training_session_provider_id" /** Controller for retrieving a set of questions. */ @Singleton @@ -47,14 +47,19 @@ class QuestionTrainingController @Inject constructor( fun startQuestionTrainingSession( profileId: ProfileId, skillIdsList: List - ): DataProvider { + ): DataProvider { return try { val retrieveQuestionsDataProvider = retrieveQuestionsForSkillIds(skillIdsList) - questionAssessmentProgressController.beginQuestionTrainingSession( - retrieveQuestionsDataProvider, profileId - ) - // Convert the data provider type to 'Any' via a transformation. - retrieveQuestionsDataProvider.transform(START_QUESTION_TRAINING_SESSION_PROVIDER_ID) { it } + val beginSessionDataProvider = + questionAssessmentProgressController.beginQuestionTrainingSession( + questionsListDataProvider = retrieveQuestionsDataProvider, profileId + ) + // Combine the data providers to ensure their results are tied together, but only take the + // result from the begin session provider (since that's the one that indicates session start + // success/failure, assuming the questions loaded successfully). + retrieveQuestionsDataProvider.combineWith( + beginSessionDataProvider, START_QUESTION_TRAINING_SESSION_PROVIDER_ID + ) { _, sessionResult -> sessionResult } } catch (e: Exception) { exceptionsController.logNonFatalException(e) dataProviders.createInMemoryDataProviderAsync(START_QUESTION_TRAINING_SESSION_PROVIDER_ID) { @@ -112,15 +117,6 @@ class QuestionTrainingController @Inject constructor( * method should only be called if there is a training session is being played, otherwise an * exception will be thrown. */ - fun stopQuestionTrainingSession(): DataProvider { - return try { - questionAssessmentProgressController.finishQuestionTrainingSession() - dataProviders.createInMemoryDataProvider(STOP_QUESTION_TRAINING_SESSION_PROVIDER_ID) { } - } catch (e: Exception) { - exceptionsController.logNonFatalException(e) - dataProviders.createInMemoryDataProviderAsync(STOP_QUESTION_TRAINING_SESSION_PROVIDER_ID) { - AsyncResult.failed(e) - } - } - } + fun stopQuestionTrainingSession(): DataProvider = + questionAssessmentProgressController.finishQuestionTrainingSession() } diff --git a/domain/src/test/java/org/oppia/android/domain/exploration/ExplorationDataControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/exploration/ExplorationDataControllerTest.kt index 1c7d718ef49..ca95c130d9d 100644 --- a/domain/src/test/java/org/oppia/android/domain/exploration/ExplorationDataControllerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/exploration/ExplorationDataControllerTest.kt @@ -2,7 +2,6 @@ package org.oppia.android.domain.exploration import android.app.Application import android.content.Context -import androidx.lifecycle.Observer import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat @@ -10,19 +9,11 @@ import dagger.BindsInstance import dagger.Component import dagger.Module import dagger.Provides +import javax.inject.Inject +import javax.inject.Singleton import org.junit.Before -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentCaptor -import org.mockito.Captor -import org.mockito.Mock -import org.mockito.Mockito.atLeastOnce -import org.mockito.Mockito.verify -import org.mockito.junit.MockitoJUnit -import org.mockito.junit.MockitoRule -import org.oppia.android.app.model.EphemeralState -import org.oppia.android.app.model.Exploration import org.oppia.android.app.model.ExplorationCheckpoint import org.oppia.android.domain.classify.InteractionsModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule @@ -53,6 +44,7 @@ import org.oppia.android.domain.topic.TEST_TOPIC_ID_0 import org.oppia.android.domain.topic.TEST_TOPIC_ID_1 import org.oppia.android.testing.FakeExceptionLogger import org.oppia.android.testing.TestLogReportingModule +import org.oppia.android.testing.data.DataProviderTestMonitor import org.oppia.android.testing.environment.TestEnvironmentConfig import org.oppia.android.testing.lightweightcheckpointing.ExplorationCheckpointTestHelper import org.oppia.android.testing.robolectric.RobolectricModule @@ -63,8 +55,6 @@ import org.oppia.android.util.caching.AssetModule import org.oppia.android.util.caching.CacheAssetsLocally import org.oppia.android.util.caching.LoadLessonProtosFromAssets import org.oppia.android.util.caching.TopicListToCache -import org.oppia.android.util.data.AsyncResult -import org.oppia.android.util.data.DataProviders.Companion.toLiveData import org.oppia.android.util.data.DataProvidersInjector import org.oppia.android.util.data.DataProvidersInjectorProvider import org.oppia.android.util.locale.LocaleProdModule @@ -75,43 +65,20 @@ import org.oppia.android.util.logging.LogLevel import org.oppia.android.util.networking.NetworkConnectionUtilDebugModule import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode -import javax.inject.Inject -import javax.inject.Singleton /** Tests for [ExplorationDataController]. */ +// Function name: test names are conventionally named with underscores. +@Suppress("FunctionName") @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) @Config(application = ExplorationDataControllerTest.TestApplication::class) class ExplorationDataControllerTest { - @Rule - @JvmField - val mockitoRule: MockitoRule = MockitoJUnit.rule() - - @Inject - lateinit var explorationDataController: ExplorationDataController - - @Inject - lateinit var explorationProgressController: ExplorationProgressController - - @Inject - lateinit var fakeExceptionLogger: FakeExceptionLogger - - @Inject - lateinit var explorationCheckpointTestHelper: ExplorationCheckpointTestHelper - - @Inject - lateinit var testCoroutineDispatchers: TestCoroutineDispatchers - - @Mock - lateinit var mockCurrentStateLiveDataObserver: Observer> + @Inject lateinit var explorationDataController: ExplorationDataController + @Inject lateinit var fakeExceptionLogger: FakeExceptionLogger + @Inject lateinit var testCoroutineDispatchers: TestCoroutineDispatchers + @Inject lateinit var monitorFactory: DataProviderTestMonitor.Factory - @Mock - lateinit var mockExplorationObserver: Observer> - - @Captor - lateinit var explorationResultCaptor: ArgumentCaptor> - - val internalProfileId: Int = -1 + private val internalProfileId: Int = -1 @Before fun setUp() { @@ -123,96 +90,60 @@ class ExplorationDataControllerTest { } @Test - fun testController_providesInitialLiveDataForFractions0Exploration() { - val explorationLiveData = - explorationDataController.getExplorationById(FRACTIONS_EXPLORATION_ID_0).toLiveData() - explorationLiveData.observeForever(mockExplorationObserver) - testCoroutineDispatchers.runCurrent() - verify(mockExplorationObserver, atLeastOnce()).onChanged(explorationResultCaptor.capture()) + fun testController_providesInitialStateForFractions0Exploration() { + val explorationResult = explorationDataController.getExplorationById(FRACTIONS_EXPLORATION_ID_0) - assertThat(explorationResultCaptor.value.isSuccess()).isTrue() - assertThat(explorationResultCaptor.value.getOrThrow()).isNotNull() - val exploration = explorationResultCaptor.value.getOrThrow() + val exploration = monitorFactory.waitForNextSuccessfulResult(explorationResult) assertThat(exploration.title).isEqualTo("What is a Fraction?") assertThat(exploration.languageCode).isEqualTo("en") assertThat(exploration.statesCount).isEqualTo(25) } @Test - fun testController_providesInitialLiveDataForFractions1Exploration() { - val explorationLiveData = - explorationDataController.getExplorationById(FRACTIONS_EXPLORATION_ID_1).toLiveData() - explorationLiveData.observeForever(mockExplorationObserver) - testCoroutineDispatchers.runCurrent() + fun testController_providesInitialStateForFractions1Exploration() { + val explorationResult = explorationDataController.getExplorationById(FRACTIONS_EXPLORATION_ID_1) - verify(mockExplorationObserver, atLeastOnce()).onChanged(explorationResultCaptor.capture()) - assertThat(explorationResultCaptor.value.isSuccess()).isTrue() - assertThat(explorationResultCaptor.value.getOrThrow()).isNotNull() - val exploration = explorationResultCaptor.value.getOrThrow() + val exploration = monitorFactory.waitForNextSuccessfulResult(explorationResult) assertThat(exploration.title).isEqualTo("The Meaning of \"Equal Parts\"") assertThat(exploration.languageCode).isEqualTo("en") assertThat(exploration.statesCount).isEqualTo(18) } @Test - fun testController_providesInitialLiveDataForRatios0Exploration() { - val explorationLiveData = - explorationDataController.getExplorationById(RATIOS_EXPLORATION_ID_0).toLiveData() - explorationLiveData.observeForever(mockExplorationObserver) - testCoroutineDispatchers.runCurrent() + fun testController_providesInitialStateForRatios0Exploration() { + val explorationResult = explorationDataController.getExplorationById(RATIOS_EXPLORATION_ID_0) - verify(mockExplorationObserver, atLeastOnce()).onChanged(explorationResultCaptor.capture()) - assertThat(explorationResultCaptor.value.isSuccess()).isTrue() - assertThat(explorationResultCaptor.value.getOrThrow()).isNotNull() - val exploration = explorationResultCaptor.value.getOrThrow() + val exploration = monitorFactory.waitForNextSuccessfulResult(explorationResult) assertThat(exploration.title).isEqualTo("What is a Ratio?") assertThat(exploration.languageCode).isEqualTo("en") assertThat(exploration.statesCount).isEqualTo(26) } @Test - fun testController_providesInitialLiveDataForRatios1Exploration() { - val explorationLiveData = - explorationDataController.getExplorationById(RATIOS_EXPLORATION_ID_1).toLiveData() - explorationLiveData.observeForever(mockExplorationObserver) - testCoroutineDispatchers.runCurrent() + fun testController_providesInitialStateForRatios1Exploration() { + val explorationResult = explorationDataController.getExplorationById(RATIOS_EXPLORATION_ID_1) - verify(mockExplorationObserver, atLeastOnce()).onChanged(explorationResultCaptor.capture()) - assertThat(explorationResultCaptor.value.isSuccess()).isTrue() - assertThat(explorationResultCaptor.value.getOrThrow()).isNotNull() - val exploration = explorationResultCaptor.value.getOrThrow() + val exploration = monitorFactory.waitForNextSuccessfulResult(explorationResult) assertThat(exploration.title).isEqualTo("Order is Important") assertThat(exploration.languageCode).isEqualTo("en") assertThat(exploration.statesCount).isEqualTo(22) } @Test - fun testController_providesInitialLiveDataForRatios2Exploration() { - val explorationLiveData = - explorationDataController.getExplorationById(RATIOS_EXPLORATION_ID_2).toLiveData() - explorationLiveData.observeForever(mockExplorationObserver) - testCoroutineDispatchers.runCurrent() + fun testController_providesInitialStateForRatios2Exploration() { + val explorationResult = explorationDataController.getExplorationById(RATIOS_EXPLORATION_ID_2) - verify(mockExplorationObserver, atLeastOnce()).onChanged(explorationResultCaptor.capture()) - assertThat(explorationResultCaptor.value.isSuccess()).isTrue() - assertThat(explorationResultCaptor.value.getOrThrow()).isNotNull() - val exploration = explorationResultCaptor.value.getOrThrow() + val exploration = monitorFactory.waitForNextSuccessfulResult(explorationResult) assertThat(exploration.title).isEqualTo("Equivalent Ratios") assertThat(exploration.languageCode).isEqualTo("en") assertThat(exploration.statesCount).isEqualTo(24) } @Test - fun testController_providesInitialLiveDataForRatios3Exploration() { - val explorationLiveData = - explorationDataController.getExplorationById(RATIOS_EXPLORATION_ID_3).toLiveData() - explorationLiveData.observeForever(mockExplorationObserver) - testCoroutineDispatchers.runCurrent() + fun testController_providesInitialStateForRatios3Exploration() { + val explorationResult = explorationDataController.getExplorationById(RATIOS_EXPLORATION_ID_3) - verify(mockExplorationObserver, atLeastOnce()).onChanged(explorationResultCaptor.capture()) - assertThat(explorationResultCaptor.value.isSuccess()).isTrue() - assertThat(explorationResultCaptor.value.getOrThrow()).isNotNull() - val exploration = explorationResultCaptor.value.getOrThrow() + val exploration = monitorFactory.waitForNextSuccessfulResult(explorationResult) assertThat(exploration.title).isEqualTo("Writing Ratios in Simplest Form") assertThat(exploration.languageCode).isEqualTo("en") assertThat(exploration.statesCount).isEqualTo(21) @@ -220,13 +151,9 @@ class ExplorationDataControllerTest { @Test fun testController_returnsFailedForNonExistentExploration() { - val explorationLiveData = - explorationDataController.getExplorationById("NON_EXISTENT_TEST").toLiveData() - explorationLiveData.observeForever(mockExplorationObserver) - testCoroutineDispatchers.runCurrent() + val explorationResult = explorationDataController.getExplorationById("NON_EXISTENT_TEST") - verify(mockExplorationObserver).onChanged(explorationResultCaptor.capture()) - assertThat(explorationResultCaptor.value.isFailure()).isTrue() + monitorFactory.waitForNextFailureResult(explorationResult) val exception = fakeExceptionLogger.getMostRecentException() assertThat(exception).isInstanceOf(IllegalStateException::class.java) assertThat(exception).hasMessageThat().contains("Asset doesn't exist: NON_EXISTENT_TEST") @@ -234,13 +161,9 @@ class ExplorationDataControllerTest { @Test fun testController_returnsFailed_logsException() { - val explorationLiveData = - explorationDataController.getExplorationById("NON_EXISTENT_TEST").toLiveData() - explorationLiveData.observeForever(mockExplorationObserver) - testCoroutineDispatchers.runCurrent() + val explorationResult = explorationDataController.getExplorationById("NON_EXISTENT_TEST") - verify(mockExplorationObserver).onChanged(explorationResultCaptor.capture()) - assertThat(explorationResultCaptor.value.isFailure()).isTrue() + monitorFactory.waitForNextFailureResult(explorationResult) val exception = fakeExceptionLogger.getMostRecentException() assertThat(exception).isInstanceOf(IllegalStateException::class.java) assertThat(exception).hasMessageThat().contains("Asset doesn't exist: NON_EXISTENT_TEST") diff --git a/domain/src/test/java/org/oppia/android/domain/exploration/ExplorationProgressControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/exploration/ExplorationProgressControllerTest.kt index a87412cf98b..4e48ed5dc06 100644 --- a/domain/src/test/java/org/oppia/android/domain/exploration/ExplorationProgressControllerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/exploration/ExplorationProgressControllerTest.kt @@ -2,8 +2,6 @@ package org.oppia.android.domain.exploration import android.app.Application import android.content.Context -import androidx.lifecycle.LiveData -import androidx.lifecycle.Observer import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat @@ -12,18 +10,14 @@ import dagger.BindsInstance import dagger.Component import dagger.Module import dagger.Provides +import java.util.Locale +import java.util.concurrent.TimeUnit +import javax.inject.Inject +import javax.inject.Singleton import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentCaptor -import org.mockito.Captor -import org.mockito.Mock -import org.mockito.Mockito.atLeastOnce -import org.mockito.Mockito.reset -import org.mockito.Mockito.verify -import org.mockito.junit.MockitoJUnit -import org.mockito.junit.MockitoRule import org.oppia.android.app.model.AnswerOutcome import org.oppia.android.app.model.CheckpointState import org.oppia.android.app.model.ClickOnImage @@ -88,8 +82,6 @@ import org.oppia.android.util.caching.AssetModule import org.oppia.android.util.caching.CacheAssetsLocally import org.oppia.android.util.caching.LoadLessonProtosFromAssets import org.oppia.android.util.caching.TopicListToCache -import org.oppia.android.util.data.AsyncResult -import org.oppia.android.util.data.DataProviders.Companion.toLiveData import org.oppia.android.util.data.DataProvidersInjector import org.oppia.android.util.data.DataProvidersInjectorProvider import org.oppia.android.util.locale.LocaleProdModule @@ -100,10 +92,6 @@ import org.oppia.android.util.logging.LogLevel import org.oppia.android.util.networking.NetworkConnectionUtilDebugModule import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode -import java.util.Locale -import java.util.concurrent.TimeUnit -import javax.inject.Inject -import javax.inject.Singleton // For context: // https://github.com/oppia/oppia/blob/37285a/extensions/interactions/Continue/directives/oppia-interactive-continue.directive.ts. @@ -129,64 +117,18 @@ class ExplorationProgressControllerTest { // - testSubmitAnswer_whileSubmittingAnotherAnswer_failsWithError // - testMoveToPrevious_whileSubmittingAnswer_failsWithError - @Rule - @JvmField - val mockitoRule: MockitoRule = MockitoJUnit.rule() - @get:Rule val oppiaTestRule = OppiaTestRule() - @Inject - lateinit var context: Context - - @Inject - lateinit var explorationDataController: ExplorationDataController - - @Inject - lateinit var explorationProgressController: ExplorationProgressController - - @Inject - lateinit var fakeExceptionLogger: FakeExceptionLogger - - @Inject - lateinit var testCoroutineDispatchers: TestCoroutineDispatchers - - @Inject - lateinit var oppiaClock: FakeOppiaClock - - @Inject - lateinit var explorationCheckpointController: ExplorationCheckpointController - - // TODO(#3813): Migrate all tests in this suite to use this factory. - @Inject - lateinit var monitorFactory: DataProviderTestMonitor.Factory - - @Inject - lateinit var translationController: TranslationController - - @Mock - lateinit var mockAsyncResultLiveDataObserver: Observer> - - @Mock - lateinit var mockAsyncAnswerOutcomeObserver: Observer> - - @Mock - lateinit var mockAsyncHintObserver: Observer> - - @Mock - lateinit var mockAsyncSolutionObserver: Observer> - - @Captor - lateinit var asyncResultCaptor: ArgumentCaptor> - - @Captor - lateinit var asyncAnswerOutcomeCaptor: ArgumentCaptor> - - @Mock - lateinit var mockExplorationCheckpointObserver: Observer> - - @Captor - lateinit var explorationCheckpointCaptor: ArgumentCaptor> + @Inject lateinit var context: Context + @Inject lateinit var explorationDataController: ExplorationDataController + @Inject lateinit var explorationProgressController: ExplorationProgressController + @Inject lateinit var fakeExceptionLogger: FakeExceptionLogger + @Inject lateinit var testCoroutineDispatchers: TestCoroutineDispatchers + @Inject lateinit var oppiaClock: FakeOppiaClock + @Inject lateinit var explorationCheckpointController: ExplorationCheckpointController + @Inject lateinit var monitorFactory: DataProviderTestMonitor.Factory + @Inject lateinit var translationController: TranslationController private val profileId = ProfileId.newBuilder().setInternalId(0).build() @@ -205,7 +147,7 @@ class ExplorationProgressControllerTest { @Test fun testPlayExploration_invalid_returnsSuccess() { - val resultLiveData = + val resultDataProvider = explorationDataController.startPlayingExploration( profileId.internalId, INVALID_TOPIC_ID, @@ -214,13 +156,10 @@ class ExplorationProgressControllerTest { shouldSavePartialProgress = false, explorationCheckpoint = ExplorationCheckpoint.getDefaultInstance() ) - resultLiveData.observeForever(mockAsyncResultLiveDataObserver) - testCoroutineDispatchers.runCurrent() // An invalid exploration is not known until it's fully loaded, and that's observed via // getCurrentState. - verify(mockAsyncResultLiveDataObserver).onChanged(asyncResultCaptor.capture()) - assertThat(asyncResultCaptor.value.isSuccess()).isTrue() + monitorFactory.waitForNextSuccessfulResult(resultDataProvider) } @Test @@ -241,7 +180,7 @@ class ExplorationProgressControllerTest { @Test fun testPlayExploration_valid_returnsSuccess() { - val resultLiveData = + val resultDataProvider = explorationDataController.startPlayingExploration( profileId.internalId, TEST_TOPIC_ID_0, @@ -250,11 +189,8 @@ class ExplorationProgressControllerTest { shouldSavePartialProgress = false, explorationCheckpoint = ExplorationCheckpoint.getDefaultInstance() ) - resultLiveData.observeForever(mockAsyncResultLiveDataObserver) - testCoroutineDispatchers.runCurrent() - verify(mockAsyncResultLiveDataObserver).onChanged(asyncResultCaptor.capture()) - assertThat(asyncResultCaptor.value.isSuccess()).isTrue() + monitorFactory.waitForNextSuccessfulResult(resultDataProvider) } @Test @@ -306,15 +242,14 @@ class ExplorationProgressControllerTest { assertThat(ephemeralState.state.name).isEqualTo("Continue") } + // TODO: add pending cases for start/stop playing exploration methods. + @Test fun testFinishExploration_beforePlaying_failWithError() { - val resultLiveData = explorationDataController.stopPlayingExploration() - resultLiveData.observeForever(mockAsyncResultLiveDataObserver) - testCoroutineDispatchers.runCurrent() + val resultDataProvider = explorationDataController.stopPlayingExploration() - verify(mockAsyncResultLiveDataObserver).onChanged(asyncResultCaptor.capture()) - assertThat(asyncResultCaptor.value.isFailure()).isTrue() - assertThat(asyncResultCaptor.value.getErrorOrNull()) + val error = monitorFactory.waitForNextFailureResult(resultDataProvider) + assertThat(error) .hasMessageThat() .contains("Cannot finish playing an exploration that hasn't yet been started") } @@ -332,7 +267,7 @@ class ExplorationProgressControllerTest { waitForGetCurrentStateSuccessfulLoad() // Try playing another exploration without finishing the previous one. - val resultLiveData = + val resultDataProvider = explorationDataController.startPlayingExploration( profileId.internalId, TEST_TOPIC_ID_0, @@ -341,12 +276,9 @@ class ExplorationProgressControllerTest { shouldSavePartialProgress = false, explorationCheckpoint = ExplorationCheckpoint.getDefaultInstance() ) - resultLiveData.observeForever(mockAsyncResultLiveDataObserver) - testCoroutineDispatchers.runCurrent() - verify(mockAsyncResultLiveDataObserver).onChanged(asyncResultCaptor.capture()) - assertThat(asyncResultCaptor.value.isFailure()).isTrue() - assertThat(asyncResultCaptor.value.getErrorOrNull()) + val error = monitorFactory.waitForNextFailureResult(resultDataProvider) + assertThat(error) .hasMessageThat() .contains("Expected to finish previous exploration before starting a new one.") } @@ -385,50 +317,15 @@ class ExplorationProgressControllerTest { @Test fun testSubmitAnswer_beforePlaying_failsWithError() { - val result = - explorationProgressController.submitAnswer(createMultipleChoiceAnswer(0)) - result.observeForever(mockAsyncAnswerOutcomeObserver) - testCoroutineDispatchers.runCurrent() + val result = explorationProgressController.submitAnswer(createMultipleChoiceAnswer(0)) // Verify that the answer submission failed. - verify( - mockAsyncAnswerOutcomeObserver, - atLeastOnce() - ).onChanged(asyncAnswerOutcomeCaptor.capture()) - assertThat(asyncAnswerOutcomeCaptor.value.isFailure()).isTrue() - assertThat(asyncAnswerOutcomeCaptor.value.getErrorOrNull()) + val error = monitorFactory.waitForNextFailureResult(result) + assertThat(error) .hasMessageThat() .contains("Cannot submit an answer if an exploration is not being played.") } - @Test - fun testSubmitAnswer_whileLoading_failsWithError() { - // Start playing an exploration, but don't wait for it to complete. - explorationDataController.startPlayingExploration( - profileId.internalId, - TEST_TOPIC_ID_0, - TEST_STORY_ID_0, - TEST_EXPLORATION_ID_2, - shouldSavePartialProgress = false, - explorationCheckpoint = ExplorationCheckpoint.getDefaultInstance() - ) - - val result = - explorationProgressController.submitAnswer(createMultipleChoiceAnswer(0)) - result.observeForever(mockAsyncAnswerOutcomeObserver) - testCoroutineDispatchers.runCurrent() - - // Verify that the answer submission failed. - verify( - mockAsyncAnswerOutcomeObserver, - atLeastOnce() - ).onChanged(asyncAnswerOutcomeCaptor.capture()) - assertThat(asyncAnswerOutcomeCaptor.value.isFailure()).isTrue() - assertThat(asyncAnswerOutcomeCaptor.value.getErrorOrNull()) - .hasMessageThat() - .contains("Cannot submit an answer while the exploration is being loaded.") - } - @Test fun testSubmitAnswer_forMultipleChoice_correctAnswer_succeeds() { playExploration( @@ -442,17 +339,10 @@ class ExplorationProgressControllerTest { waitForGetCurrentStateSuccessfulLoad() navigateToPrototypeMultipleChoiceState() - val result = - explorationProgressController.submitAnswer(createMultipleChoiceAnswer(2)) - result.observeForever(mockAsyncAnswerOutcomeObserver) - testCoroutineDispatchers.runCurrent() + val result = explorationProgressController.submitAnswer(createMultipleChoiceAnswer(2)) // Verify that the answer submission was successful. - verify( - mockAsyncAnswerOutcomeObserver, - atLeastOnce() - ).onChanged(asyncAnswerOutcomeCaptor.capture()) - assertThat(asyncAnswerOutcomeCaptor.value.isSuccess()).isTrue() + monitorFactory.waitForNextSuccessfulResult(result) } @Test @@ -468,17 +358,10 @@ class ExplorationProgressControllerTest { waitForGetCurrentStateSuccessfulLoad() navigateToPrototypeMultipleChoiceState() - val result = - explorationProgressController.submitAnswer(createMultipleChoiceAnswer(2)) - result.observeForever(mockAsyncAnswerOutcomeObserver) - testCoroutineDispatchers.runCurrent() + val result = explorationProgressController.submitAnswer(createMultipleChoiceAnswer(2)) // Verify that the answer submission was successful. - verify( - mockAsyncAnswerOutcomeObserver, - atLeastOnce() - ).onChanged(asyncAnswerOutcomeCaptor.capture()) - val answerOutcome = asyncAnswerOutcomeCaptor.value.getOrThrow() + val answerOutcome = monitorFactory.waitForNextSuccessfulResult(result) assertThat(answerOutcome.destinationCase).isEqualTo(AnswerOutcome.DestinationCase.STATE_NAME) assertThat(answerOutcome.feedback.html).contains("Correct!") } @@ -496,17 +379,10 @@ class ExplorationProgressControllerTest { waitForGetCurrentStateSuccessfulLoad() navigateToPrototypeMultipleChoiceState() - val result = - explorationProgressController.submitAnswer(createMultipleChoiceAnswer(0)) - result.observeForever(mockAsyncAnswerOutcomeObserver) - testCoroutineDispatchers.runCurrent() + val result = explorationProgressController.submitAnswer(createMultipleChoiceAnswer(0)) // Verify that the answer submission was successful. - verify( - mockAsyncAnswerOutcomeObserver, - atLeastOnce() - ).onChanged(asyncAnswerOutcomeCaptor.capture()) - assertThat(asyncAnswerOutcomeCaptor.value.isSuccess()).isTrue() + monitorFactory.waitForNextSuccessfulResult(result) } @Test @@ -522,17 +398,10 @@ class ExplorationProgressControllerTest { waitForGetCurrentStateSuccessfulLoad() navigateToPrototypeMultipleChoiceState() - val result = - explorationProgressController.submitAnswer(createMultipleChoiceAnswer(0)) - result.observeForever(mockAsyncAnswerOutcomeObserver) - testCoroutineDispatchers.runCurrent() + val result = explorationProgressController.submitAnswer(createMultipleChoiceAnswer(0)) // Verify that the answer submission was successful. - verify( - mockAsyncAnswerOutcomeObserver, - atLeastOnce() - ).onChanged(asyncAnswerOutcomeCaptor.capture()) - val answerOutcome = asyncAnswerOutcomeCaptor.value.getOrThrow() + val answerOutcome = monitorFactory.waitForNextSuccessfulResult(result) assertThat(answerOutcome.destinationCase).isEqualTo(AnswerOutcome.DestinationCase.SAME_STATE) assertThat(answerOutcome.feedback.html).contains("Try again.") } @@ -615,37 +484,13 @@ class ExplorationProgressControllerTest { @Test fun testMoveToNext_beforePlaying_failsWithError() { val moveToStateResult = explorationProgressController.moveToNextState() - moveToStateResult.observeForever(mockAsyncResultLiveDataObserver) - verify(mockAsyncResultLiveDataObserver, atLeastOnce()).onChanged(asyncResultCaptor.capture()) - assertThat(asyncResultCaptor.value.isFailure()).isTrue() - assertThat(asyncResultCaptor.value.getErrorOrNull()) + val error = monitorFactory.waitForNextFailureResult(moveToStateResult) + assertThat(error) .hasMessageThat() .contains("Cannot navigate to a next state if an exploration is not being played.") } - @Test - fun testMoveToNext_whileLoadingExploration_failsWithError() { - // Start playing an exploration, but don't wait for it to complete. - explorationDataController.startPlayingExploration( - profileId.internalId, - TEST_TOPIC_ID_0, - TEST_STORY_ID_0, - TEST_EXPLORATION_ID_2, - shouldSavePartialProgress = false, - explorationCheckpoint = ExplorationCheckpoint.getDefaultInstance() - ) - - val moveToStateResult = explorationProgressController.moveToNextState() - moveToStateResult.observeForever(mockAsyncResultLiveDataObserver) - - verify(mockAsyncResultLiveDataObserver, atLeastOnce()).onChanged(asyncResultCaptor.capture()) - assertThat(asyncResultCaptor.value.isFailure()).isTrue() - assertThat(asyncResultCaptor.value.getErrorOrNull()) - .hasMessageThat() - .contains("Cannot navigate to a next state if an exploration is being loaded.") - } - @Test fun testMoveToNext_forPendingInitialState_failsWithError() { playExploration( @@ -659,13 +504,10 @@ class ExplorationProgressControllerTest { waitForGetCurrentStateSuccessfulLoad() val moveToStateResult = explorationProgressController.moveToNextState() - moveToStateResult.observeForever(mockAsyncResultLiveDataObserver) - testCoroutineDispatchers.runCurrent() // Verify that we can't move ahead since the current state isn't yet completed. - verify(mockAsyncResultLiveDataObserver, atLeastOnce()).onChanged(asyncResultCaptor.capture()) - assertThat(asyncResultCaptor.value.isFailure()).isTrue() - assertThat(asyncResultCaptor.value.getErrorOrNull()) + val error = monitorFactory.waitForNextFailureResult(moveToStateResult) + assertThat(error) .hasMessageThat() .contains("Cannot navigate to next state; at most recent state.") } @@ -684,11 +526,8 @@ class ExplorationProgressControllerTest { submitPrototypeState1Answer() val moveToStateResult = explorationProgressController.moveToNextState() - moveToStateResult.observeForever(mockAsyncResultLiveDataObserver) - testCoroutineDispatchers.runCurrent() - verify(mockAsyncResultLiveDataObserver, atLeastOnce()).onChanged(asyncResultCaptor.capture()) - assertThat(asyncResultCaptor.value.isSuccess()).isTrue() + monitorFactory.waitForNextSuccessfulResult(moveToStateResult) } @Test @@ -725,57 +564,26 @@ class ExplorationProgressControllerTest { moveToNextState() // Try skipping past the current state. - val moveToStateResult = - explorationProgressController.moveToNextState() - moveToStateResult.observeForever(mockAsyncResultLiveDataObserver) - testCoroutineDispatchers.runCurrent() + val moveToStateResult = explorationProgressController.moveToNextState() // Verify we can't move ahead since the new state isn't yet completed. - verify(mockAsyncResultLiveDataObserver, atLeastOnce()).onChanged(asyncResultCaptor.capture()) - assertThat(asyncResultCaptor.value.isFailure()).isTrue() - assertThat(asyncResultCaptor.value.getErrorOrNull()) + val error = monitorFactory.waitForNextFailureResult(moveToStateResult) + assertThat(error) .hasMessageThat() .contains("Cannot navigate to next state; at most recent state.") } @Test fun testMoveToPrevious_beforePlaying_failsWithError() { - val moveToStateResult = - explorationProgressController.moveToPreviousState() - moveToStateResult.observeForever(mockAsyncResultLiveDataObserver) + val moveToStateResult = explorationProgressController.moveToPreviousState() testCoroutineDispatchers.runCurrent() - verify(mockAsyncResultLiveDataObserver, atLeastOnce()).onChanged(asyncResultCaptor.capture()) - assertThat(asyncResultCaptor.value.isFailure()).isTrue() - assertThat(asyncResultCaptor.value.getErrorOrNull()) + val error = monitorFactory.waitForNextFailureResult(moveToStateResult) + assertThat(error) .hasMessageThat() .contains("Cannot navigate to a previous state if an exploration is not being played.") } - @Test - fun testMoveToPrevious_whileLoadingExploration_failsWithError() { - // Start playing an exploration, but don't wait for it to complete. - explorationDataController.startPlayingExploration( - profileId.internalId, - TEST_TOPIC_ID_0, - TEST_STORY_ID_0, - TEST_EXPLORATION_ID_2, - shouldSavePartialProgress = false, - explorationCheckpoint = ExplorationCheckpoint.getDefaultInstance() - ) - - val moveToStateResult = - explorationProgressController.moveToPreviousState() - moveToStateResult.observeForever(mockAsyncResultLiveDataObserver) - testCoroutineDispatchers.runCurrent() - - verify(mockAsyncResultLiveDataObserver, atLeastOnce()).onChanged(asyncResultCaptor.capture()) - assertThat(asyncResultCaptor.value.isFailure()).isTrue() - assertThat(asyncResultCaptor.value.getErrorOrNull()) - .hasMessageThat() - .contains("Cannot navigate to a previous state if an exploration is being loaded.") - } - @Test fun testMoveToPrevious_onPendingInitialState_failsWithError() { playExploration( @@ -788,15 +596,11 @@ class ExplorationProgressControllerTest { ) waitForGetCurrentStateSuccessfulLoad() - val moveToStateResult = - explorationProgressController.moveToPreviousState() - moveToStateResult.observeForever(mockAsyncResultLiveDataObserver) - testCoroutineDispatchers.runCurrent() + val moveToStateResult = explorationProgressController.moveToPreviousState() // Verify we can't move behind since the current state is the initial exploration state. - verify(mockAsyncResultLiveDataObserver, atLeastOnce()).onChanged(asyncResultCaptor.capture()) - assertThat(asyncResultCaptor.value.isFailure()).isTrue() - assertThat(asyncResultCaptor.value.getErrorOrNull()) + val error = monitorFactory.waitForNextFailureResult(moveToStateResult) + assertThat(error) .hasMessageThat() .contains("Cannot navigate to previous state; at initial state.") } @@ -814,15 +618,11 @@ class ExplorationProgressControllerTest { waitForGetCurrentStateSuccessfulLoad() submitPrototypeState1Answer() - val moveToStateResult = - explorationProgressController.moveToPreviousState() - moveToStateResult.observeForever(mockAsyncResultLiveDataObserver) - testCoroutineDispatchers.runCurrent() + val moveToStateResult = explorationProgressController.moveToPreviousState() // Still can't navigate behind for a completed initial state since there's no previous state. - verify(mockAsyncResultLiveDataObserver, atLeastOnce()).onChanged(asyncResultCaptor.capture()) - assertThat(asyncResultCaptor.value.isFailure()).isTrue() - assertThat(asyncResultCaptor.value.getErrorOrNull()) + val error = monitorFactory.waitForNextFailureResult(moveToStateResult) + assertThat(error) .hasMessageThat() .contains("Cannot navigate to previous state; at initial state.") } @@ -840,15 +640,11 @@ class ExplorationProgressControllerTest { waitForGetCurrentStateSuccessfulLoad() playThroughPrototypeState1AndMoveToNextState() - val moveToStateResult = - explorationProgressController.moveToPreviousState() - moveToStateResult.observeForever(mockAsyncResultLiveDataObserver) - testCoroutineDispatchers.runCurrent() + val moveToStateResult = explorationProgressController.moveToPreviousState() // Verify that we can navigate to the previous state since the current state is complete and not // initial. - verify(mockAsyncResultLiveDataObserver, atLeastOnce()).onChanged(asyncResultCaptor.capture()) - assertThat(asyncResultCaptor.value.isSuccess()).isTrue() + monitorFactory.waitForNextSuccessfulResult(moveToStateResult) } @Test @@ -887,16 +683,12 @@ class ExplorationProgressControllerTest { playThroughPrototypeState1AndMoveToNextState() moveToPreviousState() - val moveToStateResult = - explorationProgressController.moveToPreviousState() - moveToStateResult.observeForever(mockAsyncResultLiveDataObserver) - testCoroutineDispatchers.runCurrent() + val moveToStateResult = explorationProgressController.moveToPreviousState() // The first previous navigation should succeed (see above), but the second will fail since // we're back at the initial state. - verify(mockAsyncResultLiveDataObserver, atLeastOnce()).onChanged(asyncResultCaptor.capture()) - assertThat(asyncResultCaptor.value.isFailure()).isTrue() - assertThat(asyncResultCaptor.value.getErrorOrNull()) + val error = monitorFactory.waitForNextFailureResult(moveToStateResult) + assertThat(error) .hasMessageThat() .contains("Cannot navigate to previous state; at initial state.") } @@ -914,17 +706,10 @@ class ExplorationProgressControllerTest { waitForGetCurrentStateSuccessfulLoad() navigateToPrototypeTextInputState() - val result = - explorationProgressController.submitAnswer(createTextInputAnswer("Finnish")) - result.observeForever(mockAsyncAnswerOutcomeObserver) - testCoroutineDispatchers.runCurrent() + val result = explorationProgressController.submitAnswer(createTextInputAnswer("Finnish")) // Verify that the answer submission was successful. - verify( - mockAsyncAnswerOutcomeObserver, - atLeastOnce() - ).onChanged(asyncAnswerOutcomeCaptor.capture()) - val answerOutcome = asyncAnswerOutcomeCaptor.value.getOrThrow() + val answerOutcome = monitorFactory.waitForNextSuccessfulResult(result) assertThat(answerOutcome.destinationCase).isEqualTo(AnswerOutcome.DestinationCase.STATE_NAME) assertThat(answerOutcome.feedback.html).contains("Correct!") } @@ -942,18 +727,11 @@ class ExplorationProgressControllerTest { waitForGetCurrentStateSuccessfulLoad() navigateToPrototypeTextInputState() - val result = - explorationProgressController.submitAnswer(createTextInputAnswer("Klingon")) - result.observeForever(mockAsyncAnswerOutcomeObserver) - testCoroutineDispatchers.runCurrent() + val result = explorationProgressController.submitAnswer(createTextInputAnswer("Klingon")) // Verify that the answer was wrong, and that there's no handler for it so the default outcome // is returned. - verify( - mockAsyncAnswerOutcomeObserver, - atLeastOnce() - ).onChanged(asyncAnswerOutcomeCaptor.capture()) - val answerOutcome = asyncAnswerOutcomeCaptor.value.getOrThrow() + val answerOutcome = monitorFactory.waitForNextSuccessfulResult(result) assertThat(answerOutcome.destinationCase).isEqualTo(AnswerOutcome.DestinationCase.SAME_STATE) assertThat(answerOutcome.feedback.html).contains("Not quite.") } @@ -1000,7 +778,9 @@ class ExplorationProgressControllerTest { // Submit 2 wrong answers to trigger a hint becoming available. submitWrongAnswerForPrototypeState2() submitWrongAnswerForPrototypeState2() - verifyOperationSucceeds(explorationProgressController.submitHintIsRevealed(hintIndex = 0)) + monitorFactory.waitForNextSuccessfulResult( + explorationProgressController.submitHintIsRevealed(hintIndex = 0) + ) // Verify that the current state updates. It should stay pending, on submission of wrong answer. val ephemeralState = waitForGetCurrentStateSuccessfulLoad() @@ -1026,15 +806,17 @@ class ExplorationProgressControllerTest { submitWrongAnswerForPrototypeState2() submitWrongAnswerForPrototypeState2() // Reveal the hint, then submit another wrong answer to trigger the solution. - verifyOperationSucceeds(explorationProgressController.submitHintIsRevealed(hintIndex = 0)) + monitorFactory.waitForNextSuccessfulResult( + explorationProgressController.submitHintIsRevealed(hintIndex = 0) + ) submitWrongAnswerForPrototypeState2() testCoroutineDispatchers.advanceTimeBy(TimeUnit.SECONDS.toMillis(10)) // Verify that the current state updates. It should stay pending, on submission of wrong answer. waitForGetCurrentStateSuccessfulLoad() - val result = explorationProgressController.submitSolutionIsRevealed() - result.observeForever(mockAsyncSolutionObserver) - testCoroutineDispatchers.runCurrent() + monitorFactory.waitForNextSuccessfulResult( + explorationProgressController.submitSolutionIsRevealed() + ) // Verify that the current state updates. Solution revealed is true. val ephemeralState = waitForGetCurrentStateSuccessfulLoad() @@ -1126,11 +908,10 @@ class ExplorationProgressControllerTest { submitWrongAnswerForPrototypeState2() val result = explorationProgressController.submitHintIsRevealed(hintIndex = 0) - result.observeForever(mockAsyncHintObserver) - testCoroutineDispatchers.runCurrent() // Verify that the helpIndex.IndexTypeCase is equal LATEST_REVEALED_HINT_INDEX because a new // revealed hint is visible. + monitorFactory.waitForNextSuccessfulResult(result) val ephemeralState = waitForGetCurrentStateSuccessfulLoad() assertThat(ephemeralState.isHintRevealed(0)).isTrue() assertThat(ephemeralState.isSolutionRevealed()).isFalse() @@ -1154,8 +935,7 @@ class ExplorationProgressControllerTest { submitWrongAnswerForPrototypeState2() submitWrongAnswerForPrototypeState2() - val result = explorationProgressController.submitHintIsRevealed(hintIndex = 0) - result.observeForever(mockAsyncHintObserver) + explorationProgressController.submitHintIsRevealed(hintIndex = 0) testCoroutineDispatchers.runCurrent() // The solution should be visible after 30 seconds of the last hint being reveled. @@ -1186,8 +966,7 @@ class ExplorationProgressControllerTest { submitWrongAnswerForPrototypeState2() submitWrongAnswerForPrototypeState2() - val result = explorationProgressController.submitHintIsRevealed(hintIndex = 0) - result.observeForever(mockAsyncHintObserver) + explorationProgressController.submitHintIsRevealed(hintIndex = 0) testCoroutineDispatchers.runCurrent() submitWrongAnswerForPrototypeState2() @@ -1219,16 +998,14 @@ class ExplorationProgressControllerTest { submitWrongAnswerForPrototypeState2() submitWrongAnswerForPrototypeState2() - val hintResult = explorationProgressController.submitHintIsRevealed(hintIndex = 0) - hintResult.observeForever(mockAsyncHintObserver) + explorationProgressController.submitHintIsRevealed(hintIndex = 0) testCoroutineDispatchers.runCurrent() // The solution should be visible after 30 seconds of the last hint being reveled. testCoroutineDispatchers.advanceTimeBy(TimeUnit.SECONDS.toMillis(30)) testCoroutineDispatchers.runCurrent() - val solutionResult = explorationProgressController.submitSolutionIsRevealed() - solutionResult.observeForever(mockAsyncSolutionObserver) + explorationProgressController.submitSolutionIsRevealed() testCoroutineDispatchers.runCurrent() // Verify that the helpIndex.IndexTypeCase is equal EVERYTHING_IS_REVEALED because a new the @@ -1281,9 +1058,7 @@ class ExplorationProgressControllerTest { waitForGetCurrentStateSuccessfulLoad() navigateToPrototypeTextInputState() - val result = - explorationProgressController.submitAnswer(createTextInputAnswer("Finnish")) - result.observeForever(mockAsyncAnswerOutcomeObserver) + explorationProgressController.submitAnswer(createTextInputAnswer("Finnish")) testCoroutineDispatchers.runCurrent() // Verify that the current state updates. It should now be completed with the correct answer. @@ -1308,9 +1083,7 @@ class ExplorationProgressControllerTest { waitForGetCurrentStateSuccessfulLoad() navigateToPrototypeTextInputState() - val result = - explorationProgressController.submitAnswer(createTextInputAnswer("Finnish ")) - result.observeForever(mockAsyncAnswerOutcomeObserver) + explorationProgressController.submitAnswer(createTextInputAnswer("Finnish ")) testCoroutineDispatchers.runCurrent() // Verify that the current state updates. The submitted answer should have a textual version @@ -1337,9 +1110,7 @@ class ExplorationProgressControllerTest { waitForGetCurrentStateSuccessfulLoad() navigateToPrototypeTextInputState() - val result = - explorationProgressController.submitAnswer(createTextInputAnswer("Klingon")) - result.observeForever(mockAsyncAnswerOutcomeObserver) + explorationProgressController.submitAnswer(createTextInputAnswer("Klingon")) testCoroutineDispatchers.runCurrent() // Verify that the current state updates. It should stay pending, and the wrong answer should be @@ -1524,15 +1295,9 @@ class ExplorationProgressControllerTest { navigateToPrototypeNumericInputState() val result = explorationProgressController.submitAnswer(createNumericInputAnswer(121.0)) - result.observeForever(mockAsyncAnswerOutcomeObserver) - testCoroutineDispatchers.runCurrent() // Verify that the answer submission was successful. - verify( - mockAsyncAnswerOutcomeObserver, - atLeastOnce() - ).onChanged(asyncAnswerOutcomeCaptor.capture()) - val answerOutcome = asyncAnswerOutcomeCaptor.value.getOrThrow() + val answerOutcome = monitorFactory.waitForNextSuccessfulResult(result) assertThat(answerOutcome.destinationCase).isEqualTo(AnswerOutcome.DestinationCase.STATE_NAME) assertThat(answerOutcome.feedback.html).contains("Correct!") } @@ -1550,18 +1315,10 @@ class ExplorationProgressControllerTest { waitForGetCurrentStateSuccessfulLoad() navigateToPrototypeNumericInputState() - val result = explorationProgressController.submitAnswer( - createNumericInputAnswer(122.0) - ) - result.observeForever(mockAsyncAnswerOutcomeObserver) - testCoroutineDispatchers.runCurrent() + val result = explorationProgressController.submitAnswer(createNumericInputAnswer(122.0)) // Verify that the answer submission failed as expected. - verify( - mockAsyncAnswerOutcomeObserver, - atLeastOnce() - ).onChanged(asyncAnswerOutcomeCaptor.capture()) - val answerOutcome = asyncAnswerOutcomeCaptor.value.getOrThrow() + val answerOutcome = monitorFactory.waitForNextSuccessfulResult(result) assertThat(answerOutcome.destinationCase).isEqualTo(AnswerOutcome.DestinationCase.SAME_STATE) assertThat(answerOutcome.feedback.html).contains("It's less than that.") } @@ -1580,15 +1337,9 @@ class ExplorationProgressControllerTest { // The first state of the exploration is the Continue interaction. val result = explorationProgressController.submitAnswer(createContinueButtonAnswer()) - result.observeForever(mockAsyncAnswerOutcomeObserver) - testCoroutineDispatchers.runCurrent() // Verify that the continue button succeeds by default. - verify( - mockAsyncAnswerOutcomeObserver, - atLeastOnce() - ).onChanged(asyncAnswerOutcomeCaptor.capture()) - val answerOutcome = asyncAnswerOutcomeCaptor.value.getOrThrow() + val answerOutcome = monitorFactory.waitForNextSuccessfulResult(result) assertThat(answerOutcome.destinationCase).isEqualTo(AnswerOutcome.DestinationCase.STATE_NAME) assertThat(answerOutcome.feedback.html).contains("Continuing onward") } @@ -1654,13 +1405,10 @@ class ExplorationProgressControllerTest { playThroughPrototypeExploration() val moveToStateResult = explorationProgressController.moveToNextState() - moveToStateResult.observeForever(mockAsyncResultLiveDataObserver) - testCoroutineDispatchers.runCurrent() // Verify we can't navigate past the last state of the exploration. - verify(mockAsyncResultLiveDataObserver, atLeastOnce()).onChanged(asyncResultCaptor.capture()) - assertThat(asyncResultCaptor.value.isFailure()).isTrue() - assertThat(asyncResultCaptor.value.getErrorOrNull()) + val error = monitorFactory.waitForNextFailureResult(moveToStateResult) + assertThat(error) .hasMessageThat() .contains("Cannot navigate to next state; at most recent state.") } @@ -1740,11 +1488,10 @@ class ExplorationProgressControllerTest { @Test fun testMoveToNext_beforePlaying_failsWithError_logsException() { - val moveToStateResult = - explorationProgressController.moveToNextState() - moveToStateResult.observeForever(mockAsyncResultLiveDataObserver) - val exception = fakeExceptionLogger.getMostRecentException() + explorationProgressController.moveToNextState() + testCoroutineDispatchers.runCurrent() + val exception = fakeExceptionLogger.getMostRecentException() assertThat(exception).isInstanceOf(IllegalStateException::class.java) assertThat(exception).hasMessageThat() .contains("Cannot navigate to a next state if an exploration is not being played.") @@ -1764,26 +1511,22 @@ class ExplorationProgressControllerTest { playThroughPrototypeState1AndMoveToNextState() moveToPreviousState() - val moveToStateResult = - explorationProgressController.moveToPreviousState() - moveToStateResult.observeForever(mockAsyncResultLiveDataObserver) + explorationProgressController.moveToPreviousState() testCoroutineDispatchers.runCurrent() - val exception = fakeExceptionLogger.getMostRecentException() + val exception = fakeExceptionLogger.getMostRecentException() assertThat(exception).isInstanceOf(IllegalStateException::class.java) - assertThat(exception).hasMessageThat() + assertThat(exception) + .hasMessageThat() .contains("Cannot navigate to previous state; at initial state.") } @Test fun testSubmitAnswer_beforePlaying_failsWithError_logsException() { - val result = explorationProgressController.submitAnswer( - createMultipleChoiceAnswer(0) - ) - result.observeForever(mockAsyncAnswerOutcomeObserver) + explorationProgressController.submitAnswer(createMultipleChoiceAnswer(0)) testCoroutineDispatchers.runCurrent() - val exception = fakeExceptionLogger.getMostRecentException() + val exception = fakeExceptionLogger.getMostRecentException() assertThat(exception).isInstanceOf(IllegalStateException::class.java) assertThat(exception).hasMessageThat() .contains("Cannot submit an answer if an exploration is not being played.") @@ -1799,10 +1542,10 @@ class ExplorationProgressControllerTest { shouldSavePartialProgress = false, explorationCheckpoint = ExplorationCheckpoint.getDefaultInstance() ) + waitForGetCurrentStateFailureLoad() val exception = fakeExceptionLogger.getMostRecentException() - assertThat(exception).isInstanceOf(IllegalStateException::class.java) assertThat(exception).hasMessageThat().contains("Asset doesn't exist: $INVALID_EXPLORATION_ID") } @@ -1819,13 +1562,12 @@ class ExplorationProgressControllerTest { ) waitForGetCurrentStateSuccessfulLoad() - val retrieveCheckpointLiveData = + val result = explorationCheckpointController.retrieveExplorationCheckpoint( - profileId, - TEST_EXPLORATION_ID_2 - ).toLiveData() + profileId, TEST_EXPLORATION_ID_2 + ) - verifyOperationSucceeds(retrieveCheckpointLiveData) + monitorFactory.waitForNextSuccessfulResult(result) } @Test @@ -2104,7 +1846,9 @@ class ExplorationProgressControllerTest { submitWrongAnswerForPrototypeState2() submitWrongAnswerForPrototypeState2() - verifyOperationSucceeds(explorationProgressController.submitHintIsRevealed(hintIndex = 0)) + monitorFactory.waitForNextSuccessfulResult( + explorationProgressController.submitHintIsRevealed(hintIndex = 0) + ) verifyCheckpointHasCorrectHelpIndex( profileId, TEST_EXPLORATION_ID_2, @@ -2131,7 +1875,9 @@ class ExplorationProgressControllerTest { submitWrongAnswerForPrototypeState2() submitWrongAnswerForPrototypeState2() // Reveal the hint, then submit another wrong answer to trigger the solution. - verifyOperationSucceeds(explorationProgressController.submitHintIsRevealed(hintIndex = 0)) + monitorFactory.waitForNextSuccessfulResult( + explorationProgressController.submitHintIsRevealed(hintIndex = 0) + ) submitWrongAnswerForPrototypeState2() testCoroutineDispatchers.advanceTimeBy(TimeUnit.SECONDS.toMillis(10)) @@ -2161,11 +1907,15 @@ class ExplorationProgressControllerTest { submitWrongAnswerForPrototypeState2() submitWrongAnswerForPrototypeState2() // Reveal the hint, then submit another wrong answer to trigger the solution. - verifyOperationSucceeds(explorationProgressController.submitHintIsRevealed(hintIndex = 0)) + monitorFactory.waitForNextSuccessfulResult( + explorationProgressController.submitHintIsRevealed(hintIndex = 0) + ) submitWrongAnswerForPrototypeState2() testCoroutineDispatchers.advanceTimeBy(TimeUnit.SECONDS.toMillis(10)) - verifyOperationSucceeds(explorationProgressController.submitSolutionIsRevealed()) + monitorFactory.waitForNextSuccessfulResult( + explorationProgressController.submitSolutionIsRevealed() + ) verifyCheckpointHasCorrectHelpIndex( profileId, TEST_EXPLORATION_ID_2, @@ -2689,7 +2439,9 @@ class ExplorationProgressControllerTest { playThroughPrototypeState1AndMoveToNextState() submitWrongAnswerForPrototypeState2() submitWrongAnswerForPrototypeState2() - verifyOperationSucceeds(explorationProgressController.submitHintIsRevealed(hintIndex = 0)) + monitorFactory.waitForNextSuccessfulResult( + explorationProgressController.submitHintIsRevealed(hintIndex = 0) + ) endExploration() playExploration( @@ -2724,7 +2476,9 @@ class ExplorationProgressControllerTest { playThroughPrototypeState1AndMoveToNextState() submitWrongAnswerForPrototypeState2() submitWrongAnswerForPrototypeState2() - verifyOperationSucceeds(explorationProgressController.submitHintIsRevealed(hintIndex = 0)) + monitorFactory.waitForNextSuccessfulResult( + explorationProgressController.submitHintIsRevealed(hintIndex = 0) + ) endExploration() playExploration( @@ -2762,7 +2516,9 @@ class ExplorationProgressControllerTest { playThroughPrototypeState1AndMoveToNextState() submitWrongAnswerForPrototypeState2() submitWrongAnswerForPrototypeState2() - verifyOperationSucceeds(explorationProgressController.submitHintIsRevealed(hintIndex = 0)) + monitorFactory.waitForNextSuccessfulResult( + explorationProgressController.submitHintIsRevealed(hintIndex = 0) + ) endExploration() playExploration( @@ -2785,7 +2541,7 @@ class ExplorationProgressControllerTest { } @Test - fun testCheckpointing_SolutionIsVisible_resumeExp_unrevealedSolutionIsVisibleOnPendingState() { + fun testCheckpointing_solutionIsVisible_resumeExp_unrevealedSolutionIsVisibleOnPendingState() { playExploration( profileId.internalId, TEST_TOPIC_ID_0, @@ -2801,7 +2557,9 @@ class ExplorationProgressControllerTest { submitWrongAnswerForPrototypeState2() // Reveal the hint, then submit another wrong answer to trigger the solution. - verifyOperationSucceeds(explorationProgressController.submitHintIsRevealed(hintIndex = 0)) + monitorFactory.waitForNextSuccessfulResult( + explorationProgressController.submitHintIsRevealed(hintIndex = 0) + ) testCoroutineDispatchers.advanceTimeBy(TimeUnit.SECONDS.toMillis(30)) endExploration() @@ -2839,14 +2597,15 @@ class ExplorationProgressControllerTest { playThroughPrototypeState1AndMoveToNextState() submitWrongAnswerForPrototypeState2() submitWrongAnswerForPrototypeState2() - verifyOperationSucceeds(explorationProgressController.submitHintIsRevealed(hintIndex = 0)) + monitorFactory.waitForNextSuccessfulResult( + explorationProgressController.submitHintIsRevealed(hintIndex = 0) + ) // The solution should be visible after 30 seconds of the last hint being reveled. testCoroutineDispatchers.advanceTimeBy(TimeUnit.SECONDS.toMillis(30)) testCoroutineDispatchers.runCurrent() - val solutionResult = explorationProgressController.submitSolutionIsRevealed() - solutionResult.observeForever(mockAsyncSolutionObserver) + explorationProgressController.submitSolutionIsRevealed() testCoroutineDispatchers.runCurrent() endExploration() @@ -3037,24 +2796,11 @@ class ExplorationProgressControllerTest { } private fun retrieveExplorationCheckpoint( - profileId: ProfileId, - explorationId: String + profileId: ProfileId, explorationId: String ): ExplorationCheckpoint { - testCoroutineDispatchers.runCurrent() - reset(mockExplorationCheckpointObserver) - val explorationCheckpointLiveData = - explorationCheckpointController.retrieveExplorationCheckpoint( - profileId, - explorationId - ).toLiveData() - explorationCheckpointLiveData.observeForever(mockExplorationCheckpointObserver) - testCoroutineDispatchers.runCurrent() - - verify(mockExplorationCheckpointObserver, atLeastOnce()) - .onChanged(explorationCheckpointCaptor.capture()) - assertThat(explorationCheckpointCaptor.value.isSuccess()).isTrue() - - return explorationCheckpointCaptor.value.getOrThrow() + val explorationCheckpointDataProvider = + explorationCheckpointController.retrieveExplorationCheckpoint(profileId, explorationId) + return monitorFactory.waitForNextSuccessfulResult(explorationCheckpointDataProvider) } private fun playExploration( @@ -3065,7 +2811,7 @@ class ExplorationProgressControllerTest { shouldSavePartialProgress: Boolean, explorationCheckpoint: ExplorationCheckpoint ) { - verifyOperationSucceeds( + monitorFactory.waitForNextSuccessfulResult( explorationDataController.startPlayingExploration( internalProfileId, topicId, @@ -3130,7 +2876,9 @@ class ExplorationProgressControllerTest { } private fun submitAnswer(userAnswer: UserAnswer): EphemeralState { - verifyOperationSucceeds(explorationProgressController.submitAnswer(userAnswer)) + monitorFactory.waitForNextSuccessfulResult( + explorationProgressController.submitAnswer(userAnswer) + ) return waitForGetCurrentStateSuccessfulLoad() } @@ -3309,17 +3057,17 @@ class ExplorationProgressControllerTest { } private fun moveToNextState(): EphemeralState { - verifyOperationSucceeds(explorationProgressController.moveToNextState()) + monitorFactory.waitForNextSuccessfulResult(explorationProgressController.moveToNextState()) return waitForGetCurrentStateSuccessfulLoad() } private fun moveToPreviousState(): EphemeralState { - verifyOperationSucceeds(explorationProgressController.moveToPreviousState()) + monitorFactory.waitForNextSuccessfulResult(explorationProgressController.moveToPreviousState()) return waitForGetCurrentStateSuccessfulLoad() } private fun endExploration() { - verifyOperationSucceeds(explorationDataController.stopPlayingExploration()) + monitorFactory.waitForNextSuccessfulResult(explorationDataController.stopPlayingExploration()) } private fun createContinueButtonAnswer() = @@ -3445,113 +3193,31 @@ class ExplorationProgressControllerTest { pendingState.helpIndex.isSolutionRevealed() private fun verifyCheckpointHasCorrectPendingStateName( - profileId: ProfileId, - explorationId: String, - pendingStateName: String + profileId: ProfileId, explorationId: String, pendingStateName: String ) { - testCoroutineDispatchers.runCurrent() - reset(mockExplorationCheckpointObserver) - val explorationCheckpointLiveData = - explorationCheckpointController.retrieveExplorationCheckpoint( - profileId, - explorationId - ).toLiveData() - explorationCheckpointLiveData.observeForever(mockExplorationCheckpointObserver) - testCoroutineDispatchers.runCurrent() - - verify(mockExplorationCheckpointObserver, atLeastOnce()) - .onChanged(explorationCheckpointCaptor.capture()) - assertThat(explorationCheckpointCaptor.value.isSuccess()).isTrue() - - assertThat(explorationCheckpointCaptor.value.getOrThrow().pendingStateName) - .isEqualTo(pendingStateName) + val checkpoint = retrieveExplorationCheckpoint(profileId, explorationId) + assertThat(checkpoint.pendingStateName).isEqualTo(pendingStateName) } private fun verifyCheckpointHasCorrectCountOfAnswers( - profileId: ProfileId, - explorationId: String, - countOfAnswers: Int + profileId: ProfileId, explorationId: String, countOfAnswers: Int ) { - testCoroutineDispatchers.runCurrent() - reset(mockExplorationCheckpointObserver) - val explorationCheckpointLiveData = - explorationCheckpointController.retrieveExplorationCheckpoint( - profileId, - explorationId - ).toLiveData() - explorationCheckpointLiveData.observeForever(mockExplorationCheckpointObserver) - testCoroutineDispatchers.runCurrent() - - verify(mockExplorationCheckpointObserver, atLeastOnce()) - .onChanged(explorationCheckpointCaptor.capture()) - assertThat(explorationCheckpointCaptor.value.isSuccess()).isTrue() - - assertThat(explorationCheckpointCaptor.value.getOrThrow().pendingUserAnswersCount) - .isEqualTo(countOfAnswers) + val checkpoint = retrieveExplorationCheckpoint(profileId, explorationId) + assertThat(checkpoint.pendingUserAnswersCount).isEqualTo(countOfAnswers) } private fun verifyCheckpointHasCorrectStateIndex( - profileId: ProfileId, - explorationId: String, - stateIndex: Int + profileId: ProfileId, explorationId: String, stateIndex: Int ) { - testCoroutineDispatchers.runCurrent() - reset(mockExplorationCheckpointObserver) - val explorationCheckpointLiveData = - explorationCheckpointController.retrieveExplorationCheckpoint( - profileId, - explorationId - ).toLiveData() - explorationCheckpointLiveData.observeForever(mockExplorationCheckpointObserver) - testCoroutineDispatchers.runCurrent() - - verify(mockExplorationCheckpointObserver, atLeastOnce()) - .onChanged(explorationCheckpointCaptor.capture()) - assertThat(explorationCheckpointCaptor.value.isSuccess()).isTrue() - - assertThat(explorationCheckpointCaptor.value.getOrThrow().stateIndex) - .isEqualTo(stateIndex) + val checkpoint = retrieveExplorationCheckpoint(profileId, explorationId) + assertThat(checkpoint.stateIndex).isEqualTo(stateIndex) } private fun verifyCheckpointHasCorrectHelpIndex( - profileId: ProfileId, - explorationId: String, - helpIndex: HelpIndex + profileId: ProfileId, explorationId: String, helpIndex: HelpIndex ) { - testCoroutineDispatchers.runCurrent() - reset(mockExplorationCheckpointObserver) - val explorationCheckpointLiveData = - explorationCheckpointController.retrieveExplorationCheckpoint( - profileId, - explorationId - ).toLiveData() - explorationCheckpointLiveData.observeForever(mockExplorationCheckpointObserver) - testCoroutineDispatchers.runCurrent() - - verify(mockExplorationCheckpointObserver, atLeastOnce()) - .onChanged(explorationCheckpointCaptor.capture()) - assertThat(explorationCheckpointCaptor.value.isSuccess()).isTrue() - - assertThat(explorationCheckpointCaptor.value.getOrThrow().helpIndex).isEqualTo(helpIndex) - } - - /** - * Verifies that the specified live data provides at least one successful operation. This will - * change test-wide mock state, and synchronizes background execution. - */ - private fun verifyOperationSucceeds(liveData: LiveData>) { - reset(mockAsyncResultLiveDataObserver) - liveData.observeForever(mockAsyncResultLiveDataObserver) - testCoroutineDispatchers.runCurrent() - verify(mockAsyncResultLiveDataObserver).onChanged(asyncResultCaptor.capture()) - asyncResultCaptor.value.apply { - // This bit of conditional logic is used to add better error reporting when failures occur. - if (isFailure()) { - throw AssertionError("Operation failed", getErrorOrNull()) - } - assertThat(isSuccess()).isTrue() - } - reset(mockAsyncResultLiveDataObserver) + val checkpoint = retrieveExplorationCheckpoint(profileId, explorationId) + assertThat(checkpoint.helpIndex).isEqualTo(helpIndex) } // TODO(#89): Move this to a common test application component. diff --git a/domain/src/test/java/org/oppia/android/domain/onboarding/AppStartupStateControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/onboarding/AppStartupStateControllerTest.kt index 5c4fbb96d73..01c0bd37e24 100644 --- a/domain/src/test/java/org/oppia/android/domain/onboarding/AppStartupStateControllerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/onboarding/AppStartupStateControllerTest.kt @@ -60,7 +60,7 @@ class AppStartupStateControllerTest { private val expirationDateFormat by lazy { SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) } @Test - fun testController_providesInitialLiveData_indicatesUserHasNotOnboardedTheApp() { + fun testController_providesInitialState_indicatesUserHasNotOnboardedTheApp() { setUpDefaultTestApplicationComponent() val appStartupState = appStartupStateController.getAppStartupState() @@ -70,7 +70,7 @@ class AppStartupStateControllerTest { } @Test - fun testControllerObserver_observedAfterSettingAppOnboarded_providesLiveData_userDidNotOnboardApp() { // ktlint-disable max-line-length + fun testControllerObserver_observedAfterSettingAppOnboarded_providesState_userDidNotOnboardApp() { setUpDefaultTestApplicationComponent() val appStartupState = appStartupStateController.getAppStartupState() @@ -95,7 +95,7 @@ class AppStartupStateControllerTest { setUpDefaultTestApplicationComponent() val appStartupState = appStartupStateController.getAppStartupState() - // The app should be considered onboarded since a new LiveData instance was observed after + // The app should be considered onboarded since a new DataProvider instance was observed after // marking the app as onboarded. val mode = monitorFactory.waitForNextSuccessfulResult(appStartupState) assertThat(mode.startupMode).isEqualTo(USER_IS_ONBOARDED) diff --git a/domain/src/test/java/org/oppia/android/domain/question/QuestionAssessmentProgressControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/question/QuestionAssessmentProgressControllerTest.kt index c914d63f58e..05725122b30 100644 --- a/domain/src/test/java/org/oppia/android/domain/question/QuestionAssessmentProgressControllerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/question/QuestionAssessmentProgressControllerTest.kt @@ -2,8 +2,6 @@ package org.oppia.android.domain.question import android.app.Application import android.content.Context -import androidx.lifecycle.LiveData -import androidx.lifecycle.Observer import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat @@ -12,19 +10,14 @@ import dagger.BindsInstance import dagger.Component import dagger.Module import dagger.Provides +import java.util.Locale +import java.util.concurrent.TimeUnit +import javax.inject.Inject +import javax.inject.Singleton import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentCaptor -import org.mockito.Captor -import org.mockito.Mock -import org.mockito.Mockito.atLeastOnce -import org.mockito.Mockito.reset -import org.mockito.Mockito.verify -import org.mockito.junit.MockitoJUnit -import org.mockito.junit.MockitoRule -import org.oppia.android.app.model.AnsweredQuestionOutcome import org.oppia.android.app.model.EphemeralQuestion import org.oppia.android.app.model.EphemeralState import org.oppia.android.app.model.EphemeralState.StateTypeCase.COMPLETED_STATE @@ -70,8 +63,6 @@ import org.oppia.android.testing.threading.TestDispatcherModule import org.oppia.android.testing.time.FakeOppiaClockModule import org.oppia.android.util.caching.AssetModule import org.oppia.android.util.caching.testing.CachingTestModule -import org.oppia.android.util.data.AsyncResult -import org.oppia.android.util.data.DataProviders.Companion.toLiveData import org.oppia.android.util.data.DataProvidersInjector import org.oppia.android.util.data.DataProvidersInjectorProvider import org.oppia.android.util.locale.LocaleProdModule @@ -82,10 +73,6 @@ import org.oppia.android.util.logging.LogLevel import org.oppia.android.util.networking.NetworkConnectionUtilDebugModule import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode -import java.util.Locale -import java.util.concurrent.TimeUnit -import javax.inject.Inject -import javax.inject.Singleton private const val TOLERANCE = 1e-5 @@ -97,59 +84,15 @@ private const val TOLERANCE = 1e-5 @LooperMode(LooperMode.Mode.PAUSED) @Config(application = QuestionAssessmentProgressControllerTest.TestApplication::class) class QuestionAssessmentProgressControllerTest { - @Rule - @JvmField - val mockitoRule: MockitoRule = MockitoJUnit.rule() - - @get:Rule - val oppiaTestRule = OppiaTestRule() - - @Inject - lateinit var context: Context - - @Inject - lateinit var questionTrainingController: QuestionTrainingController - - @Inject - lateinit var questionAssessmentProgressController: QuestionAssessmentProgressController - - @Inject - lateinit var fakeExceptionLogger: FakeExceptionLogger - - @Inject - lateinit var testCoroutineDispatchers: TestCoroutineDispatchers - - // TODO(#3813): Migrate all tests in this suite to use this factory. - @Inject - lateinit var monitorFactory: DataProviderTestMonitor.Factory - - @Inject - lateinit var translationController: TranslationController - - @Mock - lateinit var mockScoreAndMasteryLiveDataObserver: - Observer> - - @Mock - lateinit var mockAsyncNullableResultLiveDataObserver: Observer> - - @Mock - lateinit var mockAsyncAnswerOutcomeObserver: Observer> + @get:Rule val oppiaTestRule = OppiaTestRule() - @Mock - lateinit var mockAsyncResultLiveDataObserver: Observer> - - @Captor - lateinit var asyncResultCaptor: ArgumentCaptor> - - @Captor - lateinit var performanceCalculationCaptor: ArgumentCaptor> - - @Captor - lateinit var asyncNullableResultCaptor: ArgumentCaptor> - - @Captor - lateinit var asyncAnswerOutcomeCaptor: ArgumentCaptor> + @Inject lateinit var context: Context + @Inject lateinit var questionTrainingController: QuestionTrainingController + @Inject lateinit var questionAssessmentProgressController: QuestionAssessmentProgressController + @Inject lateinit var fakeExceptionLogger: FakeExceptionLogger + @Inject lateinit var testCoroutineDispatchers: TestCoroutineDispatchers + @Inject lateinit var monitorFactory: DataProviderTestMonitor.Factory + @Inject lateinit var translationController: TranslationController private lateinit var profileId1: ProfileId @@ -291,18 +234,13 @@ class QuestionAssessmentProgressControllerTest { @Test fun testSubmitAnswer_beforePlaying_failsWithError() { setUpTestApplicationWithSeed(questionSeed = 0) - val result = + + val submitAnswerProvider = questionAssessmentProgressController.submitAnswer(createMultipleChoiceAnswer(0)) - result.observeForever(mockAsyncAnswerOutcomeObserver) - testCoroutineDispatchers.runCurrent() // Verify that the answer submission failed. - verify( - mockAsyncAnswerOutcomeObserver, - atLeastOnce() - ).onChanged(asyncAnswerOutcomeCaptor.capture()) - assertThat(asyncAnswerOutcomeCaptor.value.isFailure()).isTrue() - assertThat(asyncAnswerOutcomeCaptor.value.getErrorOrNull()) + val failure = monitorFactory.waitForNextFailureResult(submitAnswerProvider) + assertThat(failure) .hasMessageThat() .contains("Cannot submit an answer if a training session has not yet begun.") } @@ -313,17 +251,10 @@ class QuestionAssessmentProgressControllerTest { startSuccessfulTrainingSession(TEST_SKILL_ID_LIST_2) waitForGetCurrentQuestionSuccessfulLoad() - val result = - questionAssessmentProgressController.submitAnswer(createMultipleChoiceAnswer(1)) - result.observeForever(mockAsyncAnswerOutcomeObserver) - testCoroutineDispatchers.runCurrent() + val result = questionAssessmentProgressController.submitAnswer(createMultipleChoiceAnswer(1)) // Verify that the answer submission was successful. - verify( - mockAsyncAnswerOutcomeObserver, - atLeastOnce() - ).onChanged(asyncAnswerOutcomeCaptor.capture()) - assertThat(asyncAnswerOutcomeCaptor.value.isSuccess()).isTrue() + monitorFactory.waitForNextSuccessfulResult(result) } @Test @@ -332,17 +263,10 @@ class QuestionAssessmentProgressControllerTest { startSuccessfulTrainingSession(TEST_SKILL_ID_LIST_2) waitForGetCurrentQuestionSuccessfulLoad() - val result = - questionAssessmentProgressController.submitAnswer(createMultipleChoiceAnswer(1)) - result.observeForever(mockAsyncAnswerOutcomeObserver) - testCoroutineDispatchers.runCurrent() + val result = questionAssessmentProgressController.submitAnswer(createMultipleChoiceAnswer(1)) // Verify that the answer submission was successful. - verify( - mockAsyncAnswerOutcomeObserver, - atLeastOnce() - ).onChanged(asyncAnswerOutcomeCaptor.capture()) - val answerOutcome = asyncAnswerOutcomeCaptor.value.getOrThrow() + val answerOutcome = monitorFactory.waitForNextSuccessfulResult(result) assertThat(answerOutcome.feedback.html).contains("That's correct!") assertThat(answerOutcome.isCorrectAnswer).isTrue() } @@ -353,17 +277,10 @@ class QuestionAssessmentProgressControllerTest { startSuccessfulTrainingSession(TEST_SKILL_ID_LIST_2) waitForGetCurrentQuestionSuccessfulLoad() - val result = - questionAssessmentProgressController.submitAnswer(createMultipleChoiceAnswer(0)) - result.observeForever(mockAsyncAnswerOutcomeObserver) - testCoroutineDispatchers.runCurrent() + val result = questionAssessmentProgressController.submitAnswer(createMultipleChoiceAnswer(0)) // Verify that the answer submission was successful. - verify( - mockAsyncAnswerOutcomeObserver, - atLeastOnce() - ).onChanged(asyncAnswerOutcomeCaptor.capture()) - assertThat(asyncAnswerOutcomeCaptor.value.isSuccess()).isTrue() + monitorFactory.waitForNextSuccessfulResult(result) } @Test @@ -372,17 +289,10 @@ class QuestionAssessmentProgressControllerTest { startSuccessfulTrainingSession(TEST_SKILL_ID_LIST_2) waitForGetCurrentQuestionSuccessfulLoad() - val result = - questionAssessmentProgressController.submitAnswer(createMultipleChoiceAnswer(0)) - result.observeForever(mockAsyncAnswerOutcomeObserver) - testCoroutineDispatchers.runCurrent() + val result = questionAssessmentProgressController.submitAnswer(createMultipleChoiceAnswer(0)) // Verify that the answer submission was successful. - verify( - mockAsyncAnswerOutcomeObserver, - atLeastOnce() - ).onChanged(asyncAnswerOutcomeCaptor.capture()) - val answerOutcome = asyncAnswerOutcomeCaptor.value.getOrThrow() + val answerOutcome = monitorFactory.waitForNextSuccessfulResult(result) assertThat(answerOutcome.feedback.html).contains("Incorrect. Try again.") assertThat(answerOutcome.isCorrectAnswer).isFalse() } @@ -448,15 +358,10 @@ class QuestionAssessmentProgressControllerTest { fun testMoveToNext_beforePlaying_failsWithError() { setUpTestApplicationWithSeed(questionSeed = 0) - val moveToStateResult = - questionAssessmentProgressController.moveToNextQuestion() - moveToStateResult.observeForever(mockAsyncNullableResultLiveDataObserver) + val moveToQuestionResult = questionAssessmentProgressController.moveToNextQuestion() - verify(mockAsyncNullableResultLiveDataObserver, atLeastOnce()).onChanged( - asyncNullableResultCaptor.capture() - ) - assertThat(asyncNullableResultCaptor.value.isFailure()).isTrue() - assertThat(asyncNullableResultCaptor.value.getErrorOrNull()) + val error = monitorFactory.waitForNextFailureResult(moveToQuestionResult) + assertThat(error) .hasMessageThat() .contains("Cannot navigate to a next question if a training session has not begun.") } @@ -467,17 +372,11 @@ class QuestionAssessmentProgressControllerTest { startSuccessfulTrainingSession(TEST_SKILL_ID_LIST_2) waitForGetCurrentQuestionSuccessfulLoad() - val moveToStateResult = - questionAssessmentProgressController.moveToNextQuestion() - moveToStateResult.observeForever(mockAsyncNullableResultLiveDataObserver) - testCoroutineDispatchers.runCurrent() + val moveToQuestionResult = questionAssessmentProgressController.moveToNextQuestion() // Verify that we can't move ahead since the current state isn't yet completed. - verify(mockAsyncNullableResultLiveDataObserver, atLeastOnce()).onChanged( - asyncNullableResultCaptor.capture() - ) - assertThat(asyncNullableResultCaptor.value.isFailure()).isTrue() - assertThat(asyncNullableResultCaptor.value.getErrorOrNull()) + val error = monitorFactory.waitForNextFailureResult(moveToQuestionResult) + assertThat(error) .hasMessageThat() .contains("Cannot navigate to next state; at most recent state.") } @@ -489,15 +388,9 @@ class QuestionAssessmentProgressControllerTest { waitForGetCurrentQuestionSuccessfulLoad() submitMultipleChoiceAnswer(1) - val moveToStateResult = - questionAssessmentProgressController.moveToNextQuestion() - moveToStateResult.observeForever(mockAsyncNullableResultLiveDataObserver) - testCoroutineDispatchers.runCurrent() + val moveToQuestionResult = questionAssessmentProgressController.moveToNextQuestion() - verify(mockAsyncNullableResultLiveDataObserver, atLeastOnce()).onChanged( - asyncNullableResultCaptor.capture() - ) - assertThat(asyncNullableResultCaptor.value.isSuccess()).isTrue() + monitorFactory.waitForNextSuccessfulResult(moveToQuestionResult) } @Test @@ -521,17 +414,11 @@ class QuestionAssessmentProgressControllerTest { moveToNextQuestion() // Try skipping past the current state. - val moveToStateResult = - questionAssessmentProgressController.moveToNextQuestion() - moveToStateResult.observeForever(mockAsyncNullableResultLiveDataObserver) - testCoroutineDispatchers.runCurrent() + val moveToQuestionResult = questionAssessmentProgressController.moveToNextQuestion() // Verify we can't move ahead since the new state isn't yet completed. - verify(mockAsyncNullableResultLiveDataObserver, atLeastOnce()).onChanged( - asyncNullableResultCaptor.capture() - ) - assertThat(asyncNullableResultCaptor.value.isFailure()).isTrue() - assertThat(asyncNullableResultCaptor.value.getErrorOrNull()) + val error = monitorFactory.waitForNextFailureResult(moveToQuestionResult) + assertThat(error) .hasMessageThat() .contains("Cannot navigate to next state; at most recent state.") } @@ -542,17 +429,10 @@ class QuestionAssessmentProgressControllerTest { startSuccessfulTrainingSession(TEST_SKILL_ID_LIST_01) waitForGetCurrentQuestionSuccessfulLoad() - val result = - questionAssessmentProgressController.submitAnswer(createTextInputAnswer("1/4")) - result.observeForever(mockAsyncAnswerOutcomeObserver) - testCoroutineDispatchers.runCurrent() + val result = questionAssessmentProgressController.submitAnswer(createTextInputAnswer("1/4")) // Verify that the answer submission was successful. - verify( - mockAsyncAnswerOutcomeObserver, - atLeastOnce() - ).onChanged(asyncAnswerOutcomeCaptor.capture()) - val answerOutcome = asyncAnswerOutcomeCaptor.value.getOrThrow() + val answerOutcome = monitorFactory.waitForNextSuccessfulResult(result) assertThat(answerOutcome.isCorrectAnswer).isTrue() assertThat(answerOutcome.feedback.html).contains("That's correct!") } @@ -563,18 +443,11 @@ class QuestionAssessmentProgressControllerTest { startSuccessfulTrainingSession(TEST_SKILL_ID_LIST_01) waitForGetCurrentQuestionSuccessfulLoad() - val result = - questionAssessmentProgressController.submitAnswer(createTextInputAnswer("2/4")) - result.observeForever(mockAsyncAnswerOutcomeObserver) - testCoroutineDispatchers.runCurrent() + val result = questionAssessmentProgressController.submitAnswer(createTextInputAnswer("2/4")) // Verify that the answer was wrong, and that there's no handler for it so the default outcome // is returned. - verify( - mockAsyncAnswerOutcomeObserver, - atLeastOnce() - ).onChanged(asyncAnswerOutcomeCaptor.capture()) - val answerOutcome = asyncAnswerOutcomeCaptor.value.getOrThrow() + val answerOutcome = monitorFactory.waitForNextSuccessfulResult(result) assertThat(answerOutcome.isCorrectAnswer).isFalse() assertThat(answerOutcome.feedback.html).isEmpty() } @@ -586,9 +459,7 @@ class QuestionAssessmentProgressControllerTest { waitForGetCurrentQuestionSuccessfulLoad() submitNumericInputAnswerAndMoveToNextQuestion(3.0) - val result = - questionAssessmentProgressController.submitAnswer(createNumericInputAnswer(5.0)) - result.observeForever(mockAsyncAnswerOutcomeObserver) + questionAssessmentProgressController.submitAnswer(createNumericInputAnswer(5.0)) testCoroutineDispatchers.runCurrent() // Verify that the current state updates. It should stay pending, and the wrong answer should be @@ -610,9 +481,7 @@ class QuestionAssessmentProgressControllerTest { waitForGetCurrentQuestionSuccessfulLoad() submitNumericInputAnswerAndMoveToNextQuestion(3.0) - val result = - questionAssessmentProgressController.submitAnswer(createNumericInputAnswer(4.0)) - result.observeForever(mockAsyncAnswerOutcomeObserver) + questionAssessmentProgressController.submitAnswer(createNumericInputAnswer(4.0)) testCoroutineDispatchers.runCurrent() // Verify that the current state updates. It should now be completed with the correct answer. @@ -632,17 +501,10 @@ class QuestionAssessmentProgressControllerTest { startSuccessfulTrainingSession(TEST_SKILL_ID_LIST_2) waitForGetCurrentQuestionSuccessfulLoad() - val result = - questionAssessmentProgressController.submitAnswer(createNumericInputAnswer(3.0)) - result.observeForever(mockAsyncAnswerOutcomeObserver) - testCoroutineDispatchers.runCurrent() + val result = questionAssessmentProgressController.submitAnswer(createNumericInputAnswer(3.0)) // Verify that the answer submission was successful. - verify( - mockAsyncAnswerOutcomeObserver, - atLeastOnce() - ).onChanged(asyncAnswerOutcomeCaptor.capture()) - val answerOutcome = asyncAnswerOutcomeCaptor.value.getOrThrow() + val answerOutcome = monitorFactory.waitForNextSuccessfulResult(result) assertThat(answerOutcome.isCorrectAnswer).isTrue() assertThat(answerOutcome.feedback.html).contains("That's correct!") } @@ -653,17 +515,10 @@ class QuestionAssessmentProgressControllerTest { startSuccessfulTrainingSession(TEST_SKILL_ID_LIST_2) waitForGetCurrentQuestionSuccessfulLoad() - val result = - questionAssessmentProgressController.submitAnswer(createNumericInputAnswer(2.0)) - result.observeForever(mockAsyncAnswerOutcomeObserver) - testCoroutineDispatchers.runCurrent() + val result = questionAssessmentProgressController.submitAnswer(createNumericInputAnswer(2.0)) // Verify that the answer submission failed as expected. - verify( - mockAsyncAnswerOutcomeObserver, - atLeastOnce() - ).onChanged(asyncAnswerOutcomeCaptor.capture()) - val answerOutcome = asyncAnswerOutcomeCaptor.value.getOrThrow() + val answerOutcome = monitorFactory.waitForNextSuccessfulResult(result) assertThat(answerOutcome.isCorrectAnswer).isFalse() assertThat(answerOutcome.feedback.html).isEmpty() } @@ -693,17 +548,11 @@ class QuestionAssessmentProgressControllerTest { submitNumericInputAnswerAndMoveToNextQuestion(5.0) submitMultipleChoiceAnswerAndMoveToNextQuestion(1) - val moveToStateResult = - questionAssessmentProgressController.moveToNextQuestion() - moveToStateResult.observeForever(mockAsyncNullableResultLiveDataObserver) - testCoroutineDispatchers.runCurrent() + val moveToQuestionResult = questionAssessmentProgressController.moveToNextQuestion() // Verify we can't navigate past the last state of the training session. - verify(mockAsyncNullableResultLiveDataObserver, atLeastOnce()).onChanged( - asyncNullableResultCaptor.capture() - ) - assertThat(asyncNullableResultCaptor.value.isFailure()).isTrue() - assertThat(asyncNullableResultCaptor.value.getErrorOrNull()) + val error = monitorFactory.waitForNextFailureResult(moveToQuestionResult) + assertThat(error) .hasMessageThat() .contains("Cannot navigate to next state; at most recent state.") } @@ -750,12 +599,10 @@ class QuestionAssessmentProgressControllerTest { submitNumericInputAnswerAndMoveToNextQuestion(5.0) submitMultipleChoiceAnswerAndMoveToNextQuestion(1) - val moveToStateResult = - questionAssessmentProgressController.moveToNextQuestion() - moveToStateResult.observeForever(mockAsyncNullableResultLiveDataObserver) + questionAssessmentProgressController.moveToNextQuestion() testCoroutineDispatchers.runCurrent() - val exception = fakeExceptionLogger.getMostRecentException() + val exception = fakeExceptionLogger.getMostRecentException() assertThat(exception).isInstanceOf(IllegalStateException::class.java) assertThat(exception).hasMessageThat() .contains("Cannot navigate to next state; at most recent state.") @@ -765,12 +612,10 @@ class QuestionAssessmentProgressControllerTest { fun testSubmitAnswer_beforePlaying_failsWithError_logsException() { setUpTestApplicationWithSeed(questionSeed = 0) - val result = - questionAssessmentProgressController.submitAnswer(createMultipleChoiceAnswer(0)) - result.observeForever(mockAsyncAnswerOutcomeObserver) + questionAssessmentProgressController.submitAnswer(createMultipleChoiceAnswer(0)) testCoroutineDispatchers.runCurrent() - val exception = fakeExceptionLogger.getMostRecentException() + val exception = fakeExceptionLogger.getMostRecentException() assertThat(exception).isInstanceOf(IllegalStateException::class.java) assertThat(exception) .hasMessageThat() @@ -783,17 +628,10 @@ class QuestionAssessmentProgressControllerTest { startSuccessfulTrainingSession(TEST_SKILL_ID_LIST_2) waitForGetCurrentQuestionSuccessfulLoad() - val result = - questionAssessmentProgressController.submitAnswer(createNumericInputAnswer(2.0)) - result.observeForever(mockAsyncAnswerOutcomeObserver) - testCoroutineDispatchers.runCurrent() + val result = questionAssessmentProgressController.submitAnswer(createNumericInputAnswer(2.0)) // Verify that the answer submission failed as expected. - verify( - mockAsyncAnswerOutcomeObserver, - atLeastOnce() - ).onChanged(asyncAnswerOutcomeCaptor.capture()) - val answerOutcome = asyncAnswerOutcomeCaptor.value.getOrThrow() + val answerOutcome = monitorFactory.waitForNextSuccessfulResult(result) assertThat(answerOutcome.isCorrectAnswer).isFalse() assertThat(answerOutcome.feedback.html).isEmpty() @@ -828,7 +666,7 @@ class QuestionAssessmentProgressControllerTest { .isEqualTo(2) val hintAndSolution = currentQuestion.ephemeralState.state.interaction.getHint(0) assertThat(hintAndSolution.hintContent.html).contains("Hint text will appear here") - verifyOperationSucceeds( + monitorFactory.waitForNextSuccessfulResult( questionAssessmentProgressController.submitHintIsRevealed(hintIndex = 0) ) @@ -846,7 +684,7 @@ class QuestionAssessmentProgressControllerTest { waitForGetCurrentQuestionSuccessfulLoad() submitTextInputAnswerAndMoveToNextQuestion("1/3") // question 0 (wrong answer) submitTextInputAnswerAndMoveToNextQuestion("1/3") // question 0 (wrong answer) - verifyOperationSucceeds( + monitorFactory.waitForNextSuccessfulResult( questionAssessmentProgressController.submitHintIsRevealed(hintIndex = 0) ) submitTextInputAnswerAndMoveToNextQuestion("1/3") // question 0 (wrong answer) @@ -859,7 +697,9 @@ class QuestionAssessmentProgressControllerTest { val hintAndSolution = currentQuestion.ephemeralState.state.interaction.solution assertThat(hintAndSolution.correctAnswer.correctAnswer).contains("1/4") - verifyOperationSucceeds(questionAssessmentProgressController.submitSolutionIsRevealed()) + monitorFactory.waitForNextSuccessfulResult( + questionAssessmentProgressController.submitSolutionIsRevealed() + ) // Verify that the current state updates. Hint revealed is true. val updatedState = waitForGetCurrentQuestionSuccessfulLoad() @@ -1012,7 +852,7 @@ class QuestionAssessmentProgressControllerTest { // Submit question 3 wrong answer submitIncorrectAnswerForQuestion3("3/4") submitIncorrectAnswerForQuestion3("3/4") - verifyOperationSucceeds( + monitorFactory.waitForNextSuccessfulResult( questionAssessmentProgressController.submitHintIsRevealed(hintIndex = 0) ) submitIncorrectAnswerForQuestion3("3/4") @@ -1075,7 +915,7 @@ class QuestionAssessmentProgressControllerTest { // Submit question 3 wrong answer submitIncorrectAnswerForQuestion3("3/4") submitIncorrectAnswerForQuestion3("3/4") - verifyOperationSucceeds( + monitorFactory.waitForNextSuccessfulResult( questionAssessmentProgressController.submitHintIsRevealed(hintIndex = 0) ) submitIncorrectAnswerForQuestion3("3/4") @@ -1220,7 +1060,7 @@ class QuestionAssessmentProgressControllerTest { // Submit question 3 wrong answer submitIncorrectAnswerForQuestion3("3/4") submitIncorrectAnswerForQuestion3("3/4") - verifyOperationSucceeds( + monitorFactory.waitForNextSuccessfulResult( questionAssessmentProgressController.submitHintIsRevealed(hintIndex = 0) ) submitIncorrectAnswerForQuestion3("3/4") @@ -1287,7 +1127,7 @@ class QuestionAssessmentProgressControllerTest { // Submit question 3 wrong answer submitIncorrectAnswerForQuestion3("3/4") submitIncorrectAnswerForQuestion3("3/4") - verifyOperationSucceeds( + monitorFactory.waitForNextSuccessfulResult( questionAssessmentProgressController.submitHintIsRevealed(hintIndex = 0) ) submitIncorrectAnswerForQuestion3("3/4") @@ -1526,11 +1366,6 @@ class QuestionAssessmentProgressControllerTest { ) } - private fun subscribeToScoreAndMasteryCalculations(skillIdList: List) { - questionAssessmentProgressController.calculateScores(skillIdList).toLiveData() - .observeForever(mockScoreAndMasteryLiveDataObserver) - } - private fun startSuccessfulTrainingSession(skillIdList: List) { startSuccessfulTrainingSession(profileId1, skillIdList) } @@ -1645,7 +1480,7 @@ class QuestionAssessmentProgressControllerTest { } else if (index == 1) { assertThat(hint.hintContent.html).contains("

Second hint text will appear here

") } - verifyOperationSucceeds( + monitorFactory.waitForNextSuccessfulResult( questionAssessmentProgressController.submitHintIsRevealed(hintIndex = index) ) } @@ -1661,7 +1496,7 @@ class QuestionAssessmentProgressControllerTest { private fun viewHintForQuestion2(ephemeralQuestion: EphemeralQuestion) { val hint = ephemeralQuestion.ephemeralState.state.interaction.getHint(0) assertThat(hint.hintContent.html).contains("

Hint text will appear here

") - verifyOperationSucceeds( + monitorFactory.waitForNextSuccessfulResult( questionAssessmentProgressController.submitHintIsRevealed(hintIndex = 0) ) } @@ -1669,7 +1504,9 @@ class QuestionAssessmentProgressControllerTest { private fun viewSolutionForQuestion2(ephemeralQuestion: EphemeralQuestion) { val solution = ephemeralQuestion.ephemeralState.state.interaction.solution assertThat(solution.correctAnswer.correctAnswer).isEqualTo("3.0") - verifyOperationSucceeds(questionAssessmentProgressController.submitSolutionIsRevealed()) + monitorFactory.waitForNextSuccessfulResult( + questionAssessmentProgressController.submitSolutionIsRevealed() + ) } private fun submitCorrectAnswerForQuestion3(): EphemeralQuestion { @@ -1683,7 +1520,9 @@ class QuestionAssessmentProgressControllerTest { private fun viewSolutionForQuestion3(ephemeralQuestion: EphemeralQuestion) { val solution = ephemeralQuestion.ephemeralState.state.interaction.solution assertThat(solution.correctAnswer.correctAnswer).isEqualTo("1/2") - verifyOperationSucceeds(questionAssessmentProgressController.submitSolutionIsRevealed()) + monitorFactory.waitForNextSuccessfulResult( + questionAssessmentProgressController.submitSolutionIsRevealed() + ) } private fun submitCorrectAnswerForQuestion4(): EphemeralQuestion { @@ -1717,32 +1556,9 @@ class QuestionAssessmentProgressControllerTest { pendingState.helpIndex.isSolutionRevealed() private fun getExpectedGrade(skillIdList: List): UserAssessmentPerformance { - subscribeToScoreAndMasteryCalculations(skillIdList) - testCoroutineDispatchers.runCurrent() - verify( - mockScoreAndMasteryLiveDataObserver, - atLeastOnce() - ).onChanged(performanceCalculationCaptor.capture()) - return performanceCalculationCaptor.value.getOrThrow() - } - - /** - * Verifies that the specified live data provides at least one successful operation. This will - * change test-wide mock state, and synchronizes background execution. - */ - private fun verifyOperationSucceeds(liveData: LiveData>) { - reset(mockAsyncResultLiveDataObserver) - liveData.observeForever(mockAsyncResultLiveDataObserver) - testCoroutineDispatchers.runCurrent() - verify(mockAsyncResultLiveDataObserver).onChanged(asyncResultCaptor.capture()) - asyncResultCaptor.value.apply { - // This bit of conditional logic is used to add better error reporting when failures occur. - if (isFailure()) { - throw AssertionError("Operation failed", getErrorOrNull()) - } - assertThat(isSuccess()).isTrue() - } - reset(mockAsyncResultLiveDataObserver) + return monitorFactory.waitForNextSuccessfulResult( + questionAssessmentProgressController.calculateScores(skillIdList) + ) } // TODO(#89): Move this to a common test application component. diff --git a/domain/src/test/java/org/oppia/android/domain/question/QuestionTrainingControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/question/QuestionTrainingControllerTest.kt index 5f592a50171..664823de956 100644 --- a/domain/src/test/java/org/oppia/android/domain/question/QuestionTrainingControllerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/question/QuestionTrainingControllerTest.kt @@ -2,7 +2,6 @@ package org.oppia.android.domain.question import android.app.Application import android.content.Context -import androidx.lifecycle.Observer import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat @@ -10,17 +9,11 @@ import dagger.BindsInstance import dagger.Component import dagger.Module import dagger.Provides +import javax.inject.Inject +import javax.inject.Singleton import org.junit.Before -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentCaptor -import org.mockito.Captor -import org.mockito.Mock -import org.mockito.Mockito.verify -import org.mockito.junit.MockitoJUnit -import org.mockito.junit.MockitoRule -import org.oppia.android.app.model.EphemeralQuestion import org.oppia.android.app.model.ProfileId import org.oppia.android.domain.classify.InteractionsModule import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule @@ -51,8 +44,6 @@ import org.oppia.android.testing.threading.TestDispatcherModule import org.oppia.android.testing.time.FakeOppiaClockModule import org.oppia.android.util.caching.AssetModule import org.oppia.android.util.caching.testing.CachingTestModule -import org.oppia.android.util.data.AsyncResult -import org.oppia.android.util.data.DataProviders.Companion.toLiveData import org.oppia.android.util.data.DataProvidersInjector import org.oppia.android.util.data.DataProvidersInjectorProvider import org.oppia.android.util.locale.LocaleProdModule @@ -63,39 +54,19 @@ import org.oppia.android.util.logging.LogLevel import org.oppia.android.util.networking.NetworkConnectionUtilDebugModule import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode -import javax.inject.Inject -import javax.inject.Singleton /** Tests for [QuestionTrainingController]. */ +// FunctionName: test names are conventionally named with underscores. +@Suppress("FunctionName") @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) @Config(application = QuestionTrainingControllerTest.TestApplication::class) class QuestionTrainingControllerTest { - @Rule - @JvmField - val mockitoRule: MockitoRule = MockitoJUnit.rule() - - @Inject - lateinit var questionTrainingController: QuestionTrainingController - - @Inject - lateinit var questionAssessmentProgressController: QuestionAssessmentProgressController - - @Inject - lateinit var fakeExceptionLogger: FakeExceptionLogger - - @Inject - lateinit var testCoroutineDispatchers: TestCoroutineDispatchers - - @Mock - lateinit var mockCurrentQuestionLiveDataObserver: Observer> - - @Captor - lateinit var currentQuestionResultCaptor: ArgumentCaptor> - - // TODO(#3813): Migrate all tests in this suite to use this factory. - @Inject - lateinit var monitorFactory: DataProviderTestMonitor.Factory + @Inject lateinit var questionTrainingController: QuestionTrainingController + @Inject lateinit var questionAssessmentProgressController: QuestionAssessmentProgressController + @Inject lateinit var fakeExceptionLogger: FakeExceptionLogger + @Inject lateinit var testCoroutineDispatchers: TestCoroutineDispatchers + @Inject lateinit var monitorFactory: DataProviderTestMonitor.Factory private lateinit var profileId1: ProfileId @@ -124,16 +95,10 @@ class QuestionTrainingControllerTest { ) testCoroutineDispatchers.runCurrent() - val resultLiveData = - questionAssessmentProgressController.getCurrentQuestion().toLiveData() - resultLiveData.observeForever(mockCurrentQuestionLiveDataObserver) - testCoroutineDispatchers.runCurrent() + val result = questionAssessmentProgressController.getCurrentQuestion() - verify(mockCurrentQuestionLiveDataObserver).onChanged(currentQuestionResultCaptor.capture()) - assertThat(currentQuestionResultCaptor.value.isSuccess()).isTrue() - assertThat(currentQuestionResultCaptor.value.getOrThrow().question.questionId).isEqualTo( - TEST_QUESTION_ID_1 - ) + val ephemeralQuestion = monitorFactory.waitForNextSuccessfulResult(result) + assertThat(ephemeralQuestion.question.questionId).isEqualTo(TEST_QUESTION_ID_1) } @Test @@ -156,16 +121,10 @@ class QuestionTrainingControllerTest { ) testCoroutineDispatchers.runCurrent() - val resultLiveData = - questionAssessmentProgressController.getCurrentQuestion().toLiveData() - resultLiveData.observeForever(mockCurrentQuestionLiveDataObserver) - testCoroutineDispatchers.runCurrent() + val result = questionAssessmentProgressController.getCurrentQuestion() - verify(mockCurrentQuestionLiveDataObserver).onChanged(currentQuestionResultCaptor.capture()) - assertThat(currentQuestionResultCaptor.value.isSuccess()).isTrue() - assertThat(currentQuestionResultCaptor.value.getOrThrow().question.questionId).isEqualTo( - TEST_QUESTION_ID_0 - ) + val ephemeralQuestion = monitorFactory.waitForNextSuccessfulResult(result) + assertThat(ephemeralQuestion.question.questionId).isEqualTo(TEST_QUESTION_ID_0) } @Test @@ -188,16 +147,10 @@ class QuestionTrainingControllerTest { ) testCoroutineDispatchers.runCurrent() - val resultLiveData = - questionAssessmentProgressController.getCurrentQuestion().toLiveData() - resultLiveData.observeForever(mockCurrentQuestionLiveDataObserver) - testCoroutineDispatchers.runCurrent() + val result = questionAssessmentProgressController.getCurrentQuestion() - verify(mockCurrentQuestionLiveDataObserver).onChanged(currentQuestionResultCaptor.capture()) - assertThat(currentQuestionResultCaptor.value.isSuccess()).isTrue() - assertThat(currentQuestionResultCaptor.value.getOrThrow().question.questionId).isEqualTo( - TEST_QUESTION_ID_3 - ) + val ephemeralQuestion = monitorFactory.waitForNextSuccessfulResult(result) + assertThat(ephemeralQuestion.question.questionId).isEqualTo(TEST_QUESTION_ID_3) } @Test diff --git a/model/src/main/proto/exploration.proto b/model/src/main/proto/exploration.proto index 25294ddcaf9..463af6f4688 100644 --- a/model/src/main/proto/exploration.proto +++ b/model/src/main/proto/exploration.proto @@ -310,6 +310,9 @@ message AnswerAndResponse { // Oppia's response to the answer the learner submitted. SubtitledHtml feedback = 2; + + // Whether the answer was labelled by the creator as correct. + bool is_correct_answer = 3; } message AnswerOutcome { diff --git a/utility/src/main/java/org/oppia/android/util/data/DataProviders.kt b/utility/src/main/java/org/oppia/android/util/data/DataProviders.kt index d53b58fd70c..d402465f4a8 100644 --- a/utility/src/main/java/org/oppia/android/util/data/DataProviders.kt +++ b/utility/src/main/java/org/oppia/android/util/data/DataProviders.kt @@ -3,18 +3,16 @@ package org.oppia.android.util.data import android.content.Context import androidx.lifecycle.LiveData import dagger.Reusable +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicReference +import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.transform import kotlinx.coroutines.launch import org.oppia.android.util.logging.ExceptionLogger import org.oppia.android.util.threading.BackgroundDispatcher -import java.util.concurrent.atomic.AtomicBoolean -import java.util.concurrent.atomic.AtomicReference -import javax.inject.Inject -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.transform /** * Various functions to create or manipulate [DataProvider]s. From da7998cf0a3064c0876283097af645b5fe524a42 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Mon, 7 Mar 2022 17:22:51 -0800 Subject: [PATCH 151/162] Post-merge fixes. --- .../android/app/topic/lessons/TopicLessonsFragmentTest.kt | 1 + .../java/org/oppia/android/domain/oppialogger/BUILD.bazel | 5 ++++- .../android/testing/threading/TestCoroutineDispatcher.kt | 3 +-- .../testing/threading/CoroutineExecutorServiceTest.kt | 5 +++-- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/app/src/sharedTest/java/org/oppia/android/app/topic/lessons/TopicLessonsFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/topic/lessons/TopicLessonsFragmentTest.kt index c4a06775983..697edb45f15 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/topic/lessons/TopicLessonsFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/topic/lessons/TopicLessonsFragmentTest.kt @@ -514,6 +514,7 @@ class TopicLessonsFragmentTest { targetViewId = R.id.chapter_recycler_view ) ).check(matches(hasDescendant(withId(R.id.chapter_container)))).perform(click()) + testCoroutineDispatchers.runCurrent() intended( allOf( hasExtra( diff --git a/domain/src/main/java/org/oppia/android/domain/oppialogger/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/oppialogger/BUILD.bazel index 38af73c4546..0faf401f0fa 100644 --- a/domain/src/main/java/org/oppia/android/domain/oppialogger/BUILD.bazel +++ b/domain/src/main/java/org/oppia/android/domain/oppialogger/BUILD.bazel @@ -31,7 +31,10 @@ kt_android_library( srcs = [ "LogStorageModule.kt", ], - visibility = ["//domain/src/main/java/org/oppia/android/domain/oppialogger:__subpackages__"], + visibility = [ + "//:oppia_testing_visibility", + "//domain/src/main/java/org/oppia/android/domain/oppialogger:__subpackages__", + ], deps = [ ":dagger", ], diff --git a/testing/src/main/java/org/oppia/android/testing/threading/TestCoroutineDispatcher.kt b/testing/src/main/java/org/oppia/android/testing/threading/TestCoroutineDispatcher.kt index 98239b60936..18366e5d004 100644 --- a/testing/src/main/java/org/oppia/android/testing/threading/TestCoroutineDispatcher.kt +++ b/testing/src/main/java/org/oppia/android/testing/threading/TestCoroutineDispatcher.kt @@ -117,8 +117,7 @@ abstract class TestCoroutineDispatcher : CoroutineDispatcher() { } private companion object { - // TODO: revert - private val STANDARD_TIMEOUT_SECONDS = TimeUnit.HOURS.toSeconds(1) + private const val STANDARD_TIMEOUT_SECONDS = 10L private val TIMEOUT_WHEN_DEBUGGING_SECONDS = TimeUnit.HOURS.toSeconds(1) private fun computeTimeout(): Long { diff --git a/testing/src/test/java/org/oppia/android/testing/threading/CoroutineExecutorServiceTest.kt b/testing/src/test/java/org/oppia/android/testing/threading/CoroutineExecutorServiceTest.kt index a64af7b45c7..03c5bbb71dc 100644 --- a/testing/src/test/java/org/oppia/android/testing/threading/CoroutineExecutorServiceTest.kt +++ b/testing/src/test/java/org/oppia/android/testing/threading/CoroutineExecutorServiceTest.kt @@ -46,6 +46,7 @@ import java.util.concurrent.TimeoutException import javax.inject.Inject import javax.inject.Singleton import org.oppia.android.testing.data.AsyncResultSubject +import org.oppia.android.testing.data.AsyncResultSubject.Companion.assertThat import org.oppia.android.util.data.AsyncResult /** @@ -349,9 +350,9 @@ class CoroutineExecutorServiceTest { // The getter should return since the task has finished. assertThat(getResult.isCompleted).isTrue() - AsyncResultSubject.assertThat(getResult.getCompleted()).isFailureThat().apply { + assertThat(getResult.getCompleted()).isFailureThat().apply { isInstanceOf(ExecutionException::class.java) - isInstanceOf(TimeoutException::class.java) + hasCauseThat().isInstanceOf(TimeoutException::class.java) } } From 07f8c595b4bb204038dda74716cbe0f4198a4d22 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Mon, 7 Mar 2022 17:31:18 -0800 Subject: [PATCH 152/162] TODO has been addressed. --- .../org/oppia/android/testing/data/DataProviderTestMonitor.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/testing/src/main/java/org/oppia/android/testing/data/DataProviderTestMonitor.kt b/testing/src/main/java/org/oppia/android/testing/data/DataProviderTestMonitor.kt index a7ee339db47..7c90109cf50 100644 --- a/testing/src/main/java/org/oppia/android/testing/data/DataProviderTestMonitor.kt +++ b/testing/src/main/java/org/oppia/android/testing/data/DataProviderTestMonitor.kt @@ -18,7 +18,6 @@ import org.oppia.android.util.data.DataProvider import org.oppia.android.util.data.DataProviders.Companion.toLiveData import javax.inject.Inject -// TODO(#3813): Migrate all data provider tests over to using this utility. /** * A test monitor for [DataProvider]s that provides operations to simplify waiting for the * provider's results, or to verify that notifications actually change the data provider when From 535348eafff69c971d10b19429011b6e9be10191 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Mon, 7 Mar 2022 18:35:02 -0800 Subject: [PATCH 153/162] Fix documentation & add tests. --- .../exploration/ExplorationDataController.kt | 22 +-- .../ExplorationProgressController.kt | 51 +++---- .../ExplorationCheckpointController.kt | 3 +- .../onboarding/AppStartupStateController.kt | 4 +- .../profile/ProfileManagementController.kt | 2 +- .../QuestionAssessmentProgressController.kt | 37 +++-- .../domain/topic/StoryProgressController.kt | 2 +- .../ExplorationProgressControllerTest.kt | 2 - .../assets/kdoc_validity_exemptions.textproto | 1 - scripts/assets/test_file_exemptions.textproto | 1 + .../testing/data/AsyncResultSubject.kt | 117 +++++++++++++++- .../oppia/android/testing/data/BUILD.bazel | 1 + .../testing/data/DataProviderTestMonitor.kt | 26 +++- .../data/DataProviderTestMonitorTest.kt | 129 ++++++++++++++++-- .../oppia/android/util/data/AsyncResult.kt | 102 +++++++++++--- 15 files changed, 404 insertions(+), 96 deletions(-) diff --git a/domain/src/main/java/org/oppia/android/domain/exploration/ExplorationDataController.kt b/domain/src/main/java/org/oppia/android/domain/exploration/ExplorationDataController.kt index f5eea90e12a..1fbb83eec34 100644 --- a/domain/src/main/java/org/oppia/android/domain/exploration/ExplorationDataController.kt +++ b/domain/src/main/java/org/oppia/android/domain/exploration/ExplorationDataController.kt @@ -38,12 +38,15 @@ class ExplorationDataController @Inject constructor( } /** - * Begins playing an exploration of the specified ID. This method is not expected to fail. - * [ExplorationProgressController] should be used to manage the play state, and monitor the load success/failure of - * the exploration. + * Begins playing an exploration of the specified ID. * - * This must be called only if no active exploration is being played. The previous exploration must have first been - * stopped using [stopPlayingExploration] otherwise an exception will be thrown. + * This method is not expected to fail. + * + * [ExplorationProgressController] should be used to manage the play state, and monitor the load + * success/failure of the exploration. + * + * This must be called only if no active exploration is being played. The previous exploration + * must have first been stopped using [stopPlayingExploration], otherwise the operation will fail. * * @param internalProfileId the ID corresponding to the profile for which exploration has to be * played @@ -53,7 +56,7 @@ class ExplorationDataController @Inject constructor( * @param shouldSavePartialProgress the boolean that indicates if partial progress has to be saved * for the current exploration * @param explorationCheckpoint the checkpoint which may be used to resume the exploration - * @return a one-time [LiveData] to observe whether initiating the play request succeeded. + * @return a one-time [DataProvider] to observe whether initiating the play request succeeded. * The exploration may still fail to load, but this provides early-failure detection. */ fun startPlayingExploration( @@ -75,8 +78,11 @@ class ExplorationDataController @Inject constructor( } /** - * Finishes the most recent exploration started by [startPlayingExploration]. This method should only be called if an - * active exploration is being played, otherwise an exception will be thrown. + * Finishes the most recent exploration started by [startPlayingExploration], and returns a + * one-off [DataProvider] indicating whether the operation succeeded. + * + * This method should only be called if an active exploration is being played, otherwise the + * operation will fail. */ fun stopPlayingExploration(): DataProvider = explorationProgressController.finishExplorationAsync() diff --git a/domain/src/main/java/org/oppia/android/domain/exploration/ExplorationProgressController.kt b/domain/src/main/java/org/oppia/android/domain/exploration/ExplorationProgressController.kt index 6b1a4207dbd..e02aca62bc0 100644 --- a/domain/src/main/java/org/oppia/android/domain/exploration/ExplorationProgressController.kt +++ b/domain/src/main/java/org/oppia/android/domain/exploration/ExplorationProgressController.kt @@ -79,13 +79,14 @@ class ExplorationProgressController @Inject constructor( // callback on the deferred returned on saving checkpoints. In this case ExplorationActivity will // make decisions based on a value of the checkpointState which might not be up-to date. - // TODO: update documentation here & elsewhere that references LiveData instead of data providers (also check for 'live data'). - private val explorationProgress = ExplorationProgress() private val explorationProgressLock = ReentrantLock() private lateinit var hintHandler: HintHandler - /** Resets this controller to begin playing the specified [Exploration]. */ + /** + * Resets this controller to begin playing the specified [Exploration], and returns a + * [DataProvider] indicating whether the start was successful. + */ internal fun beginExplorationAsync( internalProfileId: Int, topicId: String, @@ -124,7 +125,10 @@ class ExplorationProgressController @Inject constructor( } } - /** Indicates that the current exploration being played is now completed. */ + /** + * Indicates that the current exploration being played is now completed, and returns a + * [DataProvider] indicating whether the cleanup was successful. + */ internal fun finishExplorationAsync(): DataProvider { return explorationProgressLock.withLock { try { @@ -153,17 +157,18 @@ class ExplorationProgressController @Inject constructor( /** * Submits an answer to the current state and returns how the UI should respond to this answer. - * The returned [LiveData] will only have at most two results posted: a pending result, and then a - * completed success/failure result. Failures in this case represent a failure of the app + * + * The returned [DataProvider] will only have at most two results posted: a pending result, and + * then a completed success/failure result. Failures in this case represent a failure of the app * (possibly due to networking conditions). The app should report this error in a consumable way * to the user so that they may take action on it. No additional values will be reported to the - * [LiveData]. Each call to this method returns a new, distinct, [LiveData] object that must be - * observed. Note also that the returned [LiveData] is not guaranteed to begin with a pending - * state. + * [DataProvider]. Each call to this method returns a new, distinct, [DataProvider] object that + * must be observed. Note also that the returned [DataProvider] is not guaranteed to begin with a + * pending state. * - * If the app undergoes a configuration change, calling code should rely on the [LiveData] from - * [getCurrentState] to know whether a current answer is pending. That [LiveData] will have its - * state changed to pending during answer submission and until answer resolution. + * If the app undergoes a configuration change, calling code should rely on the [DataProvider] + * from [getCurrentState] to know whether a current answer is pending. That [DataProvider] will + * have its state changed to pending during answer submission and until answer resolution. * * Submitting an answer should result in the learner staying in the current state, moving to a new * state in the exploration, being shown a concept card, or being navigated to another exploration @@ -177,9 +182,9 @@ class ExplorationProgressController @Inject constructor( * to submit an answer while a previous answer is pending. That scenario will also result in a * failed answer submission. * - * No assumptions should be made about the completion order of the returned [LiveData] vs. the - * [LiveData] from [getCurrentState]. Also note that the returned [LiveData] will only have a - * single value and not be reused after that point. + * No assumptions should be made about the completion order of the returned [DataProvider] vs. the + * [DataProvider] from [getCurrentState]. Also note that the returned [DataProvider] will only + * have a single value and not be reused after that point. */ fun submitAnswer(userAnswer: UserAnswer): DataProvider { try { @@ -267,7 +272,7 @@ class ExplorationProgressController @Inject constructor( * * @param hintIndex index of the hint that was revealed in the hint list of the current pending * state - * @return a one-time [LiveData] that indicates success/failure of the operation (the actual + * @return a one-time [DataProvider] that indicates success/failure of the operation (the actual * payload of the result isn't relevant) */ fun submitHintIsRevealed(hintIndex: Int): DataProvider { @@ -315,7 +320,7 @@ class ExplorationProgressController @Inject constructor( /** * Notifies the controller that the user has revealed the solution to the current state. * - * @return a one-time [LiveData] that indicates success/failure of the operation (the actual + * @return a one-time [DataProvider] that indicates success/failure of the operation (the actual * payload of the result isn't relevant) */ fun submitSolutionIsRevealed(): DataProvider { @@ -366,7 +371,7 @@ class ExplorationProgressController @Inject constructor( * this method will throw an exception. Calling code is responsible for ensuring this method is * only called when it's possible to navigate backward. * - * @return a one-time [LiveData] indicating whether the movement to the previous state was + * @return a one-time [DataProvider] indicating whether the movement to the previous state was * successful, or a failure if state navigation was attempted at an invalid time in the state * graph (e.g. if currently viewing the initial state of the exploration). It's recommended * that calling code only listen to this result for failures, and instead rely on @@ -417,11 +422,11 @@ class ExplorationProgressController @Inject constructor( * that routes to a later state via [submitAnswer] in order for the current state to change to a * completed state before forward navigation can occur. * - * @return a one-time [LiveData] indicating whether the movement to the next state was successful, - * or a failure if state navigation was attempted at an invalid time in the state graph (e.g. - * if the current state is pending or terminal). It's recommended that calling code only - * listen to this result for failures, and instead rely on [getCurrentState] for observing a - * successful transition to another state. + * @return a one-time [DataProvider] indicating whether the movement to the next state was + * successful, or a failure if state navigation was attempted at an invalid time in the state + * graph (e.g. if the current state is pending or terminal). It's recommended that calling + * code only listen to this result for failures, and instead rely on [getCurrentState] for + * observing a successful transition to another state. */ fun moveToNextState(): DataProvider { try { diff --git a/domain/src/main/java/org/oppia/android/domain/exploration/lightweightcheckpointing/ExplorationCheckpointController.kt b/domain/src/main/java/org/oppia/android/domain/exploration/lightweightcheckpointing/ExplorationCheckpointController.kt index 3dd8ed69756..539f5a05182 100644 --- a/domain/src/main/java/org/oppia/android/domain/exploration/lightweightcheckpointing/ExplorationCheckpointController.kt +++ b/domain/src/main/java/org/oppia/android/domain/exploration/lightweightcheckpointing/ExplorationCheckpointController.kt @@ -282,8 +282,7 @@ class ExplorationCheckpointController @Inject constructor( throwable?.let { oppiaLogger.e( "ExplorationCheckpointController", - "Failed to prime cache ahead of LiveData conversion " + - "for ExplorationCheckpointController.", + "Failed to prime cache ahead of data retrieval for ExplorationCheckpointController.", it ) } diff --git a/domain/src/main/java/org/oppia/android/domain/onboarding/AppStartupStateController.kt b/domain/src/main/java/org/oppia/android/domain/onboarding/AppStartupStateController.kt index 545b6ceba9c..7d8c9db9f06 100644 --- a/domain/src/main/java/org/oppia/android/domain/onboarding/AppStartupStateController.kt +++ b/domain/src/main/java/org/oppia/android/domain/onboarding/AppStartupStateController.kt @@ -37,9 +37,7 @@ class AppStartupStateController @Inject constructor( onboardingFlowStore.primeCacheAsync().invokeOnCompletion { it?.let { oppiaLogger.e( - "DOMAIN", - "Failed to prime cache ahead of LiveData conversion for user onboarding data.", - it + "DOMAIN", "Failed to prime cache ahead of data retrieval for user onboarding data.", it ) } } diff --git a/domain/src/main/java/org/oppia/android/domain/profile/ProfileManagementController.kt b/domain/src/main/java/org/oppia/android/domain/profile/ProfileManagementController.kt index f9a9e1dfb09..3ce77635d00 100644 --- a/domain/src/main/java/org/oppia/android/domain/profile/ProfileManagementController.kt +++ b/domain/src/main/java/org/oppia/android/domain/profile/ProfileManagementController.kt @@ -125,7 +125,7 @@ class ProfileManagementController @Inject constructor( it?.let { oppiaLogger.e( "DOMAIN", - "Failed to prime cache ahead of LiveData conversion for ProfileManagementController.", + "Failed to prime cache ahead of data retrieval for ProfileManagementController.", it ) } diff --git a/domain/src/main/java/org/oppia/android/domain/question/QuestionAssessmentProgressController.kt b/domain/src/main/java/org/oppia/android/domain/question/QuestionAssessmentProgressController.kt index 97e69c42a9e..8a06f674b31 100644 --- a/domain/src/main/java/org/oppia/android/domain/question/QuestionAssessmentProgressController.kt +++ b/domain/src/main/java/org/oppia/android/domain/question/QuestionAssessmentProgressController.kt @@ -81,6 +81,10 @@ class QuestionAssessmentProgressController @Inject constructor( private val currentQuestionDataProvider: NestedTransformedDataProvider = createCurrentQuestionDataProvider(createEmptyQuestionsListDataProvider()) + /** + * Begins a training session based on the specified question list data provider and [ProfileId], + * and returns a [DataProvider] indicating whether the session was successfully started. + */ internal fun beginQuestionTrainingSession( questionsListDataProvider: DataProvider>, profileId: ProfileId @@ -111,6 +115,10 @@ class QuestionAssessmentProgressController @Inject constructor( } } + /** + * Ends the current training session and returns a [DataProvider] that indicates whether it was + * successfully ended. + */ internal fun finishQuestionTrainingSession(): DataProvider { return progressLock.withLock { try { @@ -139,17 +147,18 @@ class QuestionAssessmentProgressController @Inject constructor( /** * Submits an answer to the current question and returns how the UI should respond to this answer. - * The returned [LiveData] will only have at most two results posted: a pending result, and then a - * completed success/failure result. Failures in this case represent a failure of the app + * + * The returned [DataProvider] will only have at most two results posted: a pending result, and + * then a completed success/failure result. Failures in this case represent a failure of the app * (possibly due to networking conditions). The app should report this error in a consumable way * to the user so that they may take action on it. No additional values will be reported to the - * [LiveData]. Each call to this method returns a new, distinct, [LiveData] object that must be - * observed. Note also that the returned [LiveData] is not guaranteed to begin with a pending - * state. + * [DataProvider]. Each call to this method returns a new, distinct, [DataProvider] object that + * must be observed. Note also that the returned [DataProvider] is not guaranteed to begin with a + * pending state. * - * If the app undergoes a configuration change, calling code should rely on the [LiveData] from - * [getCurrentQuestion] to know whether a current answer is pending. That [LiveData] will have its - * state changed to pending during answer submission and until answer resolution. + * If the app undergoes a configuration change, calling code should rely on the [DataProvider] + * from [getCurrentQuestion] to know whether a current answer is pending. That [DataProvider] will + * have its state changed to pending during answer submission and until answer resolution. * * Submitting an answer should result in the learner staying in the current question or moving to * a new question in the training session. Note that once a correct answer is processed, the @@ -162,9 +171,9 @@ class QuestionAssessmentProgressController @Inject constructor( * allow users to submit an answer while a previous answer is pending. That scenario will also * result in a failed answer submission. * - * No assumptions should be made about the completion order of the returned [LiveData] vs. the - * [LiveData] from [getCurrentQuestion]. Also note that the returned [LiveData] will only have a - * single value and not be reused after that point. + * No assumptions should be made about the completion order of the returned [DataProvider] vs. the + * [DataProvider] from [getCurrentQuestion]. Also note that the returned [DataProvider] will only + * have a single value and not be reused after that point. */ fun submitAnswer(answer: UserAnswer): DataProvider { try { @@ -246,7 +255,7 @@ class QuestionAssessmentProgressController @Inject constructor( * * @param hintIndex index of the hint that was revealed in the hint list of the current pending * state - * @return a one-time [LiveData] that indicates success/failure of the operation (the actual + * @return a one-time [DataProvider] that indicates success/failure of the operation (the actual * payload of the result isn't relevant) */ fun submitHintIsRevealed(hintIndex: Int): DataProvider { @@ -286,7 +295,7 @@ class QuestionAssessmentProgressController @Inject constructor( /** * Notifies the controller that the user has revealed the solution to the current state. * - * @return a one-time [LiveData] that indicates success/failure of the operation (the actual + * @return a one-time [DataProvider] that indicates success/failure of the operation (the actual * payload of the result isn't relevant) */ fun submitSolutionIsRevealed(): DataProvider { @@ -332,7 +341,7 @@ class QuestionAssessmentProgressController @Inject constructor( * Note that if the current question is pending, the user needs to submit a correct answer via * [submitAnswer] before forward navigation can occur. * - * @return a one-time [LiveData] indicating whether the movement to the next question was + * @return a one-time [DataProvider] indicating whether the movement to the next question was * successful, or a failure if question navigation was attempted at an invalid time (such as * if the current question is pending or terminal). It's recommended that calling code only * listen to this result for failures, and instead rely on [getCurrentQuestion] for observing diff --git a/domain/src/main/java/org/oppia/android/domain/topic/StoryProgressController.kt b/domain/src/main/java/org/oppia/android/domain/topic/StoryProgressController.kt index 53852b66137..30304fd0575 100644 --- a/domain/src/main/java/org/oppia/android/domain/topic/StoryProgressController.kt +++ b/domain/src/main/java/org/oppia/android/domain/topic/StoryProgressController.kt @@ -374,7 +374,7 @@ class StoryProgressController @Inject constructor( it?.let { it -> oppiaLogger.e( "StoryProgressController", - "Failed to prime cache ahead of LiveData conversion for StoryProgressController.", + "Failed to prime cache ahead of data retrieval for StoryProgressController.", it ) } diff --git a/domain/src/test/java/org/oppia/android/domain/exploration/ExplorationProgressControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/exploration/ExplorationProgressControllerTest.kt index 8ea5660d3c6..5f141d2cdfd 100644 --- a/domain/src/test/java/org/oppia/android/domain/exploration/ExplorationProgressControllerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/exploration/ExplorationProgressControllerTest.kt @@ -245,8 +245,6 @@ class ExplorationProgressControllerTest { assertThat(ephemeralState.state.name).isEqualTo("Continue") } - // TODO: add pending cases for start/stop playing exploration methods. - @Test fun testFinishExploration_beforePlaying_failWithError() { val resultDataProvider = explorationDataController.stopPlayingExploration() diff --git a/scripts/assets/kdoc_validity_exemptions.textproto b/scripts/assets/kdoc_validity_exemptions.textproto index adb016b4ac0..f8357268559 100644 --- a/scripts/assets/kdoc_validity_exemptions.textproto +++ b/scripts/assets/kdoc_validity_exemptions.textproto @@ -358,7 +358,6 @@ exempted_file_path: "domain/src/main/java/org/oppia/android/domain/oppialogger/L exempted_file_path: "domain/src/main/java/org/oppia/android/domain/oppialogger/loguploader/LogUploadWorker.kt" exempted_file_path: "domain/src/main/java/org/oppia/android/domain/profile/ProfileManagementController.kt" exempted_file_path: "domain/src/main/java/org/oppia/android/domain/question/QuestionAssessmentProgress.kt" -exempted_file_path: "domain/src/main/java/org/oppia/android/domain/question/QuestionAssessmentProgressController.kt" exempted_file_path: "domain/src/main/java/org/oppia/android/domain/question/QuestionConstantsProvider.kt" exempted_file_path: "domain/src/main/java/org/oppia/android/domain/question/QuestionTrainingConstantsProvider.kt" exempted_file_path: "domain/src/main/java/org/oppia/android/domain/topic/StoryProgressController.kt" diff --git a/scripts/assets/test_file_exemptions.textproto b/scripts/assets/test_file_exemptions.textproto index 469bf3439e5..b36528c4160 100644 --- a/scripts/assets/test_file_exemptions.textproto +++ b/scripts/assets/test_file_exemptions.textproto @@ -643,6 +643,7 @@ exempted_file_path: "testing/src/main/java/org/oppia/android/testing/RichTextVie exempted_file_path: "testing/src/main/java/org/oppia/android/testing/TestImageLoaderModule.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/TestLogReportingModule.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/TextInputActionTestActivity.kt" +exempted_file_path: "testing/src/main/java/org/oppia/android/testing/data/AsyncResultSubject.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/environment/TestEnvironmentConfig.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/espresso/EditTextInputAction.kt" exempted_file_path: "testing/src/main/java/org/oppia/android/testing/espresso/GenericViewMatchers.kt" diff --git a/testing/src/main/java/org/oppia/android/testing/data/AsyncResultSubject.kt b/testing/src/main/java/org/oppia/android/testing/data/AsyncResultSubject.kt index 0fd9e2e98d4..1080b68ead8 100644 --- a/testing/src/main/java/org/oppia/android/testing/data/AsyncResultSubject.kt +++ b/testing/src/main/java/org/oppia/android/testing/data/AsyncResultSubject.kt @@ -13,72 +13,169 @@ import com.google.common.truth.StringSubject import com.google.common.truth.Subject import com.google.common.truth.Subject.Factory import com.google.common.truth.ThrowableSubject -import com.google.common.truth.Truth +import com.google.common.truth.Truth.assertAbout import com.google.common.truth.Truth.assertThat import com.google.common.truth.extensions.proto.LiteProtoSubject import com.google.common.truth.extensions.proto.LiteProtoTruth.assertThat import com.google.protobuf.MessageLite import org.oppia.android.util.data.AsyncResult -// TODO: file issue and add TODO to add tests. +// TODO(#4236): Add tests for this class. +/** + * Truth subject for verifying properties of [AsyncResult]s. + * + * Call [assertThat] to create the subject. + */ class AsyncResultSubject( failureMetadata: FailureMetadata?, @PublishedApi internal val actual: AsyncResult ) : Subject(failureMetadata, actual) { + /** Verifies that the [AsyncResult] under test is of type [AsyncResult.Pending]. */ fun isPending() { ensureActualIsType>() } + /** Verifies that the [AsyncResult] under test is not type [AsyncResult.Pending]. */ + fun isNotPending() { + ensureActualIsNotType>() + } + + /** Verifies that the [AsyncResult] under test is of type [AsyncResult.Success]. */ fun isSuccess() { ensureActualIsType>() } + /** Verifies that the [AsyncResult] under test is not type [AsyncResult.Success]. */ + fun isNotSuccess() { + ensureActualIsNotType>() + } + + /** Verifies that the [AsyncResult] under test is of type [AsyncResult.Failure]. */ fun isFailure() { ensureActualIsType>() } + /** Verifies that the [AsyncResult] under test is not type [AsyncResult.Failure]. */ + fun isNotFailure() { + ensureActualIsNotType>() + } + + /** + * Verifies that the [AsyncResult] under test is of type [AsyncResult.Success] and then calls + * [block] with the [AsyncResult.Success.value] result. + * + * Note that this does not perform type checking, so it's up to the caller to ensure that the [T] + * type used by the [AsyncResult] is correct. + */ fun hasSuccessValueWhere(block: T.() -> Unit) = ensureActualIsType>().value.block() + /** + * Returns a [Subject] that can be used to perform additional assertions about the + * [AsyncResult.Success.value] of the result under test (this verifies that the result is a + * success, similar to [isSuccess]). + */ fun isSuccessThat(): Subject = assertThat(ensureActualIsType>().value) /* NOTE TO DEVELOPERS: Add more subject types below, as needed. */ + /** + * Returns a [ComparableSubject] of type [C] using the same considerations as [isSuccessThat], + * except this also verifies that the success value is a [Comparable] (though it can't verify + * [C] due to type erasure). + */ inline fun > isComparableSuccessThat(): ComparableSubject = assertThat(extractSuccessValue()) + /** + * Returns a [StringSubject] using the same considerations as [isSuccessThat], except this also + * verifies that the successful value is a [String]. + */ fun isStringSuccessThat(): StringSubject = assertThat(extractSuccessValue()) + /** + * Returns a [BooleanSubject] for the success value (as a [Boolean] version of + * [isStringSuccessThat]). + */ fun isBooleanSuccessThat(): BooleanSubject = assertThat(extractSuccessValue()) + /** + * Returns an [IntegerSubject] for the success value (as an [Int] version of + * [isStringSuccessThat]). + */ fun isIntSuccessThat(): IntegerSubject = assertThat(extractSuccessValue()) + /** + * Returns a [LongSubject] for the success value (as a [Long] version of [isStringSuccessThat]). + */ fun isLongSuccessThat(): LongSubject = assertThat(extractSuccessValue()) + /** + * Returns a [FloatSubject] for the success value (as a [Float] version of [isStringSuccessThat]). + */ fun isFloatSuccessThat(): FloatSubject = assertThat(extractSuccessValue()) + /** + * Returns a [DoubleSubject] for the success value (as a [Double] version of + * [isStringSuccessThat]). + */ fun isDoubleSuccessThat(): DoubleSubject = assertThat(extractSuccessValue()) + /** + * Returns a [LiteProtoSubject] for the success value (as a [MessageLite] version of + * [isStringSuccessThat]). + */ fun isProtoSuccessThat(): LiteProtoSubject = assertThat(extractSuccessValue()) + /** + * Returns an [IterableSubject] for the success value (as an [Iterator] version of + * [isComparableSuccessThat], including the inability to verify [E]). + */ fun isIterableSuccessThat(): IterableSubject = assertThat(extractSuccessValue>()) + /** + * Returns a [MapSubject] for the success value (as a [Map] version of [isComparableSuccessThat], + * including the inability to verify [K] and [V]). + */ fun asMapSuccessThat(): MapSubject = assertThat(extractSuccessValue>()) + /** + * Returns a [ThrowableSubject] for the success value (as a [MessageLite] version of + * [isStringSuccessThat]). + */ fun asThrowableSuccessThat(): ThrowableSubject = assertThat(extractSuccessValue()) + /** + * Verifies that the result under test is a failure (similar to [isFailure]) and returns a + * [ThrowableSubject] to verify details about the [AsyncResult.Failure.error]. + */ fun isFailureThat(): ThrowableSubject = assertThat(ensureActualIsType>().error) + /** + * Verifies that the result under test is newer or the same age as [other] (per + * [AsyncResult.isNewerThanOrSameAgeAs]). + */ fun isNewerOrSameAgeAs(other: AsyncResult) { assertThat(actual.isNewerThanOrSameAgeAs(other)).isTrue() } + /** + * Verifies that the result under test is older than [other] (per + * [AsyncResult.isNewerThanOrSameAgeAs]). + */ fun isOlderThan(other: AsyncResult) { assertThat(actual.isNewerThanOrSameAgeAs(other)).isFalse() } + /** + * Verifies the result under test is successful (per [ensureActualIsType]) and returns its + * [AsyncResult.Success.value] as type [T] (this method will fail if the conversion can't happen). + * + * Note that this is a [PublishedApi] method since it's referenced in functions inlined above, and + * should never be called outside this class. + */ @PublishedApi // See: https://stackoverflow.com/a/41905907/3689782. internal inline fun extractSuccessValue(): T { return ensureActualIsType>().value.also { @@ -86,6 +183,13 @@ class AsyncResultSubject( } } + /** + * Verifies that the result under test is of type [T] (which can be useful when generally checking + * for pending, failure, or success results), failing if it isn't. + * + * Note that this is a [PublishedApi] method since it's referenced in functions inlined above, and + * should never be called outside this class. + */ @PublishedApi internal inline fun ensureActualIsType(): T { assertThat(actual).isInstanceOf(T::class.java) @@ -94,9 +198,16 @@ class AsyncResultSubject( return actual } + private inline fun ensureActualIsNotType() { + assertThat(actual).isNotInstanceOf(T::class.java) + } + companion object { + /** + * Returns a new [AsyncResultSubject] to verify aspects of the specified [AsyncResult] value. + */ fun assertThat(actual: AsyncResult): AsyncResultSubject { - return Truth.assertAbout( + return assertAbout( Factory, AsyncResult>(::AsyncResultSubject) ).that(actual) } diff --git a/testing/src/main/java/org/oppia/android/testing/data/BUILD.bazel b/testing/src/main/java/org/oppia/android/testing/data/BUILD.bazel index 2f1b3bff4ff..67f93f5d11b 100644 --- a/testing/src/main/java/org/oppia/android/testing/data/BUILD.bazel +++ b/testing/src/main/java/org/oppia/android/testing/data/BUILD.bazel @@ -30,6 +30,7 @@ kt_android_library( visibility = ["//:oppia_testing_visibility"], deps = [ ":dagger", + "//testing/src/main/java/org/oppia/android/testing/data:async_result_subject", "//testing/src/main/java/org/oppia/android/testing/mockito", "//testing/src/main/java/org/oppia/android/testing/threading:test_coroutine_dispatchers", "//third_party:androidx_test_runner", diff --git a/testing/src/main/java/org/oppia/android/testing/data/DataProviderTestMonitor.kt b/testing/src/main/java/org/oppia/android/testing/data/DataProviderTestMonitor.kt index 7c90109cf50..61960fa7829 100644 --- a/testing/src/main/java/org/oppia/android/testing/data/DataProviderTestMonitor.kt +++ b/testing/src/main/java/org/oppia/android/testing/data/DataProviderTestMonitor.kt @@ -17,6 +17,7 @@ import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.data.DataProvider import org.oppia.android.util.data.DataProviders.Companion.toLiveData import javax.inject.Inject +import org.oppia.android.testing.data.AsyncResultSubject.Companion.assertThat /** * A test monitor for [DataProvider]s that provides operations to simplify waiting for the @@ -155,15 +156,30 @@ class DataProviderTestMonitor private constructor( } } - // TODO: add documentation explaining this is useful for arrangement since it's not making - // assumptions about the result (other than there is one), which is necessary since LiveData - // must be active. Also, add tests & verify that users of the next two functions switch to this - // one, instead, when the extra assertion isn't needed. + /** + * Convenience method for verifying that [dataProvider] has at least one result (whether it be + * successful or an error), waiting if needed for the result (see [waitForNextResult]). + * + * This method ought to be used when data providers need to be processed mid-test since using + * [waitForNextSuccessfulResult] or [waitForNextFailureResult] have the disadvantages that they + * are also verifying pass/fail state (which is usually not desired mid-test during the + * arrangement and act portions). While this method is also verifying something (execution), it + * can be considered more of a sanity check than an actual check for correctness (i.e. "this + * data provider must have executed for the test to proceed"). + * + * Note that this will fail if the result of the data provider is pending (it must provide at + * least one success or failure). + */ fun ensureDataProviderExecutes(dataProvider: DataProvider) { // Waiting for a result is the same as ensuring the conditions are right for the provider to // execute (since it must return a result if it's executed, even if it's pending). val monitor = createMonitor(dataProvider) - monitor.waitForNextResult().also { monitor.stopObservingDataProvider() } + monitor.waitForNextResult().also { + monitor.stopObservingDataProvider() + }.also { + // There must be an actual result for the provider to be successful. + assertThat(it).isNotPending() + } } /** diff --git a/testing/src/test/java/org/oppia/android/testing/data/DataProviderTestMonitorTest.kt b/testing/src/test/java/org/oppia/android/testing/data/DataProviderTestMonitorTest.kt index e41099d0524..c5f12063b28 100644 --- a/testing/src/test/java/org/oppia/android/testing/data/DataProviderTestMonitorTest.kt +++ b/testing/src/test/java/org/oppia/android/testing/data/DataProviderTestMonitorTest.kt @@ -33,6 +33,7 @@ import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode import javax.inject.Inject import javax.inject.Singleton +import kotlinx.coroutines.delay import org.oppia.android.testing.data.AsyncResultSubject.Companion.assertThat /** Tests for [DataProviderTestMonitor]. */ @@ -44,17 +45,10 @@ import org.oppia.android.testing.data.AsyncResultSubject.Companion.assertThat @LooperMode(LooperMode.Mode.PAUSED) @Config(application = DataProviderTestMonitorTest.TestApplication::class) class DataProviderTestMonitorTest { - @Inject - lateinit var monitorFactory: DataProviderTestMonitor.Factory - - @Inject - lateinit var dataProviders: DataProviders - - @Inject - lateinit var asyncDataSubscriptionManager: AsyncDataSubscriptionManager - - @Inject - lateinit var testCoroutineDispatchers: TestCoroutineDispatchers + @Inject lateinit var monitorFactory: DataProviderTestMonitor.Factory + @Inject lateinit var dataProviders: DataProviders + @Inject lateinit var asyncDataSubscriptionManager: AsyncDataSubscriptionManager + @Inject lateinit var testCoroutineDispatchers: TestCoroutineDispatchers @Before fun setUp() { @@ -649,6 +643,119 @@ class DataProviderTestMonitorTest { // retrieved. } + /* Tests for ensureDataProviderExecutes */ + + @Test + fun testEnsureDataProviderExecutes_pendingDataProvider_throwsException() { + val dataProvider = dataProviders.createInMemoryDataProviderAsync("test") { + AsyncResult.Pending() + } + + val failure = + assertThrows(AssertionError::class) { + monitorFactory.ensureDataProviderExecutes(dataProvider) + } + + assertThat(failure).hasMessageThat().contains("not to be an instance of") + assertThat(failure).hasMessageThat().contains("Pending") + } + + @Test + fun testEnsureDataProviderExecutes_unfinishedDataProvider_throwsException() { + val dataProvider = dataProviders.createInMemoryDataProviderAsync("test") { + delay(1000L) + AsyncResult.Success("str value") + } + + val failure = + assertThrows(AssertionError::class) { + monitorFactory.ensureDataProviderExecutes(dataProvider) + } + + // The result will fail since the provider never even provided a result (since it required + // advancing the test clock before a result would be available). + assertThat(failure).hasMessageThat().contains("Wanted but not invoked") + } + + @Test + fun testEnsureDataProviderExecutes_failingDataProvider_doesNotThrowException() { + val dataProvider = dataProviders.createInMemoryDataProviderAsync("test") { + AsyncResult.Failure(Exception("Failure")) + } + + val failure = assertThrows(IllegalStateException::class) { + monitorFactory.waitForNextSuccessfulResult(dataProvider) + } + + assertThat(failure).hasMessageThat().contains("Expected next result to be a success") + } + + @Test + fun testEnsureDataProviderExecutes_successfulDataProvider_doesNotThrowException() { + val dataProvider = dataProviders.createInMemoryDataProviderAsync("test") { + AsyncResult.Success("str value") + } + + val result = monitorFactory.waitForNextSuccessfulResult(dataProvider) + + assertThat(result).isEqualTo("str value") + } + + @Test + fun testEnsureDataProviderExecutes_failureThenSuccess_consumed_doesNotThrowException() { + val dataProvider = + createDataProviderWithResultsQueue( + "test", AsyncResult.Failure(Exception("Failure")), AsyncResult.Success("str value") + ) + monitorFactory.waitForNextFailureResult(dataProvider) + + val result = monitorFactory.waitForNextSuccessfulResult(dataProvider) + + assertThat(result).isEqualTo("str value") + } + + @Test + fun testEnsureDataProviderExecutes_successThenFailure_consumed_doesNotThrowException() { + val dataProvider = + createDataProviderWithResultsQueue( + "test", AsyncResult.Success("str value"), AsyncResult.Failure(Exception("Failure")) + ) + monitorFactory.waitForNextSuccessfulResult(dataProvider) + + val failure = assertThrows(IllegalStateException::class) { + monitorFactory.waitForNextSuccessfulResult(dataProvider) + } + + assertThat(failure).hasMessageThat().contains("Expected next result to be a success") + } + + @Test + fun testEnsureDataProviderExecutes_differentValues_consumed_doesNotThrowException() { + val dataProvider = + createDataProviderWithResultsQueue( + "test", AsyncResult.Success("first"), AsyncResult.Success("second") + ) + monitorFactory.waitForNextSuccessfulResult(dataProvider) + + val result = monitorFactory.waitForNextSuccessfulResult(dataProvider) + + assertThat(result).isEqualTo("second") + } + + @Test + fun testEnsureDataProviderExecutes_twiceForChangedProvider_doesNotThrowException() { + val dataProvider = + createDataProviderWithResultsQueue( + "test", AsyncResult.Success("first"), AsyncResult.Success("second") + ) + + val firstResult = monitorFactory.waitForNextSuccessfulResult(dataProvider) + val secondResult = monitorFactory.waitForNextSuccessfulResult(dataProvider) + + assertThat(firstResult).isEqualTo("first") + assertThat(secondResult).isEqualTo("second") + } + /* Tests for waitForNextSuccessfulResult */ @Test diff --git a/utility/src/main/java/org/oppia/android/util/data/AsyncResult.kt b/utility/src/main/java/org/oppia/android/util/data/AsyncResult.kt index 75dbf8260a2..a806b740ba9 100644 --- a/utility/src/main/java/org/oppia/android/util/data/AsyncResult.kt +++ b/utility/src/main/java/org/oppia/android/util/data/AsyncResult.kt @@ -2,33 +2,83 @@ package org.oppia.android.util.data import android.os.SystemClock -// TODO: update documentation to explain how to create these, and how to check the result type. -/** Represents the result from a single asynchronous function. */ +/** + * Represents the result from a single asynchronous function. + * + * [AsyncResult]s exist in one of three states: + * - [Pending] to indicate an operation that hasn't yet completed + * - [Success] to indicate an operation that finished as expected + * - [Failure] to indicate an operation that finished in an unexpected way + * + * Since this is a sealed class, each type of result can be created by constructing one of the types + * listed above. Results can also leverage Kotlin's exhaustive ``when`` statements when checking for + * the type of results received: + * + * ```kotlin + * when (result) { + * is AsyncResult.Pending -> { /* Show that the operation is pending. */ } + * is AsyncResult.Success -> { /* Do something with result.value. */ } + * is AsyncResult.Failure -> { /* Do something with result.error. */ } + * } + * ``` + * + * Note that the above is the suggested way to always check for a result type since it ensures that + * all possibilities are always considered, and can help minimize bugs. + * + * **A note on immutability:** This class is inherently immutable, though it may contain types which + * are not (based on [T]). It's **strongly** suggested to only ever use this class with immutable + * [T] types since the app's entire multithreading environment assumes this class to be safe to pass + * between threads and coroutines. + * + * **A note on pending:** While [AsyncResult.Pending] exists, some [DataProvider]s may instead elect + * to simply not provide a result until one is available. Both are acceptable results so long as the + * UI knows how to react (i.e. it's the difference between the UI showing a loading indicator upon + * initiating the operation and stopping it when a success/error is received, versus initiating the + * loading indicator only after a pending result is received). It's generally recommended that data + * providers always provide a pending result by default, but it may lead to a better user experience + * to utilize it as a signal that a long operation is underway). This API may be changed in the + * future to make these design choices more clear-cut and deliberate when implementing data + * providers. + */ sealed class AsyncResult { + /** + * The timestamp (in millis) of when this result was created. + * + * This value should only be used to compared results created in the same process, and should + * never be persisted or transferred across process boundaries. This value should be stable across + * changes to the user device's system clock or calendar settings. + */ protected abstract val resultTimeMillis: Long - /** Returns whether this result is newer than, or the same age as, the specified result of the same type. */ + /** + * Returns whether this result is newer than, or the same age as, the specified result of the same + * type. + */ fun isNewerThanOrSameAgeAs(otherResult: AsyncResult): Boolean { return resultTimeMillis >= otherResult.resultTimeMillis } /** - * Returns a version of this result that retains its pending and failed states, but transforms its success state - * according to the specified transformation function. + * Returns a version of this result that retains its pending and failed states, but transforms its + * success state according to the specified transformation function. * - * Note that if the current result is a failure, the transformed result's failure will be a chained exception with - * this result's failure as the root cause to preserve this transformation in the exception's stacktrace. + * Note that if the current result is a failure, the transformed result's failure will be a + * chained exception with this result's failure as the root cause to preserve this transformation + * in the exception's stacktrace. * - * Note also that the specified transformation function should have no side effects, and be non-blocking. + * Note also that the specified transformation function should have no side effects, and be + * non-blocking. */ fun transform(transformFunction: (T) -> O): AsyncResult { return transformWithResult { value -> Success(transformFunction(value)) } } /** - * Returns a transformed version of this result in the same way as [transform] except it supports using a blocking - * transformation function instead of a non-blocking one. Note that the transform function is only used if the current - * result is a success, at which case the function's result becomes the new, transformed result. + * Returns a transformed version of this result in the same way as [transform] except it supports + * using a blocking transformation function instead of a non-blocking one. + * + * Note that the transform function is only used if the current result is a success, at which case + * the function's result becomes the new, transformed result. */ suspend fun transformAsync(transformFunction: suspend (T) -> AsyncResult): AsyncResult { return transformWithResultAsync { value -> @@ -37,14 +87,17 @@ sealed class AsyncResult { } /** - * Returns a version of this result that retains its pending and failed states, but combines its success state with - * the success state of another result, according to the specified combine function. + * Returns a version of this result that retains its pending and failed states, but combines its + * success state with the success state of another result, according to the specified combine + * function. * - * Note that if the other result is either pending or failed, that pending or failed state will be propagated to the - * returned result rather than attempting to combine the two states. Only successful states are combined. + * Note that if the other result is either pending or failed, that pending or failed state will be + * propagated to the returned result rather than attempting to combine the two states. Only + * successful states are combined. * - * Note that if the current result is a failure, the transformed result's failure will be a chained exception with - * this result's failure as the root cause to preserve this combination in the exception's stacktrace. + * Note that if the current result is a failure, the transformed result's failure will be a + * chained exception with this result's failure as the root cause to preserve this combination in + * the exception's stacktrace. * * Note also that the specified combine function should have no side effects, and be non-blocking. */ @@ -58,9 +111,12 @@ sealed class AsyncResult { } /** - * Returns a version of this result that is combined with another result in the same way as [combineWith], except it - * supports using a blocking combine function instead of a non-blocking one. Note that the combine function is only - * used if both results are a success, at which case the function's result becomes the new, combined result. + * Returns a version of this result that is combined with another result in the same way as + * [combineWith], except it supports using a blocking combine function instead of a non-blocking + * one. + * + * Note that the combine function is only used if both results are a success, at which case the + * function's result becomes the new, combined result. */ suspend fun combineWithAsync( otherResult: AsyncResult, @@ -94,15 +150,17 @@ sealed class AsyncResult { /** A chained exception to preserve failure stacktraces for [transform] and [transformAsync]. */ class ChainedFailureException(cause: Throwable) : Exception(cause) + /** [AsyncResult] representing an operation that may be completed in the future. */ data class Pending( override val resultTimeMillis: Long = SystemClock.uptimeMillis() ) : AsyncResult() + /** [AsyncResult] representing an operation that succeeded with a specific [value]. */ data class Success( val value: T, override val resultTimeMillis: Long = SystemClock.uptimeMillis() - ) : AsyncResult() { - } + ) : AsyncResult() + /** [AsyncResult] representing an operation that failed with a specific [error]. */ data class Failure( val error: Throwable, override val resultTimeMillis: Long = SystemClock.uptimeMillis() ) : AsyncResult() From e67a0c337b33940320a681fdd63575f3ffc9c79e Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Mon, 7 Mar 2022 18:37:09 -0800 Subject: [PATCH 154/162] Lint fixes. --- .../NavigationDrawerFragmentPresenter.kt | 2 +- .../app/options/OptionsFragmentPresenter.kt | 2 +- .../ExplorationActivityPresenter.kt | 1 - ...tionExplorationManagerFragmentPresenter.kt | 1 - .../player/state/StateFragmentPresenter.kt | 2 +- .../StateFragmentTestActivityPresenter.kt | 2 +- .../profile/AddProfileActivityPresenter.kt | 2 +- .../app/profile/AdminPinActivityPresenter.kt | 4 +- .../profile/PinPasswordActivityPresenter.kt | 4 +- .../ResetPinDialogFragmentPresenter.kt | 4 +- .../profile/ProfileEditFragmentPresenter.kt | 2 +- .../profile/ProfileRenameFragmentPresenter.kt | 2 +- .../ProfileResetPinFragmentPresenter.kt | 4 +- .../app/story/StoryFragmentPresenter.kt | 2 +- .../ExplorationTestActivityPresenter.kt | 2 +- .../QuestionPlayerActivityPresenter.kt | 2 +- .../QuestionPlayerFragmentPresenter.kt | 2 +- .../persistence/PersistentCacheStoreTest.kt | 10 +-- .../domain/audio/AudioPlayerController.kt | 9 +- .../ModifyLessonProgressController.kt | 2 +- .../ExplorationProgressController.kt | 4 +- .../ExplorationCheckpointController.kt | 34 ++++---- .../android/domain/locale/LocaleController.kt | 12 +-- .../syncup/PlatformParameterSyncUpWorker.kt | 2 +- .../profile/ProfileManagementController.kt | 86 +++++++++++-------- .../question/QuestionTrainingController.kt | 5 +- .../android/domain/topic/TopicController.kt | 7 +- .../domain/topic/TopicListController.kt | 2 +- .../translation/TranslationController.kt | 2 +- .../domain/audio/AudioPlayerControllerTest.kt | 3 +- .../CellularAudioDialogControllerTest.kt | 4 +- .../ModifyLessonProgressControllerTest.kt | 2 +- .../ExplorationDataControllerTest.kt | 4 +- .../ExplorationProgressControllerTest.kt | 27 ++++-- .../ExplorationCheckpointControllerTest.kt | 6 +- .../AppStartupStateControllerTest.kt | 14 +-- .../analytics/AnalyticsControllerTest.kt | 2 +- .../exceptions/ExceptionsControllerTest.kt | 4 +- ...aughtExceptionLoggerStartupListenerTest.kt | 2 +- .../PlatformParameterControllerTest.kt | 4 +- .../PlatformParameterSyncUpWorkerTest.kt | 4 +- .../ProfileManagementControllerTest.kt | 10 +-- ...uestionAssessmentProgressControllerTest.kt | 8 +- .../QuestionTrainingControllerTest.kt | 4 +- .../topic/StoryProgressControllerTest.kt | 9 +- .../domain/topic/TopicControllerTest.kt | 6 +- .../domain/topic/TopicListControllerTest.kt | 12 ++- .../testing/data/AsyncResultSubject.kt | 2 +- .../testing/data/DataProviderTestMonitor.kt | 4 +- .../ExplorationCheckpointTestHelper.kt | 4 +- .../data/DataProviderTestMonitorTest.kt | 4 +- .../ExplorationCheckpointTestHelperTest.kt | 12 +-- .../testing/profile/ProfileTestHelperTest.kt | 4 +- .../story/StoryProgressTestHelperTest.kt | 6 +- .../threading/CoroutineExecutorServiceTest.kt | 5 +- .../oppia/android/util/data/AsyncResult.kt | 6 +- .../oppia/android/util/data/DataProviders.kt | 6 +- .../android/util/data/AsyncResultTest.kt | 7 +- .../android/util/data/DataProvidersTest.kt | 7 +- 59 files changed, 223 insertions(+), 184 deletions(-) diff --git a/app/src/main/java/org/oppia/android/app/drawer/NavigationDrawerFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/drawer/NavigationDrawerFragmentPresenter.kt index c8e6b46c7bd..1caee17fa2d 100644 --- a/app/src/main/java/org/oppia/android/app/drawer/NavigationDrawerFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/drawer/NavigationDrawerFragmentPresenter.kt @@ -7,6 +7,7 @@ import android.view.ViewGroup import androidx.appcompat.app.ActionBarDrawerToggle import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.Toolbar +import androidx.core.view.forEach import androidx.drawerlayout.widget.DrawerLayout import androidx.fragment.app.Fragment import androidx.lifecycle.LiveData @@ -40,7 +41,6 @@ import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.data.DataProviders.Companion.toLiveData import org.oppia.android.util.statusbar.StatusBarColor import javax.inject.Inject -import androidx.core.view.forEach const val NAVIGATION_PROFILE_ID_ARGUMENT_KEY = "NavigationDrawerFragmentPresenter.navigation_profile_id" diff --git a/app/src/main/java/org/oppia/android/app/options/OptionsFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/options/OptionsFragmentPresenter.kt index 4e03543a077..369f0efa623 100644 --- a/app/src/main/java/org/oppia/android/app/options/OptionsFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/options/OptionsFragmentPresenter.kt @@ -21,10 +21,10 @@ import org.oppia.android.databinding.OptionStoryTextSizeBinding import org.oppia.android.databinding.OptionsFragmentBinding import org.oppia.android.domain.oppialogger.OppiaLogger import org.oppia.android.domain.profile.ProfileManagementController +import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.data.DataProviders.Companion.toLiveData import java.security.InvalidParameterException import javax.inject.Inject -import org.oppia.android.util.data.AsyncResult const val READING_TEXT_SIZE = "READING_TEXT_SIZE" const val APP_LANGUAGE = "APP_LANGUAGE" diff --git a/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationActivityPresenter.kt index 164e3a6716a..4f131159d98 100755 --- a/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationActivityPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationActivityPresenter.kt @@ -30,7 +30,6 @@ import org.oppia.android.domain.oppialogger.OppiaLogger import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.data.DataProviders.Companion.toLiveData import javax.inject.Inject -import org.oppia.android.app.model.ExplorationCheckpointDetails private const val TAG_UNSAVED_EXPLORATION_DIALOG = "UNSAVED_EXPLORATION_DIALOG" private const val TAG_STOP_EXPLORATION_DIALOG = "STOP_EXPLORATION_DIALOG" diff --git a/app/src/main/java/org/oppia/android/app/player/exploration/HintsAndSolutionExplorationManagerFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/player/exploration/HintsAndSolutionExplorationManagerFragmentPresenter.kt index 22f3f9f25e5..2f7e4613911 100644 --- a/app/src/main/java/org/oppia/android/app/player/exploration/HintsAndSolutionExplorationManagerFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/player/exploration/HintsAndSolutionExplorationManagerFragmentPresenter.kt @@ -60,6 +60,5 @@ class HintsAndSolutionExplorationManagerFragmentPresenter @Inject constructor( } } } - } } diff --git a/app/src/main/java/org/oppia/android/app/player/state/StateFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/player/state/StateFragmentPresenter.kt index c110827d88a..e3eafd4baee 100755 --- a/app/src/main/java/org/oppia/android/app/player/state/StateFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/player/state/StateFragmentPresenter.kt @@ -39,12 +39,12 @@ import org.oppia.android.domain.exploration.ExplorationProgressController import org.oppia.android.domain.oppialogger.OppiaLogger import org.oppia.android.domain.topic.StoryProgressController import org.oppia.android.util.data.AsyncResult +import org.oppia.android.util.data.DataProvider import org.oppia.android.util.data.DataProviders.Companion.toLiveData import org.oppia.android.util.gcsresource.DefaultResourceBucketName import org.oppia.android.util.parser.html.ExplorationHtmlParserEntityType import org.oppia.android.util.system.OppiaClock import javax.inject.Inject -import org.oppia.android.util.data.DataProvider const val STATE_FRAGMENT_PROFILE_ID_ARGUMENT_KEY = "StateFragmentPresenter.state_fragment_profile_id" diff --git a/app/src/main/java/org/oppia/android/app/player/state/testing/StateFragmentTestActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/player/state/testing/StateFragmentTestActivityPresenter.kt index 365452a5e7d..9929a4077ee 100644 --- a/app/src/main/java/org/oppia/android/app/player/state/testing/StateFragmentTestActivityPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/player/state/testing/StateFragmentTestActivityPresenter.kt @@ -19,8 +19,8 @@ import org.oppia.android.domain.topic.TEST_EXPLORATION_ID_2 import org.oppia.android.domain.topic.TEST_STORY_ID_0 import org.oppia.android.domain.topic.TEST_TOPIC_ID_0 import org.oppia.android.util.data.AsyncResult -import javax.inject.Inject import org.oppia.android.util.data.DataProviders.Companion.toLiveData +import javax.inject.Inject private const val TEST_ACTIVITY_TAG = "TestActivity" diff --git a/app/src/main/java/org/oppia/android/app/profile/AddProfileActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/profile/AddProfileActivityPresenter.kt index 9f67bd2bbc1..82a0ad1a982 100644 --- a/app/src/main/java/org/oppia/android/app/profile/AddProfileActivityPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/profile/AddProfileActivityPresenter.kt @@ -25,13 +25,13 @@ import com.bumptech.glide.request.target.Target import org.oppia.android.R import org.oppia.android.app.activity.ActivityScope import org.oppia.android.app.translation.AppLanguageResourceHandler +import org.oppia.android.app.utility.TextInputEditTextHelper.Companion.onTextChanged import org.oppia.android.app.viewmodel.ViewModelProvider import org.oppia.android.databinding.AddProfileActivityBinding import org.oppia.android.domain.profile.ProfileManagementController import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.data.DataProviders.Companion.toLiveData import javax.inject.Inject -import org.oppia.android.app.utility.TextInputEditTextHelper.Companion.onTextChanged const val GALLERY_INTENT_RESULT_CODE = 1 diff --git a/app/src/main/java/org/oppia/android/app/profile/AdminPinActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/profile/AdminPinActivityPresenter.kt index f4c2b160a13..8a7a082abb1 100644 --- a/app/src/main/java/org/oppia/android/app/profile/AdminPinActivityPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/profile/AdminPinActivityPresenter.kt @@ -11,13 +11,13 @@ import org.oppia.android.app.activity.ActivityScope import org.oppia.android.app.administratorcontrols.AdministratorControlsActivity import org.oppia.android.app.model.ProfileId import org.oppia.android.app.translation.AppLanguageResourceHandler +import org.oppia.android.app.utility.TextInputEditTextHelper.Companion.onTextChanged import org.oppia.android.app.viewmodel.ViewModelProvider import org.oppia.android.databinding.AdminPinActivityBinding import org.oppia.android.domain.profile.ProfileManagementController +import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.data.DataProviders.Companion.toLiveData import javax.inject.Inject -import org.oppia.android.util.data.AsyncResult -import org.oppia.android.app.utility.TextInputEditTextHelper.Companion.onTextChanged /** The presenter for [AdminPinActivity]. */ @ActivityScope diff --git a/app/src/main/java/org/oppia/android/app/profile/PinPasswordActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/profile/PinPasswordActivityPresenter.kt index 51f940939ca..1894391df81 100644 --- a/app/src/main/java/org/oppia/android/app/profile/PinPasswordActivityPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/profile/PinPasswordActivityPresenter.kt @@ -14,13 +14,13 @@ import org.oppia.android.app.home.HomeActivity import org.oppia.android.app.model.ProfileId import org.oppia.android.app.translation.AppLanguageResourceHandler import org.oppia.android.app.utility.LifecycleSafeTimerFactory +import org.oppia.android.app.utility.TextInputEditTextHelper.Companion.onTextChanged import org.oppia.android.app.viewmodel.ViewModelProvider import org.oppia.android.databinding.PinPasswordActivityBinding import org.oppia.android.domain.profile.ProfileManagementController +import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.data.DataProviders.Companion.toLiveData import javax.inject.Inject -import org.oppia.android.util.data.AsyncResult -import org.oppia.android.app.utility.TextInputEditTextHelper.Companion.onTextChanged private const val TAG_ADMIN_SETTINGS_DIALOG = "ADMIN_SETTINGS_DIALOG" private const val TAG_RESET_PIN_DIALOG = "RESET_PIN_DIALOG" diff --git a/app/src/main/java/org/oppia/android/app/profile/ResetPinDialogFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/profile/ResetPinDialogFragmentPresenter.kt index 96a89d15112..0b6d5beb5b6 100644 --- a/app/src/main/java/org/oppia/android/app/profile/ResetPinDialogFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/profile/ResetPinDialogFragmentPresenter.kt @@ -12,13 +12,13 @@ import org.oppia.android.R import org.oppia.android.app.fragment.FragmentScope import org.oppia.android.app.model.ProfileId import org.oppia.android.app.translation.AppLanguageResourceHandler +import org.oppia.android.app.utility.TextInputEditTextHelper.Companion.onTextChanged import org.oppia.android.app.viewmodel.ViewModelProvider import org.oppia.android.databinding.ResetPinDialogBinding import org.oppia.android.domain.profile.ProfileManagementController +import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.data.DataProviders.Companion.toLiveData import javax.inject.Inject -import org.oppia.android.util.data.AsyncResult -import org.oppia.android.app.utility.TextInputEditTextHelper.Companion.onTextChanged /** The presenter for [ResetPinDialogFragment]. */ @FragmentScope diff --git a/app/src/main/java/org/oppia/android/app/settings/profile/ProfileEditFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/settings/profile/ProfileEditFragmentPresenter.kt index ad057a019e2..10bc638a31b 100644 --- a/app/src/main/java/org/oppia/android/app/settings/profile/ProfileEditFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/settings/profile/ProfileEditFragmentPresenter.kt @@ -14,9 +14,9 @@ import org.oppia.android.app.model.ProfileId import org.oppia.android.databinding.ProfileEditFragmentBinding import org.oppia.android.domain.oppialogger.OppiaLogger import org.oppia.android.domain.profile.ProfileManagementController +import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.data.DataProviders.Companion.toLiveData import javax.inject.Inject -import org.oppia.android.util.data.AsyncResult /** Argument key for profile deletion dialog in [ProfileEditFragment]. */ const val TAG_PROFILE_DELETION_DIALOG = "PROFILE_DELETION_DIALOG" diff --git a/app/src/main/java/org/oppia/android/app/settings/profile/ProfileRenameFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/settings/profile/ProfileRenameFragmentPresenter.kt index e436385e7c6..6350c2e4581 100644 --- a/app/src/main/java/org/oppia/android/app/settings/profile/ProfileRenameFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/settings/profile/ProfileRenameFragmentPresenter.kt @@ -12,13 +12,13 @@ import org.oppia.android.R import org.oppia.android.app.fragment.FragmentScope import org.oppia.android.app.model.ProfileId import org.oppia.android.app.translation.AppLanguageResourceHandler +import org.oppia.android.app.utility.TextInputEditTextHelper.Companion.onTextChanged import org.oppia.android.app.viewmodel.ViewModelProvider import org.oppia.android.databinding.ProfileRenameFragmentBinding import org.oppia.android.domain.profile.ProfileManagementController import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.data.DataProviders.Companion.toLiveData import javax.inject.Inject -import org.oppia.android.app.utility.TextInputEditTextHelper.Companion.onTextChanged /** The presenter for [ProfileRenameFragment]. */ @FragmentScope diff --git a/app/src/main/java/org/oppia/android/app/settings/profile/ProfileResetPinFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/settings/profile/ProfileResetPinFragmentPresenter.kt index a4b982dd063..b4afe1d73e1 100644 --- a/app/src/main/java/org/oppia/android/app/settings/profile/ProfileResetPinFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/settings/profile/ProfileResetPinFragmentPresenter.kt @@ -12,13 +12,13 @@ import androidx.lifecycle.Observer import org.oppia.android.R import org.oppia.android.app.model.ProfileId import org.oppia.android.app.translation.AppLanguageResourceHandler +import org.oppia.android.app.utility.TextInputEditTextHelper.Companion.onTextChanged import org.oppia.android.app.viewmodel.ViewModelProvider import org.oppia.android.databinding.ProfileResetPinFragmentBinding import org.oppia.android.domain.profile.ProfileManagementController +import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.data.DataProviders.Companion.toLiveData import javax.inject.Inject -import org.oppia.android.util.data.AsyncResult -import org.oppia.android.app.utility.TextInputEditTextHelper.Companion.onTextChanged /** The presenter for [ProfileResetPinFragment]. */ class ProfileResetPinFragmentPresenter @Inject constructor( diff --git a/app/src/main/java/org/oppia/android/app/story/StoryFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/story/StoryFragmentPresenter.kt index 37f16e09391..e0029bfe15d 100644 --- a/app/src/main/java/org/oppia/android/app/story/StoryFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/story/StoryFragmentPresenter.kt @@ -36,12 +36,12 @@ import org.oppia.android.databinding.StoryHeaderViewBinding import org.oppia.android.domain.exploration.ExplorationDataController import org.oppia.android.domain.oppialogger.OppiaLogger import org.oppia.android.util.data.AsyncResult +import org.oppia.android.util.data.DataProviders.Companion.toLiveData import org.oppia.android.util.gcsresource.DefaultResourceBucketName import org.oppia.android.util.parser.html.HtmlParser import org.oppia.android.util.parser.html.TopicHtmlParserEntityType import org.oppia.android.util.system.OppiaClock import javax.inject.Inject -import org.oppia.android.util.data.DataProviders.Companion.toLiveData /** The presenter for [StoryFragment]. */ class StoryFragmentPresenter @Inject constructor( diff --git a/app/src/main/java/org/oppia/android/app/testing/ExplorationTestActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/testing/ExplorationTestActivityPresenter.kt index e6b9ce27943..5fda9abfdb1 100644 --- a/app/src/main/java/org/oppia/android/app/testing/ExplorationTestActivityPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/testing/ExplorationTestActivityPresenter.kt @@ -13,8 +13,8 @@ import org.oppia.android.domain.topic.TEST_EXPLORATION_ID_2 import org.oppia.android.domain.topic.TEST_STORY_ID_0 import org.oppia.android.domain.topic.TEST_TOPIC_ID_0 import org.oppia.android.util.data.AsyncResult -import javax.inject.Inject import org.oppia.android.util.data.DataProviders.Companion.toLiveData +import javax.inject.Inject private const val INTERNAL_PROFILE_ID = 0 private const val TOPIC_ID = TEST_TOPIC_ID_0 diff --git a/app/src/main/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerActivityPresenter.kt index 082d67aec81..d3a69cfcfe8 100644 --- a/app/src/main/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerActivityPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerActivityPresenter.kt @@ -9,9 +9,9 @@ import org.oppia.android.app.model.ProfileId import org.oppia.android.databinding.QuestionPlayerActivityBinding import org.oppia.android.domain.oppialogger.OppiaLogger import org.oppia.android.domain.question.QuestionTrainingController +import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.data.DataProviders.Companion.toLiveData import javax.inject.Inject -import org.oppia.android.util.data.AsyncResult const val TAG_QUESTION_PLAYER_FRAGMENT = "TAG_QUESTION_PLAYER_FRAGMENT" private const val TAG_HINTS_AND_SOLUTION_QUESTION_MANAGER = "HINTS_AND_SOLUTION_QUESTION_MANAGER" diff --git a/app/src/main/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerFragmentPresenter.kt index f3329a3e45b..331a4911008 100644 --- a/app/src/main/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/topic/questionplayer/QuestionPlayerFragmentPresenter.kt @@ -33,11 +33,11 @@ import org.oppia.android.databinding.QuestionPlayerFragmentBinding import org.oppia.android.domain.oppialogger.OppiaLogger import org.oppia.android.domain.question.QuestionAssessmentProgressController import org.oppia.android.util.data.AsyncResult +import org.oppia.android.util.data.DataProvider import org.oppia.android.util.data.DataProviders.Companion.toLiveData import org.oppia.android.util.gcsresource.QuestionResourceBucketName import org.oppia.android.util.system.OppiaClock import javax.inject.Inject -import org.oppia.android.util.data.DataProvider /** The presenter for [QuestionPlayerFragment]. */ @FragmentScope diff --git a/data/src/test/java/org/oppia/android/data/persistence/PersistentCacheStoreTest.kt b/data/src/test/java/org/oppia/android/data/persistence/PersistentCacheStoreTest.kt index 1129b672df3..11d58728a53 100644 --- a/data/src/test/java/org/oppia/android/data/persistence/PersistentCacheStoreTest.kt +++ b/data/src/test/java/org/oppia/android/data/persistence/PersistentCacheStoreTest.kt @@ -10,11 +10,6 @@ import dagger.BindsInstance import dagger.Component import dagger.Module import dagger.Provides -import java.io.File -import java.io.FileOutputStream -import java.io.IOException -import javax.inject.Inject -import javax.inject.Singleton import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -35,6 +30,11 @@ import org.oppia.android.util.data.DataProvidersInjectorProvider import org.oppia.android.util.threading.BackgroundDispatcher import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode +import java.io.File +import java.io.FileOutputStream +import java.io.IOException +import javax.inject.Inject +import javax.inject.Singleton private const val CACHE_NAME_1 = "test_cache_1" private const val CACHE_NAME_2 = "test_cache_2" diff --git a/domain/src/main/java/org/oppia/android/domain/audio/AudioPlayerController.kt b/domain/src/main/java/org/oppia/android/domain/audio/AudioPlayerController.kt index de43ecd963c..756177c31ef 100644 --- a/domain/src/main/java/org/oppia/android/domain/audio/AudioPlayerController.kt +++ b/domain/src/main/java/org/oppia/android/domain/audio/AudioPlayerController.kt @@ -138,7 +138,8 @@ class AudioPlayerController @Inject constructor( } mediaPlayer.setOnErrorListener { _, what, extra -> playProgress?.value = - AsyncResult.Failure(AudioPlayerException("Audio Player put in error state with what: $what and extra: $extra") + AsyncResult.Failure( + AudioPlayerException("Audio Player put in error state with what: $what and extra: $extra") ) releaseMediaPlayer() initializeMediaPlayer() @@ -211,7 +212,8 @@ class AudioPlayerController @Inject constructor( check(prepared) { "Media Player not in a prepared state" } if (mediaPlayer.isPlaying) { playProgress?.value = - AsyncResult.Success(PlayProgress(PlayStatus.PAUSED, mediaPlayer.currentPosition, duration) + AsyncResult.Success( + PlayProgress(PlayStatus.PAUSED, mediaPlayer.currentPosition, duration) ) mediaPlayer.pause() stopUpdatingSeekBar() @@ -237,7 +239,8 @@ class AudioPlayerController @Inject constructor( val position = if (completed) 0 else mediaPlayer.currentPosition completed = false playProgress?.postValue( - AsyncResult.Success(PlayProgress(PlayStatus.PLAYING, position, mediaPlayer.duration) + AsyncResult.Success( + PlayProgress(PlayStatus.PLAYING, position, mediaPlayer.duration) ) ) } diff --git a/domain/src/main/java/org/oppia/android/domain/devoptions/ModifyLessonProgressController.kt b/domain/src/main/java/org/oppia/android/domain/devoptions/ModifyLessonProgressController.kt index 47cec594855..5bd95b9f858 100644 --- a/domain/src/main/java/org/oppia/android/domain/devoptions/ModifyLessonProgressController.kt +++ b/domain/src/main/java/org/oppia/android/domain/devoptions/ModifyLessonProgressController.kt @@ -8,13 +8,13 @@ import org.oppia.android.app.model.TopicProgress import org.oppia.android.domain.topic.StoryProgressController import org.oppia.android.domain.topic.TopicController import org.oppia.android.domain.topic.TopicListController +import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.data.DataProvider import org.oppia.android.util.data.DataProviders.Companion.combineWith import org.oppia.android.util.data.DataProviders.Companion.transformAsync import org.oppia.android.util.system.OppiaClock import javax.inject.Inject import javax.inject.Singleton -import org.oppia.android.util.data.AsyncResult private const val GET_ALL_TOPICS_PROVIDER_ID = "get_all_topics_provider_id" private const val GET_ALL_TOPICS_COMBINED_PROVIDER_ID = "get_all_topics_combined_provider_id" diff --git a/domain/src/main/java/org/oppia/android/domain/exploration/ExplorationProgressController.kt b/domain/src/main/java/org/oppia/android/domain/exploration/ExplorationProgressController.kt index e02aca62bc0..11c403a259c 100644 --- a/domain/src/main/java/org/oppia/android/domain/exploration/ExplorationProgressController.kt +++ b/domain/src/main/java/org/oppia/android/domain/exploration/ExplorationProgressController.kt @@ -18,6 +18,7 @@ import org.oppia.android.domain.translation.TranslationController import org.oppia.android.util.data.AsyncDataSubscriptionManager import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.data.DataProvider +import org.oppia.android.util.data.DataProviders import org.oppia.android.util.data.DataProviders.Companion.transformAsync import org.oppia.android.util.locale.OppiaLocale import org.oppia.android.util.system.OppiaClock @@ -25,7 +26,6 @@ import java.util.concurrent.locks.ReentrantLock import javax.inject.Inject import javax.inject.Singleton import kotlin.concurrent.withLock -import org.oppia.android.util.data.DataProviders private const val BEGIN_EXPLORATION_RESULT_PROVIDER_ID = "ExplorationProgressController.begin_exploration_result" @@ -118,7 +118,7 @@ class ExplorationProgressController @Inject constructor( ) { null } } catch (e: Exception) { exceptionsController.logNonFatalException(e) - return@withLock dataProviders.createInMemoryDataProviderAsync( + return@withLock dataProviders.createInMemoryDataProviderAsync( BEGIN_EXPLORATION_RESULT_PROVIDER_ID ) { AsyncResult.Failure(e) } } diff --git a/domain/src/main/java/org/oppia/android/domain/exploration/lightweightcheckpointing/ExplorationCheckpointController.kt b/domain/src/main/java/org/oppia/android/domain/exploration/lightweightcheckpointing/ExplorationCheckpointController.kt index 539f5a05182..21bf8e7767b 100644 --- a/domain/src/main/java/org/oppia/android/domain/exploration/lightweightcheckpointing/ExplorationCheckpointController.kt +++ b/domain/src/main/java/org/oppia/android/domain/exploration/lightweightcheckpointing/ExplorationCheckpointController.kt @@ -156,17 +156,19 @@ class ExplorationCheckpointController @Inject constructor( AsyncResult.Success(checkpoint) } checkpoint != null && exploration.version != checkpoint.explorationVersion -> { - AsyncResult.Failure(OutdatedExplorationCheckpointException( - "checkpoint with version: ${checkpoint.explorationVersion} cannot be used to " + - "resume exploration $explorationId with version: ${exploration.version}" - ) + AsyncResult.Failure( + OutdatedExplorationCheckpointException( + "checkpoint with version: ${checkpoint.explorationVersion} cannot be used to " + + "resume exploration $explorationId with version: ${exploration.version}" + ) ) } else -> { - AsyncResult.Failure(ExplorationCheckpointNotFoundException( - "Checkpoint with the explorationId $explorationId was not found " + - "for profileId ${profileId.internalId}." - ) + AsyncResult.Failure( + ExplorationCheckpointNotFoundException( + "Checkpoint with the explorationId $explorationId was not found " + + "for profileId ${profileId.internalId}." + ) ) } } @@ -201,9 +203,10 @@ class ExplorationCheckpointController @Inject constructor( .build() AsyncResult.Success(explorationCheckpointDetails) } else { - AsyncResult.Failure(ExplorationCheckpointNotFoundException( - "No saved checkpoints in $CACHE_NAME for profileId ${profileId.internalId}." - ) + AsyncResult.Failure( + ExplorationCheckpointNotFoundException( + "No saved checkpoints in $CACHE_NAME for profileId ${profileId.internalId}." + ) ) } } @@ -253,10 +256,11 @@ class ExplorationCheckpointController @Inject constructor( ): AsyncResult { return when (deferred.await()) { ExplorationCheckpointActionStatus.CHECKPOINT_NOT_FOUND -> - AsyncResult.Failure(ExplorationCheckpointNotFoundException( - "No saved checkpoint with explorationId ${explorationId!!} found for " + - "the profileId ${profileId!!.internalId}." - ) + AsyncResult.Failure( + ExplorationCheckpointNotFoundException( + "No saved checkpoint with explorationId ${explorationId!!} found for " + + "the profileId ${profileId!!.internalId}." + ) ) ExplorationCheckpointActionStatus.SUCCESS -> AsyncResult.Success(null) } diff --git a/domain/src/main/java/org/oppia/android/domain/locale/LocaleController.kt b/domain/src/main/java/org/oppia/android/domain/locale/LocaleController.kt index f565d68c1d3..09b9e1eac99 100644 --- a/domain/src/main/java/org/oppia/android/domain/locale/LocaleController.kt +++ b/domain/src/main/java/org/oppia/android/domain/locale/LocaleController.kt @@ -215,8 +215,9 @@ class LocaleController @Inject constructor( fun retrieveSystemLanguage(): DataProvider { val providerId = SYSTEM_LANGUAGE_DATA_PROVIDER_ID return getSystemLocaleProfile().transformAsync(providerId) { systemLocaleProfile -> - AsyncResult.Success(retrieveLanguageDefinitionFromSystemCode(systemLocaleProfile)?.language - ?: OppiaLanguage.LANGUAGE_UNSPECIFIED + AsyncResult.Success( + retrieveLanguageDefinitionFromSystemCode(systemLocaleProfile)?.language + ?: OppiaLanguage.LANGUAGE_UNSPECIFIED ) } } @@ -303,9 +304,10 @@ class LocaleController @Inject constructor( val locale = computeLocale(language, systemLocaleProfile, usageMode) as? T return locale?.let { AsyncResult.Success(it) - } ?: AsyncResult.Failure(IllegalStateException( - "Language $language for usage $usageMode doesn't match supported language definitions" - ) + } ?: AsyncResult.Failure( + IllegalStateException( + "Language $language for usage $usageMode doesn't match supported language definitions" + ) ) } diff --git a/domain/src/main/java/org/oppia/android/domain/platformparameter/syncup/PlatformParameterSyncUpWorker.kt b/domain/src/main/java/org/oppia/android/domain/platformparameter/syncup/PlatformParameterSyncUpWorker.kt index 4b21480d08b..87c91472a0d 100644 --- a/domain/src/main/java/org/oppia/android/domain/platformparameter/syncup/PlatformParameterSyncUpWorker.kt +++ b/domain/src/main/java/org/oppia/android/domain/platformparameter/syncup/PlatformParameterSyncUpWorker.kt @@ -17,12 +17,12 @@ import org.oppia.android.domain.oppialogger.OppiaLogger import org.oppia.android.domain.oppialogger.exceptions.ExceptionsController import org.oppia.android.domain.platformparameter.PlatformParameterController import org.oppia.android.domain.util.getStringFromData +import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.threading.BackgroundDispatcher import retrofit2.Response import java.lang.IllegalArgumentException import java.lang.IllegalStateException import javax.inject.Inject -import org.oppia.android.util.data.AsyncResult /** Worker class that fetches and caches the latest platform parameters from the remote service. */ class PlatformParameterSyncUpWorker private constructor( diff --git a/domain/src/main/java/org/oppia/android/domain/profile/ProfileManagementController.kt b/domain/src/main/java/org/oppia/android/domain/profile/ProfileManagementController.kt index 3ce77635d00..700c456168e 100644 --- a/domain/src/main/java/org/oppia/android/domain/profile/ProfileManagementController.kt +++ b/domain/src/main/java/org/oppia/android/domain/profile/ProfileManagementController.kt @@ -146,10 +146,11 @@ class ProfileManagementController @Inject constructor( if (profile != null) { AsyncResult.Success(profile) } else { - AsyncResult.Failure(ProfileNotFoundException( - "ProfileId ${profileId.internalId} does" + - " not match an existing Profile" - ) + AsyncResult.Failure( + ProfileNotFoundException( + "ProfileId ${profileId.internalId} does" + + " not match an existing Profile" + ) ) } } @@ -579,10 +580,11 @@ class ProfileManagementController @Inject constructor( currentProfileId = profileId.internalId return@createInMemoryDataProviderAsync AsyncResult.Success(0) } - AsyncResult.Failure(ProfileNotFoundException( - "ProfileId ${profileId.internalId} is" + - " not associated with an existing profile" - ) + AsyncResult.Failure( + ProfileNotFoundException( + "ProfileId ${profileId.internalId} is" + + " not associated with an existing profile" + ) ) } } @@ -642,32 +644,48 @@ class ProfileManagementController @Inject constructor( ): AsyncResult { return when (deferred.await()) { ProfileActionStatus.SUCCESS -> AsyncResult.Success(null) - ProfileActionStatus.INVALID_PROFILE_NAME -> AsyncResult.Failure(ProfileNameOnlyLettersException("$name does not contain only letters") - ) - ProfileActionStatus.PROFILE_NAME_NOT_UNIQUE -> AsyncResult.Failure(ProfileNameNotUniqueException("$name is not unique to other profiles") - ) - ProfileActionStatus.FAILED_TO_STORE_IMAGE -> AsyncResult.Failure(FailedToStoreImageException( - "Failed to store user's selected avatar image" - ) - ) - ProfileActionStatus.FAILED_TO_GENERATE_GRAVATAR -> AsyncResult.Failure(FailedToGenerateGravatarException("Failed to generate a gravatar url") - ) - ProfileActionStatus.FAILED_TO_DELETE_DIR -> AsyncResult.Failure(FailedToDeleteDirException( - "Failed to delete directory with ${profileId?.internalId}" - ) - ) - ProfileActionStatus.PROFILE_NOT_FOUND -> AsyncResult.Failure(ProfileNotFoundException( - "ProfileId ${profileId?.internalId} does not match an existing Profile" - ) - ) - ProfileActionStatus.PROFILE_NOT_ADMIN -> AsyncResult.Failure(ProfileNotAdminException( - "ProfileId ${profileId?.internalId} does not match an existing admin" - ) - ) - ProfileActionStatus.PROFILE_ALREADY_HAS_ADMIN -> AsyncResult.Failure(ProfileAlreadyHasAdminException( - "Profile cannot be an admin" - ) - ) + ProfileActionStatus.INVALID_PROFILE_NAME -> + AsyncResult.Failure( + ProfileNameOnlyLettersException("$name does not contain only letters") + ) + ProfileActionStatus.PROFILE_NAME_NOT_UNIQUE -> + AsyncResult.Failure( + ProfileNameNotUniqueException("$name is not unique to other profiles") + ) + ProfileActionStatus.FAILED_TO_STORE_IMAGE -> + AsyncResult.Failure( + FailedToStoreImageException( + "Failed to store user's selected avatar image" + ) + ) + ProfileActionStatus.FAILED_TO_GENERATE_GRAVATAR -> + AsyncResult.Failure( + FailedToGenerateGravatarException("Failed to generate a gravatar url") + ) + ProfileActionStatus.FAILED_TO_DELETE_DIR -> + AsyncResult.Failure( + FailedToDeleteDirException( + "Failed to delete directory with ${profileId?.internalId}" + ) + ) + ProfileActionStatus.PROFILE_NOT_FOUND -> + AsyncResult.Failure( + ProfileNotFoundException( + "ProfileId ${profileId?.internalId} does not match an existing Profile" + ) + ) + ProfileActionStatus.PROFILE_NOT_ADMIN -> + AsyncResult.Failure( + ProfileNotAdminException( + "ProfileId ${profileId?.internalId} does not match an existing admin" + ) + ) + ProfileActionStatus.PROFILE_ALREADY_HAS_ADMIN -> + AsyncResult.Failure( + ProfileAlreadyHasAdminException( + "Profile cannot be an admin" + ) + ) } } diff --git a/domain/src/main/java/org/oppia/android/domain/question/QuestionTrainingController.kt b/domain/src/main/java/org/oppia/android/domain/question/QuestionTrainingController.kt index e0fe4660ac8..f55e83bb117 100644 --- a/domain/src/main/java/org/oppia/android/domain/question/QuestionTrainingController.kt +++ b/domain/src/main/java/org/oppia/android/domain/question/QuestionTrainingController.kt @@ -4,15 +4,14 @@ import org.oppia.android.app.model.ProfileId import org.oppia.android.app.model.Question import org.oppia.android.domain.oppialogger.exceptions.ExceptionsController import org.oppia.android.domain.topic.TopicController +import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.data.DataProvider import org.oppia.android.util.data.DataProviders +import org.oppia.android.util.data.DataProviders.Companion.combineWith import org.oppia.android.util.data.DataProviders.Companion.transform import javax.inject.Inject import javax.inject.Singleton import kotlin.random.Random -import org.oppia.android.util.data.AsyncResult -import org.oppia.android.util.data.DataProviders.Companion.combineWith -import org.oppia.android.util.data.DataProviders.Companion.combineWithAsync private const val RETRIEVE_QUESTION_FOR_SKILLS_ID_PROVIDER_ID = "retrieve_question_for_skills_id_provider_id" diff --git a/domain/src/main/java/org/oppia/android/domain/topic/TopicController.kt b/domain/src/main/java/org/oppia/android/domain/topic/TopicController.kt index ab831717cc5..d9f94efb97f 100755 --- a/domain/src/main/java/org/oppia/android/domain/topic/TopicController.kt +++ b/domain/src/main/java/org/oppia/android/domain/topic/TopicController.kt @@ -174,9 +174,10 @@ class TopicController @Inject constructor( if (chapterSummary != null) { AsyncResult.Success(chapterSummary) } else { - AsyncResult.Failure(ChapterNotFoundException( - "Chapter for exploration $explorationId not found in story $storyId and topic $topicId" - ) + AsyncResult.Failure( + ChapterNotFoundException( + "Chapter for exploration $explorationId not found in story $storyId and topic $topicId" + ) ) } } diff --git a/domain/src/main/java/org/oppia/android/domain/topic/TopicListController.kt b/domain/src/main/java/org/oppia/android/domain/topic/TopicListController.kt index 9d8cda5ded5..6a6c80366af 100644 --- a/domain/src/main/java/org/oppia/android/domain/topic/TopicListController.kt +++ b/domain/src/main/java/org/oppia/android/domain/topic/TopicListController.kt @@ -29,6 +29,7 @@ import org.oppia.android.domain.util.JsonAssetRetriever import org.oppia.android.domain.util.getStringFromObject import org.oppia.android.util.caching.AssetRepository import org.oppia.android.util.caching.LoadLessonProtosFromAssets +import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.data.DataProvider import org.oppia.android.util.data.DataProviders import org.oppia.android.util.data.DataProviders.Companion.transformAsync @@ -36,7 +37,6 @@ import org.oppia.android.util.system.OppiaClock import java.util.concurrent.TimeUnit import javax.inject.Inject import javax.inject.Singleton -import org.oppia.android.util.data.AsyncResult private const val ONE_WEEK_IN_DAYS = 7 diff --git a/domain/src/main/java/org/oppia/android/domain/translation/TranslationController.kt b/domain/src/main/java/org/oppia/android/domain/translation/TranslationController.kt index 007233a2d7a..6774e0cb70b 100644 --- a/domain/src/main/java/org/oppia/android/domain/translation/TranslationController.kt +++ b/domain/src/main/java/org/oppia/android/domain/translation/TranslationController.kt @@ -17,6 +17,7 @@ import org.oppia.android.app.model.WrittenTranslationContext import org.oppia.android.app.model.WrittenTranslationLanguageSelection import org.oppia.android.domain.locale.LocaleController import org.oppia.android.util.data.AsyncDataSubscriptionManager +import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.data.DataProvider import org.oppia.android.util.data.DataProviders import org.oppia.android.util.data.DataProviders.Companion.transform @@ -26,7 +27,6 @@ import java.util.concurrent.locks.ReentrantLock import javax.inject.Inject import javax.inject.Singleton import kotlin.concurrent.withLock -import org.oppia.android.util.data.AsyncResult private const val SYSTEM_LANGUAGE_LOCALE_DATA_PROVIDER_ID = "system_language_locale" private const val APP_LANGUAGE_DATA_PROVIDER_ID = "app_language" diff --git a/domain/src/test/java/org/oppia/android/domain/audio/AudioPlayerControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/audio/AudioPlayerControllerTest.kt index 9add96bd2c0..561b3beb570 100644 --- a/domain/src/test/java/org/oppia/android/domain/audio/AudioPlayerControllerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/audio/AudioPlayerControllerTest.kt @@ -28,6 +28,7 @@ import org.oppia.android.domain.oppialogger.LogStorageModule import org.oppia.android.testing.FakeExceptionLogger import org.oppia.android.testing.TestLogReportingModule import org.oppia.android.testing.assertThrows +import org.oppia.android.testing.data.AsyncResultSubject.Companion.assertThat import org.oppia.android.testing.robolectric.RobolectricModule import org.oppia.android.testing.threading.TestCoroutineDispatchers import org.oppia.android.testing.threading.TestDispatcherModule @@ -49,8 +50,6 @@ import org.robolectric.shadows.util.DataSource import java.io.IOException import javax.inject.Inject import javax.inject.Singleton -import org.oppia.android.testing.data.AsyncResultSubject -import org.oppia.android.testing.data.AsyncResultSubject.Companion.assertThat /** Tests for [AudioPlayerControllerTest]. */ // FunctionName: test names are conventionally named with underscores. diff --git a/domain/src/test/java/org/oppia/android/domain/audio/CellularAudioDialogControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/audio/CellularAudioDialogControllerTest.kt index 6e21f7aaded..6bb9a706e3c 100644 --- a/domain/src/test/java/org/oppia/android/domain/audio/CellularAudioDialogControllerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/audio/CellularAudioDialogControllerTest.kt @@ -9,8 +9,6 @@ import dagger.BindsInstance import dagger.Component import dagger.Module import dagger.Provides -import javax.inject.Inject -import javax.inject.Singleton import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -31,6 +29,8 @@ import org.oppia.android.util.logging.LogLevel import org.oppia.android.util.networking.NetworkConnectionUtilDebugModule import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode +import javax.inject.Inject +import javax.inject.Singleton // FunctionName: test names are conventionally named with underscores. @Suppress("FunctionName") diff --git a/domain/src/test/java/org/oppia/android/domain/devoptions/ModifyLessonProgressControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/devoptions/ModifyLessonProgressControllerTest.kt index 7d787abfa56..60827cf127d 100644 --- a/domain/src/test/java/org/oppia/android/domain/devoptions/ModifyLessonProgressControllerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/devoptions/ModifyLessonProgressControllerTest.kt @@ -18,6 +18,7 @@ import org.oppia.android.app.model.StorySummary import org.oppia.android.app.model.Topic import org.oppia.android.domain.oppialogger.LogStorageModule import org.oppia.android.testing.TestLogReportingModule +import org.oppia.android.testing.data.DataProviderTestMonitor import org.oppia.android.testing.environment.TestEnvironmentConfig import org.oppia.android.testing.robolectric.RobolectricModule import org.oppia.android.testing.story.StoryProgressTestHelper @@ -40,7 +41,6 @@ import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode import javax.inject.Inject import javax.inject.Singleton -import org.oppia.android.testing.data.DataProviderTestMonitor /** Tests for [ModifyLessonProgressController]. */ // FunctionName: test names are conventionally named with underscores. diff --git a/domain/src/test/java/org/oppia/android/domain/exploration/ExplorationDataControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/exploration/ExplorationDataControllerTest.kt index 0aec2e3bffc..461e240ed31 100644 --- a/domain/src/test/java/org/oppia/android/domain/exploration/ExplorationDataControllerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/exploration/ExplorationDataControllerTest.kt @@ -9,8 +9,6 @@ import dagger.BindsInstance import dagger.Component import dagger.Module import dagger.Provides -import javax.inject.Inject -import javax.inject.Singleton import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -68,6 +66,8 @@ import org.oppia.android.util.logging.LogLevel import org.oppia.android.util.networking.NetworkConnectionUtilDebugModule import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode +import javax.inject.Inject +import javax.inject.Singleton /** Tests for [ExplorationDataController]. */ // Function name: test names are conventionally named with underscores. diff --git a/domain/src/test/java/org/oppia/android/domain/exploration/ExplorationProgressControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/exploration/ExplorationProgressControllerTest.kt index 5f141d2cdfd..4ed0eade1d2 100644 --- a/domain/src/test/java/org/oppia/android/domain/exploration/ExplorationProgressControllerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/exploration/ExplorationProgressControllerTest.kt @@ -10,10 +10,6 @@ import dagger.BindsInstance import dagger.Component import dagger.Module import dagger.Provides -import java.util.Locale -import java.util.concurrent.TimeUnit -import javax.inject.Inject -import javax.inject.Singleton import org.junit.Before import org.junit.Rule import org.junit.Test @@ -95,6 +91,10 @@ import org.oppia.android.util.logging.LogLevel import org.oppia.android.util.networking.NetworkConnectionUtilDebugModule import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode +import java.util.Locale +import java.util.concurrent.TimeUnit +import javax.inject.Inject +import javax.inject.Singleton // For context: // https://github.com/oppia/oppia/blob/37285a/extensions/interactions/Continue/directives/oppia-interactive-continue.directive.ts. @@ -2797,7 +2797,8 @@ class ExplorationProgressControllerTest { } private fun retrieveExplorationCheckpoint( - profileId: ProfileId, explorationId: String + profileId: ProfileId, + explorationId: String ): ExplorationCheckpoint { val explorationCheckpointDataProvider = explorationCheckpointController.retrieveExplorationCheckpoint(profileId, explorationId) @@ -3194,28 +3195,36 @@ class ExplorationProgressControllerTest { pendingState.helpIndex.isSolutionRevealed() private fun verifyCheckpointHasCorrectPendingStateName( - profileId: ProfileId, explorationId: String, pendingStateName: String + profileId: ProfileId, + explorationId: String, + pendingStateName: String ) { val checkpoint = retrieveExplorationCheckpoint(profileId, explorationId) assertThat(checkpoint.pendingStateName).isEqualTo(pendingStateName) } private fun verifyCheckpointHasCorrectCountOfAnswers( - profileId: ProfileId, explorationId: String, countOfAnswers: Int + profileId: ProfileId, + explorationId: String, + countOfAnswers: Int ) { val checkpoint = retrieveExplorationCheckpoint(profileId, explorationId) assertThat(checkpoint.pendingUserAnswersCount).isEqualTo(countOfAnswers) } private fun verifyCheckpointHasCorrectStateIndex( - profileId: ProfileId, explorationId: String, stateIndex: Int + profileId: ProfileId, + explorationId: String, + stateIndex: Int ) { val checkpoint = retrieveExplorationCheckpoint(profileId, explorationId) assertThat(checkpoint.stateIndex).isEqualTo(stateIndex) } private fun verifyCheckpointHasCorrectHelpIndex( - profileId: ProfileId, explorationId: String, helpIndex: HelpIndex + profileId: ProfileId, + explorationId: String, + helpIndex: HelpIndex ) { val checkpoint = retrieveExplorationCheckpoint(profileId, explorationId) assertThat(checkpoint.helpIndex).isEqualTo(helpIndex) diff --git a/domain/src/test/java/org/oppia/android/domain/exploration/lightweightcheckpointing/ExplorationCheckpointControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/exploration/lightweightcheckpointing/ExplorationCheckpointControllerTest.kt index 0f6906d6c19..02daf2d19eb 100644 --- a/domain/src/test/java/org/oppia/android/domain/exploration/lightweightcheckpointing/ExplorationCheckpointControllerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/exploration/lightweightcheckpointing/ExplorationCheckpointControllerTest.kt @@ -9,8 +9,6 @@ import dagger.BindsInstance import dagger.Component import dagger.Module import dagger.Provides -import javax.inject.Inject -import javax.inject.Singleton import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -50,6 +48,8 @@ import org.oppia.android.util.logging.LogLevel import org.oppia.android.util.networking.NetworkConnectionUtilDebugModule import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode +import javax.inject.Inject +import javax.inject.Singleton /** * The base exploration id for every exploration used for testing [ExplorationCheckpointController]. @@ -185,7 +185,7 @@ class ExplorationCheckpointControllerTest { FRACTIONS_EXPLORATION_ID_0 ) - val updatedCheckpoint =monitorFactory.waitForNextSuccessfulResult(retrieveCheckpointProvider) + val updatedCheckpoint = monitorFactory.waitForNextSuccessfulResult(retrieveCheckpointProvider) assertThat(updatedCheckpoint.pendingStateName) .isEqualTo(FRACTIONS_STORY_0_EXPLORATION_0_SECOND_STATE_NAME) } diff --git a/domain/src/test/java/org/oppia/android/domain/onboarding/AppStartupStateControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/onboarding/AppStartupStateControllerTest.kt index 01c0bd37e24..e1c23d44250 100644 --- a/domain/src/test/java/org/oppia/android/domain/onboarding/AppStartupStateControllerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/onboarding/AppStartupStateControllerTest.kt @@ -12,13 +12,6 @@ import dagger.BindsInstance import dagger.Component import dagger.Module import dagger.Provides -import java.text.SimpleDateFormat -import java.time.Duration -import java.time.Instant -import java.util.Date -import java.util.Locale -import javax.inject.Inject -import javax.inject.Singleton import org.junit.Test import org.junit.runner.RunWith import org.oppia.android.app.model.AppStartupState.StartupMode.APP_IS_DEPRECATED @@ -43,6 +36,13 @@ import org.oppia.android.util.networking.NetworkConnectionUtilDebugModule import org.oppia.android.util.system.OppiaClockModule import org.robolectric.Shadows.shadowOf import org.robolectric.annotation.Config +import java.text.SimpleDateFormat +import java.time.Duration +import java.time.Instant +import java.util.Date +import java.util.Locale +import javax.inject.Inject +import javax.inject.Singleton /** Tests for [AppStartupStateController]. */ // FunctionName: test names are conventionally named with underscores. diff --git a/domain/src/test/java/org/oppia/android/domain/oppialogger/analytics/AnalyticsControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/oppialogger/analytics/AnalyticsControllerTest.kt index 54dd94d3a0c..dc3011d2a32 100644 --- a/domain/src/test/java/org/oppia/android/domain/oppialogger/analytics/AnalyticsControllerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/oppialogger/analytics/AnalyticsControllerTest.kt @@ -25,6 +25,7 @@ import org.oppia.android.domain.oppialogger.EventLogStorageCacheSize import org.oppia.android.domain.oppialogger.OppiaLogger import org.oppia.android.testing.FakeEventLogger import org.oppia.android.testing.TestLogReportingModule +import org.oppia.android.testing.data.DataProviderTestMonitor import org.oppia.android.testing.robolectric.RobolectricModule import org.oppia.android.testing.threading.TestDispatcherModule import org.oppia.android.testing.time.FakeOppiaClockModule @@ -43,7 +44,6 @@ import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode import javax.inject.Inject import javax.inject.Singleton -import org.oppia.android.testing.data.DataProviderTestMonitor private const val TEST_TIMESTAMP = 1556094120000 private const val TEST_TOPIC_ID = "test_topicId" diff --git a/domain/src/test/java/org/oppia/android/domain/oppialogger/exceptions/ExceptionsControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/oppialogger/exceptions/ExceptionsControllerTest.kt index fc7da13ceb7..7e4bc2e6614 100644 --- a/domain/src/test/java/org/oppia/android/domain/oppialogger/exceptions/ExceptionsControllerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/oppialogger/exceptions/ExceptionsControllerTest.kt @@ -9,8 +9,6 @@ import dagger.BindsInstance import dagger.Component import dagger.Module import dagger.Provides -import javax.inject.Inject -import javax.inject.Singleton import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -36,6 +34,8 @@ import org.oppia.android.util.networking.NetworkConnectionUtil.ProdConnectionSta import org.oppia.android.util.networking.NetworkConnectionUtilDebugModule import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode +import javax.inject.Inject +import javax.inject.Singleton private const val TEST_TIMESTAMP_IN_MILLIS_ONE = 1556094120000 private const val TEST_TIMESTAMP_IN_MILLIS_TWO = 1556094110000 diff --git a/domain/src/test/java/org/oppia/android/domain/oppialogger/exceptions/UncaughtExceptionLoggerStartupListenerTest.kt b/domain/src/test/java/org/oppia/android/domain/oppialogger/exceptions/UncaughtExceptionLoggerStartupListenerTest.kt index 3fdc3a009ef..b0702bf7430 100644 --- a/domain/src/test/java/org/oppia/android/domain/oppialogger/exceptions/UncaughtExceptionLoggerStartupListenerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/oppialogger/exceptions/UncaughtExceptionLoggerStartupListenerTest.kt @@ -15,6 +15,7 @@ import org.junit.runner.RunWith import org.oppia.android.domain.oppialogger.ExceptionLogStorageCacheSize import org.oppia.android.testing.FakeExceptionLogger import org.oppia.android.testing.TestLogReportingModule +import org.oppia.android.testing.data.DataProviderTestMonitor import org.oppia.android.testing.robolectric.RobolectricModule import org.oppia.android.testing.threading.TestDispatcherModule import org.oppia.android.testing.time.FakeOppiaClockModule @@ -32,7 +33,6 @@ import org.robolectric.annotation.LooperMode import javax.inject.Inject import javax.inject.Qualifier import javax.inject.Singleton -import org.oppia.android.testing.data.DataProviderTestMonitor // FunctionName: test names are conventionally named with underscores. @Suppress("FunctionName") diff --git a/domain/src/test/java/org/oppia/android/domain/platformparameter/PlatformParameterControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/platformparameter/PlatformParameterControllerTest.kt index dd9bbb7fb9e..6bbb4c3366b 100644 --- a/domain/src/test/java/org/oppia/android/domain/platformparameter/PlatformParameterControllerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/platformparameter/PlatformParameterControllerTest.kt @@ -9,8 +9,6 @@ import dagger.BindsInstance import dagger.Component import dagger.Module import dagger.Provides -import javax.inject.Inject -import javax.inject.Singleton import org.junit.Test import org.junit.runner.RunWith import org.oppia.android.app.model.PlatformParameter @@ -30,6 +28,8 @@ import org.oppia.android.util.networking.NetworkConnectionUtilDebugModule import org.oppia.android.util.platformparameter.PlatformParameterSingleton import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode +import javax.inject.Inject +import javax.inject.Singleton private const val STRING_PLATFORM_PARAMETER_NAME = "string_platform_parameter_name" private const val STRING_PLATFORM_PARAMETER_VALUE = "string_platform_parameter_value" diff --git a/domain/src/test/java/org/oppia/android/domain/platformparameter/syncup/PlatformParameterSyncUpWorkerTest.kt b/domain/src/test/java/org/oppia/android/domain/platformparameter/syncup/PlatformParameterSyncUpWorkerTest.kt index a9b693eafda..799b615842c 100644 --- a/domain/src/test/java/org/oppia/android/domain/platformparameter/syncup/PlatformParameterSyncUpWorkerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/platformparameter/syncup/PlatformParameterSyncUpWorkerTest.kt @@ -20,8 +20,6 @@ import dagger.BindsInstance import dagger.Component import dagger.Module import dagger.Provides -import javax.inject.Inject -import javax.inject.Singleton import okhttp3.OkHttpClient import org.junit.Before import org.junit.Test @@ -69,6 +67,8 @@ import org.robolectric.annotation.LooperMode import retrofit2.Retrofit import retrofit2.converter.moshi.MoshiConverterFactory import retrofit2.mock.MockRetrofit +import javax.inject.Inject +import javax.inject.Singleton /** Tests for [PlatformParameterSyncUpWorker]. */ // FunctionName: test names are conventionally named with underscores. diff --git a/domain/src/test/java/org/oppia/android/domain/profile/ProfileManagementControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/profile/ProfileManagementControllerTest.kt index 74c38d924b2..036b41444e6 100644 --- a/domain/src/test/java/org/oppia/android/domain/profile/ProfileManagementControllerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/profile/ProfileManagementControllerTest.kt @@ -14,17 +14,22 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.oppia.android.app.model.AppLanguage +import org.oppia.android.app.model.AppLanguage.CHINESE_APP_LANGUAGE import org.oppia.android.app.model.AudioLanguage +import org.oppia.android.app.model.AudioLanguage.FRENCH_AUDIO_LANGUAGE import org.oppia.android.app.model.Profile import org.oppia.android.app.model.ProfileDatabase import org.oppia.android.app.model.ProfileId +import org.oppia.android.app.model.ReadingTextSize.MEDIUM_TEXT_SIZE import org.oppia.android.domain.oppialogger.LogStorageModule import org.oppia.android.testing.TestLogReportingModule +import org.oppia.android.testing.data.DataProviderTestMonitor import org.oppia.android.testing.profile.ProfileTestHelper import org.oppia.android.testing.robolectric.RobolectricModule import org.oppia.android.testing.threading.TestCoroutineDispatchers import org.oppia.android.testing.threading.TestDispatcherModule import org.oppia.android.testing.time.FakeOppiaClockModule +import org.oppia.android.util.data.DataProvider import org.oppia.android.util.data.DataProvidersInjector import org.oppia.android.util.data.DataProvidersInjectorProvider import org.oppia.android.util.locale.LocaleProdModule @@ -39,11 +44,6 @@ import java.io.File import java.io.FileInputStream import javax.inject.Inject import javax.inject.Singleton -import org.oppia.android.app.model.AppLanguage.CHINESE_APP_LANGUAGE -import org.oppia.android.app.model.AudioLanguage.FRENCH_AUDIO_LANGUAGE -import org.oppia.android.app.model.ReadingTextSize.MEDIUM_TEXT_SIZE -import org.oppia.android.testing.data.DataProviderTestMonitor -import org.oppia.android.util.data.DataProvider /** Tests for [ProfileManagementControllerTest]. */ // FunctionName: test names are conventionally named with underscores. diff --git a/domain/src/test/java/org/oppia/android/domain/question/QuestionAssessmentProgressControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/question/QuestionAssessmentProgressControllerTest.kt index 332dbfac330..b906664be8f 100644 --- a/domain/src/test/java/org/oppia/android/domain/question/QuestionAssessmentProgressControllerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/question/QuestionAssessmentProgressControllerTest.kt @@ -10,10 +10,6 @@ import dagger.BindsInstance import dagger.Component import dagger.Module import dagger.Provides -import java.util.Locale -import java.util.concurrent.TimeUnit -import javax.inject.Inject -import javax.inject.Singleton import org.junit.Before import org.junit.Rule import org.junit.Test @@ -76,6 +72,10 @@ import org.oppia.android.util.logging.LogLevel import org.oppia.android.util.networking.NetworkConnectionUtilDebugModule import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode +import java.util.Locale +import java.util.concurrent.TimeUnit +import javax.inject.Inject +import javax.inject.Singleton private const val TOLERANCE = 1e-5 diff --git a/domain/src/test/java/org/oppia/android/domain/question/QuestionTrainingControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/question/QuestionTrainingControllerTest.kt index 2593cc0dbc6..5b181a11894 100644 --- a/domain/src/test/java/org/oppia/android/domain/question/QuestionTrainingControllerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/question/QuestionTrainingControllerTest.kt @@ -9,8 +9,6 @@ import dagger.BindsInstance import dagger.Component import dagger.Module import dagger.Provides -import javax.inject.Inject -import javax.inject.Singleton import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -57,6 +55,8 @@ import org.oppia.android.util.logging.LogLevel import org.oppia.android.util.networking.NetworkConnectionUtilDebugModule import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode +import javax.inject.Inject +import javax.inject.Singleton /** Tests for [QuestionTrainingController]. */ // FunctionName: test names are conventionally named with underscores. diff --git a/domain/src/test/java/org/oppia/android/domain/topic/StoryProgressControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/topic/StoryProgressControllerTest.kt index 4c7ea4e279c..08faa224e3b 100644 --- a/domain/src/test/java/org/oppia/android/domain/topic/StoryProgressControllerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/topic/StoryProgressControllerTest.kt @@ -9,8 +9,6 @@ import dagger.BindsInstance import dagger.Component import dagger.Module import dagger.Provides -import javax.inject.Inject -import javax.inject.Singleton import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -35,6 +33,8 @@ import org.oppia.android.util.logging.LogLevel import org.oppia.android.util.networking.NetworkConnectionUtilDebugModule import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode +import javax.inject.Inject +import javax.inject.Singleton /** Tests for [StoryProgressController]. */ // FunctionName: test names are conventionally named with underscores. @@ -276,7 +276,10 @@ class StoryProgressControllerTest { } private fun retrieveChapterPlayState( - profileId: ProfileId, topicId: String, storyId: String, explorationId: String + profileId: ProfileId, + topicId: String, + storyId: String, + explorationId: String ): ChapterPlayState { val playStateProvider = storyProgressController.retrieveChapterPlayStateByExplorationId( diff --git a/domain/src/test/java/org/oppia/android/domain/topic/TopicControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/topic/TopicControllerTest.kt index 3c11b74d378..4fa110258e9 100755 --- a/domain/src/test/java/org/oppia/android/domain/topic/TopicControllerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/topic/TopicControllerTest.kt @@ -10,9 +10,6 @@ import dagger.BindsInstance import dagger.Component import dagger.Module import dagger.Provides -import java.util.Locale -import javax.inject.Inject -import javax.inject.Singleton import org.junit.Before import org.junit.Ignore import org.junit.Rule @@ -55,6 +52,9 @@ import org.oppia.android.util.logging.LogLevel import org.oppia.android.util.networking.NetworkConnectionUtilDebugModule import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode +import java.util.Locale +import javax.inject.Inject +import javax.inject.Singleton private const val INVALID_STORY_ID_1 = "INVALID_STORY_ID_1" private const val INVALID_TOPIC_ID_1 = "INVALID_TOPIC_ID_1" diff --git a/domain/src/test/java/org/oppia/android/domain/topic/TopicListControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/topic/TopicListControllerTest.kt index f21ddadfc20..6e7c0e81340 100644 --- a/domain/src/test/java/org/oppia/android/domain/topic/TopicListControllerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/topic/TopicListControllerTest.kt @@ -9,12 +9,11 @@ import dagger.BindsInstance import dagger.Component import dagger.Module import dagger.Provides -import javax.inject.Inject -import javax.inject.Singleton import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.oppia.android.app.model.ProfileId +import org.oppia.android.app.model.PromotedActivityList import org.oppia.android.app.model.PromotedStory import org.oppia.android.app.model.TopicSummary import org.oppia.android.app.model.UpcomingTopic @@ -45,6 +44,8 @@ import org.oppia.android.util.parser.image.DefaultGcsPrefix import org.oppia.android.util.parser.image.ImageDownloadUrlTemplate import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode +import javax.inject.Inject +import javax.inject.Singleton /** Tests for [TopicListController]. */ // FunctionName: test names are conventionally named with underscores. @@ -747,8 +748,11 @@ class TopicListControllerTest { private fun retrieveTopicList() = monitorFactory.waitForNextSuccessfulResult(topicListController.getTopicList()) - private fun retrievePromotedActivityList() = - monitorFactory.waitForNextSuccessfulResult(topicListController.getPromotedActivityList(profileId0)) + private fun retrievePromotedActivityList(): PromotedActivityList { + return monitorFactory.waitForNextSuccessfulResult( + topicListController.getPromotedActivityList(profileId0) + ) + } private fun setUpTestApplicationComponent() { ApplicationProvider.getApplicationContext().inject(this) diff --git a/testing/src/main/java/org/oppia/android/testing/data/AsyncResultSubject.kt b/testing/src/main/java/org/oppia/android/testing/data/AsyncResultSubject.kt index 1080b68ead8..d610a1d6154 100644 --- a/testing/src/main/java/org/oppia/android/testing/data/AsyncResultSubject.kt +++ b/testing/src/main/java/org/oppia/android/testing/data/AsyncResultSubject.kt @@ -85,7 +85,7 @@ class AsyncResultSubject( * except this also verifies that the success value is a [Comparable] (though it can't verify * [C] due to type erasure). */ - inline fun > isComparableSuccessThat(): ComparableSubject = + inline fun > isComparableSuccessThat(): ComparableSubject = assertThat(extractSuccessValue()) /** diff --git a/testing/src/main/java/org/oppia/android/testing/data/DataProviderTestMonitor.kt b/testing/src/main/java/org/oppia/android/testing/data/DataProviderTestMonitor.kt index 61960fa7829..c9dec47324f 100644 --- a/testing/src/main/java/org/oppia/android/testing/data/DataProviderTestMonitor.kt +++ b/testing/src/main/java/org/oppia/android/testing/data/DataProviderTestMonitor.kt @@ -3,21 +3,21 @@ package org.oppia.android.testing.data import androidx.lifecycle.LiveData import androidx.lifecycle.Observer import androidx.test.platform.app.InstrumentationRegistry -import java.lang.IllegalStateException import org.mockito.ArgumentCaptor import org.mockito.Mockito.atLeastOnce import org.mockito.Mockito.mock import org.mockito.Mockito.never import org.mockito.Mockito.reset import org.mockito.Mockito.verify +import org.oppia.android.testing.data.AsyncResultSubject.Companion.assertThat import org.oppia.android.testing.data.DataProviderTestMonitor.Factory import org.oppia.android.testing.mockito.anyOrNull import org.oppia.android.testing.threading.TestCoroutineDispatchers import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.data.DataProvider import org.oppia.android.util.data.DataProviders.Companion.toLiveData +import java.lang.IllegalStateException import javax.inject.Inject -import org.oppia.android.testing.data.AsyncResultSubject.Companion.assertThat /** * A test monitor for [DataProvider]s that provides operations to simplify waiting for the diff --git a/testing/src/main/java/org/oppia/android/testing/lightweightcheckpointing/ExplorationCheckpointTestHelper.kt b/testing/src/main/java/org/oppia/android/testing/lightweightcheckpointing/ExplorationCheckpointTestHelper.kt index 567d2ce1274..3fa1b289e88 100644 --- a/testing/src/main/java/org/oppia/android/testing/lightweightcheckpointing/ExplorationCheckpointTestHelper.kt +++ b/testing/src/main/java/org/oppia/android/testing/lightweightcheckpointing/ExplorationCheckpointTestHelper.kt @@ -13,9 +13,11 @@ import org.mockito.MockitoAnnotations import org.oppia.android.app.model.ExplorationCheckpoint import org.oppia.android.app.model.ProfileId import org.oppia.android.domain.exploration.lightweightcheckpointing.ExplorationCheckpointController +import org.oppia.android.domain.exploration.lightweightcheckpointing.ExplorationCheckpointController.ExplorationCheckpointNotFoundException import org.oppia.android.domain.topic.FRACTIONS_EXPLORATION_ID_0 import org.oppia.android.domain.topic.FRACTIONS_EXPLORATION_ID_1 import org.oppia.android.domain.topic.RATIOS_EXPLORATION_ID_0 +import org.oppia.android.testing.data.AsyncResultSubject.Companion.assertThat import org.oppia.android.testing.threading.TestCoroutineDispatchers import org.oppia.android.testing.time.FakeOppiaClock import org.oppia.android.util.data.AsyncResult @@ -23,8 +25,6 @@ import org.oppia.android.util.data.DataProvider import org.oppia.android.util.data.DataProviders.Companion.toLiveData import javax.inject.Inject import javax.inject.Singleton -import org.oppia.android.domain.exploration.lightweightcheckpointing.ExplorationCheckpointController.ExplorationCheckpointNotFoundException -import org.oppia.android.testing.data.AsyncResultSubject.Companion.assertThat /** The exploration title of Fractions topic, story 0, exploration 0. */ const val FRACTIONS_EXPLORATION_0_TITLE = "What is a Fraction?" diff --git a/testing/src/test/java/org/oppia/android/testing/data/DataProviderTestMonitorTest.kt b/testing/src/test/java/org/oppia/android/testing/data/DataProviderTestMonitorTest.kt index c5f12063b28..6335e207172 100644 --- a/testing/src/test/java/org/oppia/android/testing/data/DataProviderTestMonitorTest.kt +++ b/testing/src/test/java/org/oppia/android/testing/data/DataProviderTestMonitorTest.kt @@ -9,6 +9,7 @@ import dagger.BindsInstance import dagger.Component import dagger.Module import dagger.Provides +import kotlinx.coroutines.delay import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -16,6 +17,7 @@ import org.mockito.exceptions.verification.NeverWantedButInvoked import org.oppia.android.domain.oppialogger.LogStorageModule import org.oppia.android.testing.TestLogReportingModule import org.oppia.android.testing.assertThrows +import org.oppia.android.testing.data.AsyncResultSubject.Companion.assertThat import org.oppia.android.testing.robolectric.RobolectricModule import org.oppia.android.testing.threading.TestCoroutineDispatchers import org.oppia.android.testing.threading.TestDispatcherModule @@ -33,8 +35,6 @@ import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode import javax.inject.Inject import javax.inject.Singleton -import kotlinx.coroutines.delay -import org.oppia.android.testing.data.AsyncResultSubject.Companion.assertThat /** Tests for [DataProviderTestMonitor]. */ // FunctionName: test names are conventionally named with underscores. diff --git a/testing/src/test/java/org/oppia/android/testing/lightweightcheckpointing/ExplorationCheckpointTestHelperTest.kt b/testing/src/test/java/org/oppia/android/testing/lightweightcheckpointing/ExplorationCheckpointTestHelperTest.kt index 0aeb0800f11..799be06163f 100644 --- a/testing/src/test/java/org/oppia/android/testing/lightweightcheckpointing/ExplorationCheckpointTestHelperTest.kt +++ b/testing/src/test/java/org/oppia/android/testing/lightweightcheckpointing/ExplorationCheckpointTestHelperTest.kt @@ -9,8 +9,6 @@ import dagger.BindsInstance import dagger.Component import dagger.Module import dagger.Provides -import javax.inject.Inject -import javax.inject.Singleton import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -44,6 +42,8 @@ import org.oppia.android.util.logging.LogLevel import org.oppia.android.util.networking.NetworkConnectionUtilDebugModule import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode +import javax.inject.Inject +import javax.inject.Singleton /** Tests for [ExplorationCheckpointTestHelper]. */ // FunctionName: test names are conventionally named with underscores. @@ -135,8 +135,9 @@ class ExplorationCheckpointTestHelperTest { val checkpoint = retrieveCheckpoint(profileId, FRACTIONS_EXPLORATION_ID_1) assertThat(checkpoint.explorationTitle).isEqualTo(FRACTIONS_EXPLORATION_1_TITLE) assertThat(checkpoint.pendingStateName) - .isEqualTo(FRACTIONS_STORY_0_EXPLORATION_1_FIRST_STATE_NAME - ) + .isEqualTo( + FRACTIONS_STORY_0_EXPLORATION_1_FIRST_STATE_NAME + ) explorationCheckpointTestHelper.updateCheckpointForFractionsStory0Exploration1( profileId = profileId, @@ -188,7 +189,8 @@ class ExplorationCheckpointTestHelperTest { } private fun retrieveCheckpoint( - profileId: ProfileId, explorationId: String + profileId: ProfileId, + explorationId: String ): ExplorationCheckpoint { val retrieveCheckpointProvider = explorationCheckpointController.retrieveExplorationCheckpoint(profileId, explorationId) diff --git a/testing/src/test/java/org/oppia/android/testing/profile/ProfileTestHelperTest.kt b/testing/src/test/java/org/oppia/android/testing/profile/ProfileTestHelperTest.kt index 5799ac7ac84..163b3bf3909 100644 --- a/testing/src/test/java/org/oppia/android/testing/profile/ProfileTestHelperTest.kt +++ b/testing/src/test/java/org/oppia/android/testing/profile/ProfileTestHelperTest.kt @@ -10,8 +10,6 @@ import dagger.BindsInstance import dagger.Component import dagger.Module import dagger.Provides -import javax.inject.Inject -import javax.inject.Singleton import org.junit.Before import org.junit.Rule import org.junit.Test @@ -43,6 +41,8 @@ import org.oppia.android.util.logging.LogLevel import org.oppia.android.util.networking.NetworkConnectionUtilDebugModule import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode +import javax.inject.Inject +import javax.inject.Singleton /** Tests for [ProfileTestHelper]. */ // FunctionName: test names are conventionally named with underscores. diff --git a/testing/src/test/java/org/oppia/android/testing/story/StoryProgressTestHelperTest.kt b/testing/src/test/java/org/oppia/android/testing/story/StoryProgressTestHelperTest.kt index f2635f82ea2..6831d7b2254 100644 --- a/testing/src/test/java/org/oppia/android/testing/story/StoryProgressTestHelperTest.kt +++ b/testing/src/test/java/org/oppia/android/testing/story/StoryProgressTestHelperTest.kt @@ -9,9 +9,6 @@ import dagger.BindsInstance import dagger.Component import dagger.Module import dagger.Provides -import java.util.concurrent.TimeUnit -import javax.inject.Inject -import javax.inject.Singleton import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -63,6 +60,9 @@ import org.oppia.android.util.networking.NetworkConnectionUtilDebugModule import org.oppia.android.util.parser.image.ImageParsingModule import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode +import java.util.concurrent.TimeUnit +import javax.inject.Inject +import javax.inject.Singleton /** Tests for [StoryProgressTestHelper]. */ // FunctionName: test names are conventionally named with underscores. diff --git a/testing/src/test/java/org/oppia/android/testing/threading/CoroutineExecutorServiceTest.kt b/testing/src/test/java/org/oppia/android/testing/threading/CoroutineExecutorServiceTest.kt index 03c5bbb71dc..2e158ee93a1 100644 --- a/testing/src/test/java/org/oppia/android/testing/threading/CoroutineExecutorServiceTest.kt +++ b/testing/src/test/java/org/oppia/android/testing/threading/CoroutineExecutorServiceTest.kt @@ -30,9 +30,11 @@ import org.mockito.junit.MockitoJUnit import org.mockito.junit.MockitoRule import org.oppia.android.testing.TestLogReportingModule import org.oppia.android.testing.assertThrows +import org.oppia.android.testing.data.AsyncResultSubject.Companion.assertThat import org.oppia.android.testing.robolectric.RobolectricModule import org.oppia.android.testing.time.FakeOppiaClockModule import org.oppia.android.testing.time.FakeSystemClock +import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.threading.BackgroundDispatcher import org.robolectric.annotation.LooperMode import java.util.concurrent.Callable @@ -45,9 +47,6 @@ import java.util.concurrent.TimeUnit import java.util.concurrent.TimeoutException import javax.inject.Inject import javax.inject.Singleton -import org.oppia.android.testing.data.AsyncResultSubject -import org.oppia.android.testing.data.AsyncResultSubject.Companion.assertThat -import org.oppia.android.util.data.AsyncResult /** * Tests for [CoroutineExecutorService]. NOTE: significant care should be taken when modifying these diff --git a/utility/src/main/java/org/oppia/android/util/data/AsyncResult.kt b/utility/src/main/java/org/oppia/android/util/data/AsyncResult.kt index a806b740ba9..423c7b18d2e 100644 --- a/utility/src/main/java/org/oppia/android/util/data/AsyncResult.kt +++ b/utility/src/main/java/org/oppia/android/util/data/AsyncResult.kt @@ -157,11 +157,13 @@ sealed class AsyncResult { /** [AsyncResult] representing an operation that succeeded with a specific [value]. */ data class Success( - val value: T, override val resultTimeMillis: Long = SystemClock.uptimeMillis() + val value: T, + override val resultTimeMillis: Long = SystemClock.uptimeMillis() ) : AsyncResult() /** [AsyncResult] representing an operation that failed with a specific [error]. */ data class Failure( - val error: Throwable, override val resultTimeMillis: Long = SystemClock.uptimeMillis() + val error: Throwable, + override val resultTimeMillis: Long = SystemClock.uptimeMillis() ) : AsyncResult() } diff --git a/utility/src/main/java/org/oppia/android/util/data/DataProviders.kt b/utility/src/main/java/org/oppia/android/util/data/DataProviders.kt index d402465f4a8..17ea4d31b8a 100644 --- a/utility/src/main/java/org/oppia/android/util/data/DataProviders.kt +++ b/utility/src/main/java/org/oppia/android/util/data/DataProviders.kt @@ -3,9 +3,6 @@ package org.oppia.android.util.data import android.content.Context import androidx.lifecycle.LiveData import dagger.Reusable -import java.util.concurrent.atomic.AtomicBoolean -import java.util.concurrent.atomic.AtomicReference -import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job @@ -13,6 +10,9 @@ import kotlinx.coroutines.flow.transform import kotlinx.coroutines.launch import org.oppia.android.util.logging.ExceptionLogger import org.oppia.android.util.threading.BackgroundDispatcher +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicReference +import javax.inject.Inject /** * Various functions to create or manipulate [DataProvider]s. diff --git a/utility/src/test/java/org/oppia/android/util/data/AsyncResultTest.kt b/utility/src/test/java/org/oppia/android/util/data/AsyncResultTest.kt index 313ef401516..8ae947d0bd3 100644 --- a/utility/src/test/java/org/oppia/android/util/data/AsyncResultTest.kt +++ b/utility/src/test/java/org/oppia/android/util/data/AsyncResultTest.kt @@ -11,6 +11,7 @@ import kotlinx.coroutines.async import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.oppia.android.testing.data.AsyncResultSubject.Companion.assertThat import org.oppia.android.testing.robolectric.RobolectricModule import org.oppia.android.testing.threading.BackgroundTestDispatcher import org.oppia.android.testing.threading.TestCoroutineDispatcher @@ -18,11 +19,10 @@ import org.oppia.android.testing.threading.TestCoroutineDispatchers import org.oppia.android.testing.threading.TestDispatcherModule import org.oppia.android.testing.time.FakeOppiaClockModule import org.oppia.android.testing.time.FakeSystemClock +import org.oppia.android.util.data.AsyncResult.ChainedFailureException import org.robolectric.annotation.LooperMode import javax.inject.Inject import javax.inject.Singleton -import org.oppia.android.testing.data.AsyncResultSubject.Companion.assertThat -import org.oppia.android.util.data.AsyncResult.ChainedFailureException /** Tests for [AsyncResult]. */ // FunctionName: test names are conventionally named with underscores. @@ -170,7 +170,8 @@ class AsyncResultTest { val resultHash = AsyncResult.Pending().hashCode() assertThat(resultHash).isNotEqualTo( - AsyncResult.Failure(UnsupportedOperationException() + AsyncResult.Failure( + UnsupportedOperationException() ).hashCode() ) } diff --git a/utility/src/test/java/org/oppia/android/util/data/DataProvidersTest.kt b/utility/src/test/java/org/oppia/android/util/data/DataProvidersTest.kt index 7d0ad8eef7f..9a0893e7b53 100644 --- a/utility/src/test/java/org/oppia/android/util/data/DataProvidersTest.kt +++ b/utility/src/test/java/org/oppia/android/util/data/DataProvidersTest.kt @@ -5,11 +5,7 @@ import android.content.Context import androidx.lifecycle.Observer import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.google.common.truth.FailureMetadata -import com.google.common.truth.Subject -import com.google.common.truth.Truth.assertAbout import com.google.common.truth.Truth.assertThat -import com.google.common.truth.Truth.assertWithMessage import dagger.BindsInstance import dagger.Component import dagger.Module @@ -32,6 +28,7 @@ import org.mockito.junit.MockitoJUnit import org.mockito.junit.MockitoRule import org.oppia.android.testing.FakeExceptionLogger import org.oppia.android.testing.TestLogReportingModule +import org.oppia.android.testing.data.AsyncResultSubject.Companion.assertThat import org.oppia.android.testing.robolectric.RobolectricModule import org.oppia.android.testing.threading.TestCoroutineDispatchers import org.oppia.android.testing.threading.TestDispatcherModule @@ -47,8 +44,6 @@ import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode import javax.inject.Inject import javax.inject.Singleton -import org.oppia.android.testing.data.AsyncResultSubject -import org.oppia.android.testing.data.AsyncResultSubject.Companion.assertThat private const val BASE_PROVIDER_ID_0 = "base_id_0" private const val BASE_PROVIDER_ID_1 = "base_id_1" From fd329948fc992b891ba40adb8ab792b65fc71952 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Wed, 9 Mar 2022 18:08:36 -0800 Subject: [PATCH 155/162] Fix gradle tests. I've verified in this commit that all Gradle tests build & run locally (at least on Robolectric). --- data/build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/data/build.gradle b/data/build.gradle index 16bc0587325..4df74e342c7 100644 --- a/data/build.gradle +++ b/data/build.gradle @@ -82,6 +82,7 @@ dependencies { 'androidx.test.ext:junit:1.1.1', 'com.google.dagger:dagger:2.24', 'com.google.truth:truth:1.1.3', + 'com.google.truth.extensions:truth-liteproto-extension:1.1.3', 'com.squareup.okhttp3:mockwebserver:4.1.0', 'com.squareup.okhttp3:okhttp:4.1.0', 'junit:junit:4.12', From 391cf222f11eed6d2b6ecd1a4b073dfa62d896a3 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 17 Mar 2022 14:26:53 -0700 Subject: [PATCH 156/162] Post-merge fix. --- model/src/main/proto/BUILD.bazel | 1 + 1 file changed, 1 insertion(+) diff --git a/model/src/main/proto/BUILD.bazel b/model/src/main/proto/BUILD.bazel index 90da6e48b77..b38796c5237 100644 --- a/model/src/main/proto/BUILD.bazel +++ b/model/src/main/proto/BUILD.bazel @@ -37,6 +37,7 @@ java_lite_proto_library( oppia_proto_library( name = "event_logger_proto", srcs = ["oppia_logger.proto"], + deps = [":exploration_proto"], ) java_lite_proto_library( From c29a47c7b9c1354df839ef85075e7f55d6aea016 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 17 Mar 2022 14:45:25 -0700 Subject: [PATCH 157/162] More post-merge fixes. --- .../app/parser/FractionParsingUiErrorTest.kt | 21 ++++++++++--------- .../android/util/math/FractionParserTest.kt | 3 +-- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/app/src/test/java/org/oppia/android/app/parser/FractionParsingUiErrorTest.kt b/app/src/test/java/org/oppia/android/app/parser/FractionParsingUiErrorTest.kt index c3e2a1c805f..df39f6a196e 100644 --- a/app/src/test/java/org/oppia/android/app/parser/FractionParsingUiErrorTest.kt +++ b/app/src/test/java/org/oppia/android/app/parser/FractionParsingUiErrorTest.kt @@ -157,6 +157,17 @@ class FractionParsingUiErrorTest { } } + @Test + fun testSubmitTimeError_noDenominator_invalidFormat_hasRelevantErrorMessage() { + activityRule.scenario.onActivity { activity -> + val errorMessage = fractionParser.getSubmitTimeError("3/") + .toUiError() + .getErrorMessageFromStringRes(activity.appLanguageResourceHandler) + assertThat(errorMessage) + .isEqualTo("Please enter a valid fraction (e.g., 5/3 or 1 2/3)") + } + } + @Test fun testRealTimeError_validRegularFraction_noErrorMessage() { activityRule.scenario.onActivity { activity -> @@ -211,16 +222,6 @@ class FractionParsingUiErrorTest { } } - @Test - fun testSubmitTimeError_noDenominator_invalidFormat_hasRelevantErrorMessage() { - activityRule.scenario.onActivity { activity -> - val errorMessage = fractionParser.getSubmitTimeError("3/") - .getErrorMessageFromStringRes(activity.appLanguageResourceHandler) - assertThat(errorMessage) - .isEqualTo("Please enter a valid fraction (e.g., 5/3 or 1 2/3)") - } - } - private fun setUpTestApplicationComponent() { ApplicationProvider.getApplicationContext().inject(this) } diff --git a/utility/src/test/java/org/oppia/android/util/math/FractionParserTest.kt b/utility/src/test/java/org/oppia/android/util/math/FractionParserTest.kt index 27988ea1e0e..864a429f753 100644 --- a/utility/src/test/java/org/oppia/android/util/math/FractionParserTest.kt +++ b/utility/src/test/java/org/oppia/android/util/math/FractionParserTest.kt @@ -6,7 +6,6 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.oppia.android.app.model.Fraction -import org.oppia.android.app.parser.StringToFractionParser import org.oppia.android.testing.assertThrows import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode @@ -94,7 +93,7 @@ class FractionParserTest { @Test fun testSubmitTimeError_noDenominator_returnsInvalidFormat() { val error = fractionParser.getSubmitTimeError("3/") - assertThat(error).isEqualTo(StringToFractionParser.FractionParsingError.INVALID_FORMAT) + assertThat(error).isEqualTo(FractionParser.FractionParsingError.INVALID_FORMAT) } @Test From 445664a36757bebf45c9c960ff507ca70f053508 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 17 Mar 2022 14:48:44 -0700 Subject: [PATCH 158/162] Fix TODO comment. --- .../org/oppia/android/testing/math/MathParsingErrorSubject.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/src/main/java/org/oppia/android/testing/math/MathParsingErrorSubject.kt b/testing/src/main/java/org/oppia/android/testing/math/MathParsingErrorSubject.kt index 53c8bad235f..9c62f183aca 100644 --- a/testing/src/main/java/org/oppia/android/testing/math/MathParsingErrorSubject.kt +++ b/testing/src/main/java/org/oppia/android/testing/math/MathParsingErrorSubject.kt @@ -43,7 +43,7 @@ import org.oppia.android.util.math.MathParsingError.UnbalancedParenthesesError import org.oppia.android.util.math.MathParsingError.UnnecessarySymbolsError import org.oppia.android.util.math.MathParsingError.VariableInNumericExpressionError -// TODO(#4132): file issue to add tests. +// TODO(#4132): Add tests for this class. /** * Truth subject for verifying properties of [MathParsingError]s. From 8fdad011de1f97e098b4b2f97e5ff87f058366ed Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 17 Mar 2022 14:57:27 -0700 Subject: [PATCH 159/162] Post-merge lint fixes. --- .../analytics/AnalyticsControllerTest.kt | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/domain/src/test/java/org/oppia/android/domain/oppialogger/analytics/AnalyticsControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/oppialogger/analytics/AnalyticsControllerTest.kt index 78ec27110ef..6c221881999 100644 --- a/domain/src/test/java/org/oppia/android/domain/oppialogger/analytics/AnalyticsControllerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/oppialogger/analytics/AnalyticsControllerTest.kt @@ -12,21 +12,6 @@ import dagger.Provides import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.ACTIVITYCONTEXT_NOT_SET -import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.CONCEPT_CARD_CONTEXT -import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.EXPLORATION_CONTEXT -import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.QUESTION_CONTEXT -import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.REVISION_CARD_CONTEXT -import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.STORY_CONTEXT -import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.TOPIC_CONTEXT -import org.oppia.android.app.model.EventLog.EventAction -import org.mockito.ArgumentCaptor -import org.mockito.Captor -import org.mockito.Mock -import org.mockito.Mockito.atLeastOnce -import org.mockito.Mockito.verify -import org.mockito.junit.MockitoJUnit -import org.mockito.junit.MockitoRule import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.OPEN_CONCEPT_CARD import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.OPEN_EXPLORATION_ACTIVITY import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.OPEN_INFO_TAB From 10081a6bcfddeb9fa54ac4da759acc14b4fa1635 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 17 Mar 2022 15:28:55 -0700 Subject: [PATCH 160/162] Post-merge fix. --- WORKSPACE | 4 ++-- utility/build.gradle | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/WORKSPACE b/WORKSPACE index 83602e3be3a..a79cf09e469 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -133,9 +133,9 @@ git_repository( # min target SDK version to be compatible with Oppia. git_repository( name = "kotlitex", - commit = "ba51aaca442d248b7b44b4eeb5bb9846ad45b245", + commit = "108a12fd8743a09936d614ca7d012c8f079bad62", remote = "https://github.com/oppia/kotlitex", - shallow_since = "1645642252 -0800", + shallow_since = "1647554845 -0700", ) bind( diff --git a/utility/build.gradle b/utility/build.gradle index ee1c5ea55f1..eaec50931fe 100644 --- a/utility/build.gradle +++ b/utility/build.gradle @@ -65,7 +65,7 @@ dependencies { 'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0-alpha03', 'androidx.work:work-runtime-ktx:2.4.0', 'com.github.oppia:androidsvg:6bd15f69caee3e6857fcfcd123023716b4adec1d', - 'com.github.oppia:kotlitex:ba51aaca442d248b7b44b4eeb5bb9846ad45b245', + 'com.github.oppia:kotlitex:108a12fd8743a09936d614ca7d012c8f079bad62', 'com.github.bumptech.glide:glide:4.11.0', 'com.google.dagger:dagger:2.24', 'com.google.firebase:firebase-analytics-ktx:17.5.0', From 06b51d3e65a78090a3c53c42b39f1bd948916f0c Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Thu, 17 Mar 2022 18:02:13 -0700 Subject: [PATCH 161/162] Fix exploration routing issue. The underlying problem was that the PR inadvertently changed the behavior of comparing two results wherein results of different times would be considered different and be re-delivered (which happened to break exploration routing, and likely a variety of other places). This introduces a new method for checking equivelance rather than confusingly assuming that users of AsyncResult don't care about the result's time during comparison checking. New tests have been added to verify the functionality works as expected (and fails when expected), and I manually verified that the exploration routing issue was fixed. More details on the specific user-facing issue will be added to the PR as a comment. --- .../testing/data/AsyncResultSubject.kt | 7 + .../oppia/android/util/data/AsyncResult.kt | 40 ++++- .../oppia/android/util/data/DataProviders.kt | 4 +- .../android/util/data/AsyncResultTest.kt | 165 ++++++++++++++++++ .../android/util/data/DataProvidersTest.kt | 44 ++++- 5 files changed, 248 insertions(+), 12 deletions(-) diff --git a/testing/src/main/java/org/oppia/android/testing/data/AsyncResultSubject.kt b/testing/src/main/java/org/oppia/android/testing/data/AsyncResultSubject.kt index d610a1d6154..a4f3eeb8155 100644 --- a/testing/src/main/java/org/oppia/android/testing/data/AsyncResultSubject.kt +++ b/testing/src/main/java/org/oppia/android/testing/data/AsyncResultSubject.kt @@ -169,6 +169,13 @@ class AsyncResultSubject( assertThat(actual.isNewerThanOrSameAgeAs(other)).isFalse() } + /** + * Returns a [BooleanSubject] for verifying whether the [AsyncResult] under test and [other] + * effectively have the same value per [AsyncResult.hasSameEffectiveValueAs]. + */ + fun hasSameEffectiveValueAs(other: AsyncResult): BooleanSubject = + assertThat(actual.hasSameEffectiveValueAs(other)) + /** * Verifies the result under test is successful (per [ensureActualIsType]) and returns its * [AsyncResult.Success.value] as type [T] (this method will fail if the conversion can't happen). diff --git a/utility/src/main/java/org/oppia/android/util/data/AsyncResult.kt b/utility/src/main/java/org/oppia/android/util/data/AsyncResult.kt index 423c7b18d2e..1cdc6230dd5 100644 --- a/utility/src/main/java/org/oppia/android/util/data/AsyncResult.kt +++ b/utility/src/main/java/org/oppia/android/util/data/AsyncResult.kt @@ -39,6 +39,12 @@ import android.os.SystemClock * to utilize it as a signal that a long operation is underway). This API may be changed in the * future to make these design choices more clear-cut and deliberate when implementing data * providers. + * + * **A note on equality:** Two [AsyncResult]s may not be equal or have the same hash code if they + * are different ages as indicated by [resultTimeMillis]. For this reason, care must be taken when + * storing results in hash or equivalence based data structures. When comparing two results where + * the results' ages should be ignored, one can use [hasSameEffectiveValueAs] instead of direct + * equality checking. */ sealed class AsyncResult { /** @@ -58,6 +64,22 @@ sealed class AsyncResult { return resultTimeMillis >= otherResult.resultTimeMillis } + /** + * Returns whether this result has the same effective value as [other], that is, that the two can + * be considered equal in value but not necessarily age. + * + * This function is useful for cases when the end effect of processing an [AsyncResult] is not + * dependent on the age of the result (indicated by [resultTimeMillis]) which may affect functions + * like [equals] and [hashCode]. + */ + fun hasSameEffectiveValueAs(other: AsyncResult): Boolean { + return when (val thisResult = this) { + is Pending -> other is Pending // Two pending results are effectively equal. + is Success -> if (other is Success) thisResult.isEffectivelyEqualTo(other) else false + is Failure -> if (other is Failure) thisResult.isEffectivelyEqualTo(other) else false + } + } + /** * Returns a version of this result that retains its pending and failed states, but transforms its * success state according to the specified transformation function. @@ -159,11 +181,25 @@ sealed class AsyncResult { data class Success( val value: T, override val resultTimeMillis: Long = SystemClock.uptimeMillis() - ) : AsyncResult() + ) : AsyncResult() { + /** + * Returns whether this [Success] is effectively equal to [otherResult] (i.e., has the same + * [value], but not necessarily the same [resultTimeMillis]). + */ + internal fun isEffectivelyEqualTo(otherResult: Success): Boolean = + value == otherResult.value + } /** [AsyncResult] representing an operation that failed with a specific [error]. */ data class Failure( val error: Throwable, override val resultTimeMillis: Long = SystemClock.uptimeMillis() - ) : AsyncResult() + ) : AsyncResult() { + /** + * Returns whether this [Failure] is effectively equal to [otherResult] (i.e., has the same + * [error], but not necessarily the same [resultTimeMillis]). + */ + internal fun isEffectivelyEqualTo(otherResult: Failure): Boolean = + error == otherResult.error + } } diff --git a/utility/src/main/java/org/oppia/android/util/data/DataProviders.kt b/utility/src/main/java/org/oppia/android/util/data/DataProviders.kt index 17ea4d31b8a..26a4c8b2fd9 100644 --- a/utility/src/main/java/org/oppia/android/util/data/DataProviders.kt +++ b/utility/src/main/java/org/oppia/android/util/data/DataProviders.kt @@ -330,7 +330,9 @@ class DataProviders @Inject constructor( checkNotNull(value) { "Null values should not be posted to NotifiableAsyncLiveData." } val currentCache = cache // This is safe because cache can only be changed on the main thread. if (currentCache != null) { - if (value.isNewerThanOrSameAgeAs(currentCache) && currentCache != value) { + if (value.isNewerThanOrSameAgeAs(currentCache) && + !currentCache.hasSameEffectiveValueAs(value) + ) { // Only propagate the value if it's changed and is newer since it's possible for observer // callbacks to happen out-of-order. cache = value diff --git a/utility/src/test/java/org/oppia/android/util/data/AsyncResultTest.kt b/utility/src/test/java/org/oppia/android/util/data/AsyncResultTest.kt index 8ae947d0bd3..64c8ed47ed3 100644 --- a/utility/src/test/java/org/oppia/android/util/data/AsyncResultTest.kt +++ b/utility/src/test/java/org/oppia/android/util/data/AsyncResultTest.kt @@ -150,6 +150,45 @@ class AsyncResultTest { assertThat(result).isNotEqualTo(AsyncResult.Success("Success")) } + @Test + fun testPendingResult_andPendingResult_sameExceptAge_areNotEqual() { + val result1 = AsyncResult.Pending() + + fakeSystemClock.advanceTime(millis = 10) + val result2 = AsyncResult.Pending() + + assertThat(result1).isNotEqualTo(result2) + } + + @Test + fun testPendingResult_andPendingResult_sameExceptAge_areEffectivelyEqual() { + val result1 = AsyncResult.Pending() + + fakeSystemClock.advanceTime(millis = 10) + val result2 = AsyncResult.Pending() + + // Two pending results are always effectively equal. + assertThat(result1).hasSameEffectiveValueAs(result2).isTrue() + } + + @Test + fun testPendingResult_andSuccessResult_areNotEffectivelyEqual() { + val result1 = AsyncResult.Pending() + val result2 = AsyncResult.Success("Success") + + // A pending result is never equivalent to a successful one. + assertThat(result1).hasSameEffectiveValueAs(result2).isFalse() + } + + @Test + fun testPendingResult_andFailureResult_areNotEffectivelyEqual() { + val result1 = AsyncResult.Pending() + val result2 = AsyncResult.Failure(UnsupportedOperationException()) + + // A pending result is never equivalent to a failing one. + assertThat(result1).hasSameEffectiveValueAs(result2).isFalse() + } + @Test fun testPendingResult_hashCode_isEqualToAnotherPendingResult() { val resultHash = AsyncResult.Pending().hashCode() @@ -176,6 +215,16 @@ class AsyncResultTest { ) } + @Test + fun testPendingResult_andPendingResult_sameExceptAge_hashCodes_areNotEqual() { + val result1 = AsyncResult.Pending() + + fakeSystemClock.advanceTime(millis = 10) + val result2 = AsyncResult.Pending() + + assertThat(result1.hashCode()).isNotEqualTo(result2.hashCode()) + } + @Test fun testPendingResult_comparedWithItself_isTheSameAge() { val result = AsyncResult.Pending() @@ -413,6 +462,53 @@ class AsyncResultTest { assertThat(result).isNotEqualTo(AsyncResult.Failure(UnsupportedOperationException())) } + @Test + fun testSucceededResult_andSuccessfulResult_sameExceptAge_areNotEqual() { + val result1 = AsyncResult.Success("Success") + + fakeSystemClock.advanceTime(millis = 10) + val result2 = AsyncResult.Success("Success") + + assertThat(result1).isNotEqualTo(result2) + } + + @Test + fun testSucceededResult_andPendingResult_areNotEffectivelyEqual() { + val result1 = AsyncResult.Success("Success") + val result2 = AsyncResult.Pending() + + // A successful result is never equivalent to a pending one. + assertThat(result1).hasSameEffectiveValueAs(result2).isFalse() + } + + @Test + fun testSucceededResult_andSucceededResult_sameValue_differentAges_areEffectivelyEqual() { + val result1 = AsyncResult.Success("Success") + + fakeSystemClock.advanceTime(millis = 10) + val result2 = AsyncResult.Success("Success") + + assertThat(result1).hasSameEffectiveValueAs(result2).isTrue() + } + + @Test + fun testSucceededResult_andSucceededResult_differentValues_areNotEffectivelyEqual() { + val result1 = AsyncResult.Success("Success1") + val result2 = AsyncResult.Success("Success2") + + // The two results have different effective values. + assertThat(result1).hasSameEffectiveValueAs(result2).isFalse() + } + + @Test + fun testSucceededResult_andFailureResult_areNotEffectivelyEqual() { + val result1 = AsyncResult.Success("Success1") + val result2 = AsyncResult.Failure(UnsupportedOperationException()) + + // A successful result is never equivalent to a failing one. + assertThat(result1).hasSameEffectiveValueAs(result2).isFalse() + } + @Test fun testSucceededResult_hashCode_isNotEqualToPendingResult() { val resultHash = AsyncResult.Success("Success").hashCode() @@ -451,6 +547,16 @@ class AsyncResultTest { ) } + @Test + fun testSucceededResult_andSucceededResult_sameExceptAge_hashCodes_areNotEqual() { + val result1 = AsyncResult.Success("Success") + + fakeSystemClock.advanceTime(millis = 10) + val result2 = AsyncResult.Success("Success") + + assertThat(result1.hashCode()).isNotEqualTo(result2.hashCode()) + } + @Test fun testSucceededResult_comparedWithItself_isTheSameAge() { val result = AsyncResult.Success("value") @@ -692,6 +798,54 @@ class AsyncResultTest { ) } + @Test + fun testFailedResult_andFailedResult_sameExceptAge_areNotEqual() { + val result1 = AsyncResult.Failure(UnsupportedOperationException()) + + fakeSystemClock.advanceTime(millis = 10) + val result2 = AsyncResult.Failure(UnsupportedOperationException()) + + assertThat(result1).isNotEqualTo(result2) + } + + @Test + fun testFailedResult_andPendingResult_areNotEffectivelyEqual() { + val result1 = AsyncResult.Failure(UnsupportedOperationException()) + val result2 = AsyncResult.Pending() + + // A failing result is never equivalent to a pending one. + assertThat(result1).hasSameEffectiveValueAs(result2).isFalse() + } + + @Test + fun testFailedResult_andSucceededResult_areNotEffectivelyEqual() { + val result1 = AsyncResult.Failure(UnsupportedOperationException()) + val result2 = AsyncResult.Success("Success1") + + // A failing result is never equivalent to a successful one. + assertThat(result1).hasSameEffectiveValueAs(result2).isFalse() + } + + @Test + fun testFailedResult_andFailedResult_sameException_differentAges_areEffectivelyEqual() { + val exception = UnsupportedOperationException("Reason") + val result1 = AsyncResult.Failure(exception) + + fakeSystemClock.advanceTime(millis = 10) + val result2 = AsyncResult.Failure(exception) + + assertThat(result1).hasSameEffectiveValueAs(result2).isTrue() + } + + @Test + fun testFailedResult_andFailedResult_differentValues_areNotEffectivelyEqual() { + val result1 = AsyncResult.Failure(UnsupportedOperationException("Reason 1")) + val result2 = AsyncResult.Failure(UnsupportedOperationException("Reason 2")) + + // The two results have different effective values. + assertThat(result1).hasSameEffectiveValueAs(result2).isFalse() + } + @Test fun testFailedResult_hashCode_isNotEqualToPendingResult() { val resultHash = AsyncResult.Failure(UnsupportedOperationException("Reason")).hashCode() @@ -727,6 +881,17 @@ class AsyncResultTest { ) } + @Test + fun testFailedResult_andFailedResult_sameExceptAge_hashCodes_areNotEqual() { + val exception = UnsupportedOperationException("Reason") + val result1 = AsyncResult.Failure(exception) + + fakeSystemClock.advanceTime(millis = 10) + val result2 = AsyncResult.Failure(exception) + + assertThat(result1.hashCode()).isNotEqualTo(result2.hashCode()) + } + @Test fun testFailedResult_comparedWithItself_isTheSameAge() { val result = AsyncResult.Failure(RuntimeException()) diff --git a/utility/src/test/java/org/oppia/android/util/data/DataProvidersTest.kt b/utility/src/test/java/org/oppia/android/util/data/DataProvidersTest.kt index 9a0893e7b53..27085a29b38 100644 --- a/utility/src/test/java/org/oppia/android/util/data/DataProvidersTest.kt +++ b/utility/src/test/java/org/oppia/android/util/data/DataProvidersTest.kt @@ -23,7 +23,7 @@ import org.mockito.Mock import org.mockito.Mockito.atLeastOnce import org.mockito.Mockito.reset import org.mockito.Mockito.verify -import org.mockito.Mockito.verifyZeroInteractions +import org.mockito.Mockito.verifyNoMoreInteractions import org.mockito.junit.MockitoJUnit import org.mockito.junit.MockitoRule import org.oppia.android.testing.FakeExceptionLogger @@ -33,6 +33,7 @@ import org.oppia.android.testing.robolectric.RobolectricModule import org.oppia.android.testing.threading.TestCoroutineDispatchers import org.oppia.android.testing.threading.TestDispatcherModule import org.oppia.android.testing.time.FakeOppiaClockModule +import org.oppia.android.testing.time.FakeSystemClock import org.oppia.android.util.data.DataProviders.Companion.combineWith import org.oppia.android.util.data.DataProviders.Companion.combineWithAsync import org.oppia.android.util.data.DataProviders.Companion.toLiveData @@ -104,6 +105,9 @@ class DataProvidersTest { @Captor lateinit var intResultCaptor: ArgumentCaptor> + @Inject + lateinit var fakeSystemClock: FakeSystemClock + private var inMemoryCachedStr: String? = null private var inMemoryCachedStr2: String? = null @@ -228,7 +232,7 @@ class DataProvidersTest { testCoroutineDispatchers.advanceUntilIdle() // The observer should have no interactions since the data hasn't changed. - verifyZeroInteractions(mockIntLiveDataObserver) + verifyNoMoreInteractions(mockIntLiveDataObserver) } @Test @@ -266,6 +270,28 @@ class DataProvidersTest { assertThat(simpleDataProvider.callCount).isEqualTo(2) // Sanity check for the test logic itself. } + @Test + fun testConvertToLiveData_dataProvider_providesPendingResultTwice_doesNotRedeliver() { + val simpleDataProvider = object : DataProvider(context) { + override fun getId(): Any = "simple_data_provider" + + // Return a new pending result for each call. + override suspend fun retrieveData(): AsyncResult = AsyncResult.Pending() + } + // Ensure the initial value is retrieved. + simpleDataProvider.toLiveData().observeForever(mockIntLiveDataObserver) + testCoroutineDispatchers.advanceUntilIdle() + reset(mockIntLiveDataObserver) + + testCoroutineDispatchers.advanceTimeBy(10) + asyncDataSubscriptionManager.notifyChangeAsync(simpleDataProvider.getId()) + testCoroutineDispatchers.advanceUntilIdle() + + // Despite there being a notification, it shouldn't redeliver the result since the two values + // are effectively equal. + verifyNoMoreInteractions(mockIntLiveDataObserver) + } + @Test fun testInMemoryDataProvider_toLiveData_deliversInMemoryValue() { val dataProvider = createSuccessfulDataProvider(BASE_PROVIDER_ID_0, STR_VALUE_0) @@ -288,7 +314,7 @@ class DataProvidersTest { testCoroutineDispatchers.advanceUntilIdle() // The observer should not be notified again since the value hasn't changed. - verifyZeroInteractions(mockStringLiveDataObserver) + verifyNoMoreInteractions(mockStringLiveDataObserver) } @Test @@ -479,7 +505,7 @@ class DataProvidersTest { dataProvider.toLiveData().observeForever(mockStringLiveDataObserver) // The observer should never be called since the underlying async function hasn't yet completed. - verifyZeroInteractions(mockStringLiveDataObserver) + verifyNoMoreInteractions(mockStringLiveDataObserver) } @Test @@ -889,7 +915,7 @@ class DataProvidersTest { dataProvider.toLiveData().observeForever(mockIntLiveDataObserver) // No value should be delivered since the async function is blocked. - verifyZeroInteractions(mockIntLiveDataObserver) + verifyNoMoreInteractions(mockIntLiveDataObserver) } @Test @@ -2115,7 +2141,7 @@ class DataProvidersTest { dataProvider.toLiveData().observeForever(mockStringLiveDataObserver) // The value should not yet be delivered since the first provider is blocked. - verifyZeroInteractions(mockStringLiveDataObserver) + verifyNoMoreInteractions(mockStringLiveDataObserver) } @Test @@ -2154,7 +2180,7 @@ class DataProvidersTest { dataProvider.toLiveData().observeForever(mockStringLiveDataObserver) // The value should not yet be delivered since the first provider is blocked. - verifyZeroInteractions(mockStringLiveDataObserver) + verifyNoMoreInteractions(mockStringLiveDataObserver) } @Test @@ -2191,7 +2217,7 @@ class DataProvidersTest { dataProvider.toLiveData().observeForever(mockStringLiveDataObserver) // The value should not yet be delivered. - verifyZeroInteractions(mockStringLiveDataObserver) + verifyNoMoreInteractions(mockStringLiveDataObserver) } @Test @@ -2359,7 +2385,7 @@ class DataProvidersTest { dataProvider.toLiveData().observeForever(mockIntLiveDataObserver) // No value should be delivered since the async function is blocked. - verifyZeroInteractions(mockIntLiveDataObserver) + verifyNoMoreInteractions(mockIntLiveDataObserver) } @Test From 2bd741a712d3baace4cb6d6f296cf2a8a0f859b3 Mon Sep 17 00:00:00 2001 From: Ben Henning Date: Fri, 18 Mar 2022 01:24:57 -0700 Subject: [PATCH 162/162] Update KotliTeX version. This version doesn't have debug drawing enabled. --- WORKSPACE | 2 +- utility/build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/WORKSPACE b/WORKSPACE index a79cf09e469..52fbdf3c363 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -133,7 +133,7 @@ git_repository( # min target SDK version to be compatible with Oppia. git_repository( name = "kotlitex", - commit = "108a12fd8743a09936d614ca7d012c8f079bad62", + commit = "6b7db8ff9e0f4a70bdaa25f482143e038fd0c301", remote = "https://github.com/oppia/kotlitex", shallow_since = "1647554845 -0700", ) diff --git a/utility/build.gradle b/utility/build.gradle index eaec50931fe..99271dbc3ae 100644 --- a/utility/build.gradle +++ b/utility/build.gradle @@ -65,7 +65,7 @@ dependencies { 'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0-alpha03', 'androidx.work:work-runtime-ktx:2.4.0', 'com.github.oppia:androidsvg:6bd15f69caee3e6857fcfcd123023716b4adec1d', - 'com.github.oppia:kotlitex:108a12fd8743a09936d614ca7d012c8f079bad62', + 'com.github.oppia:kotlitex:6b7db8ff9e0f4a70bdaa25f482143e038fd0c301', 'com.github.bumptech.glide:glide:4.11.0', 'com.google.dagger:dagger:2.24', 'com.google.firebase:firebase-analytics-ktx:17.5.0',