#include "TestHarness.h" #include "src/weather/WeatherParser.h" #include "src/weather/WeatherLocationResolver.h" #include "src/weather/WeatherSummaryFormatter.h" namespace { WeatherLocationCandidate candidate( const QString &city, const QString &admin, const QString &country, const QString &countryCode, const QString &featureCode, qint64 population, double latitude, double longitude) { WeatherLocationCandidate value; value.cityName = city; value.adminName = admin; value.countryName = country; value.countryCode = countryCode; value.featureCode = featureCode; value.population = population; value.latitude = latitude; value.longitude = longitude; return value; } } QA_TEST(WeatherParser, ExtractsExplicitCitiesAndDates) { const WeatherParser parser; const WeatherQuery current = parser.parse(QStringLiteral("西安天气怎么样")); const WeatherQuery tomorrow = parser.parse(QStringLiteral("明天西安天气怎么样")); const WeatherQuery afterTomorrow = parser.parse(QStringLiteral("后天纽约天气")); QA_EXPECT_EQ(current.cityName, QStringLiteral("西安")); QA_EXPECT_EQ(current.kind, WeatherQueryKind::Current); QA_EXPECT_EQ(tomorrow.cityName, QStringLiteral("西安")); QA_EXPECT_EQ(tomorrow.dateOffset, 1); QA_EXPECT_EQ(afterTomorrow.cityName, QStringLiteral("纽约")); QA_EXPECT_EQ(afterTomorrow.dateOffset, 2); } QA_TEST(WeatherParser, ParsesRangeAndDefaultCityRequest) { const WeatherParser parser; const WeatherQuery range = parser.parse(QStringLiteral("未来三天北京天气")); const WeatherQuery defaultCity = parser.parse(QStringLiteral("今天天气怎么样")); QA_EXPECT_EQ(range.kind, WeatherQueryKind::Range); QA_EXPECT_EQ(range.forecastDays, 3); QA_EXPECT_EQ(range.cityName, QStringLiteral("北京")); QA_EXPECT(defaultCity.cityName.isEmpty()); } QA_TEST(WeatherParser, RejectsUnsupportedQueries) { const WeatherParser parser; QA_EXPECT(!parser.parse(QStringLiteral("西安空气质量")).unsupportedReason.isEmpty()); QA_EXPECT(!parser.parse(QStringLiteral("北京天气预警")).unsupportedReason.isEmpty()); QA_EXPECT(!parser.parse(QStringLiteral("明天穿什么")).unsupportedReason.isEmpty()); } QA_TEST(WeatherLocationResolver, ResolvesChineseInternationalCities) { const WeatherLocationSearch newYork = WeatherLocationResolver::resolveSearch(QStringLiteral("纽约")); const WeatherLocationSearch tokyo = WeatherLocationResolver::resolveSearch(QStringLiteral("东京")); const WeatherLocationSearch japanTokyo = WeatherLocationResolver::resolveSearch(QStringLiteral("日本东京")); const WeatherLocationSearch qualifiedTokyo = WeatherLocationResolver::resolveSearch(QStringLiteral("东京,日本")); QA_EXPECT_EQ(newYork.queryName, QStringLiteral("New York")); QA_EXPECT_EQ(newYork.countryCode, QStringLiteral("US")); QA_EXPECT_EQ(tokyo.queryName, QStringLiteral("Tokyo")); QA_EXPECT_EQ(tokyo.countryCode, QStringLiteral("JP")); QA_EXPECT_EQ(japanTokyo.queryName, QStringLiteral("Tokyo")); QA_EXPECT_EQ(japanTokyo.countryCode, QStringLiteral("JP")); QA_EXPECT_EQ(qualifiedTokyo.queryName, QStringLiteral("Tokyo")); QA_EXPECT_EQ(qualifiedTokyo.countryCode, QStringLiteral("JP")); } QA_TEST(WeatherLocationResolver, PreservesDomesticCityAndCountryQualifier) { const WeatherLocationSearch xian = WeatherLocationResolver::resolveSearch(QStringLiteral("西安市")); const WeatherLocationSearch chinaXian = WeatherLocationResolver::resolveSearch(QStringLiteral("中国西安")); QA_EXPECT_EQ(xian.queryName, QStringLiteral("西安")); QA_EXPECT(xian.countryCode.isEmpty()); QA_EXPECT_EQ(chinaXian.queryName, QStringLiteral("西安")); QA_EXPECT_EQ(chinaXian.countryCode, QStringLiteral("CN")); } QA_TEST(WeatherLocationResolver, SuppressesLowValueAlternatives) { const QVector candidates = { candidate(QStringLiteral("西安"), QStringLiteral("陕西"), QStringLiteral("中国"), QStringLiteral("CN"), QStringLiteral("PPLA"), 6500000, 34.26, 108.93), candidate(QStringLiteral("西安"), QStringLiteral("湖南"), QStringLiteral("中国"), QStringLiteral("CN"), QStringLiteral("PPL"), 1200, 27.10, 111.20), candidate(QStringLiteral("西安"), QStringLiteral("广东"), QStringLiteral("中国"), QStringLiteral("CN"), QStringLiteral("PPL"), -1, 23.10, 113.20), }; const QVector selected = WeatherLocationResolver::selectRelevantCandidates( candidates, WeatherLocationResolver::resolveSearch(QStringLiteral("西安"))); QA_EXPECT_EQ(selected.size(), 1); QA_EXPECT(!WeatherLocationResolver::hasAmbiguity(selected)); } QA_TEST(WeatherLocationResolver, KeepsPlausibleAmbiguity) { const QVector candidates = { candidate(QStringLiteral("Springfield"), QStringLiteral("Missouri"), QStringLiteral("United States"), QStringLiteral("US"), QStringLiteral("PPLA2"), 170000, 37.21, -93.29), candidate(QStringLiteral("Springfield"), QStringLiteral("Illinois"), QStringLiteral("United States"), QStringLiteral("US"), QStringLiteral("PPLA"), 114000, 39.80, -89.64), candidate(QStringLiteral("Springfield"), QStringLiteral("Georgia"), QStringLiteral("United States"), QStringLiteral("US"), QStringLiteral("PPL"), 2700, 32.37, -81.31), }; const QVector selected = WeatherLocationResolver::selectRelevantCandidates( candidates, WeatherLocationResolver::resolveSearch(QStringLiteral("Springfield"))); QA_EXPECT_EQ(selected.size(), 2); QA_EXPECT(WeatherLocationResolver::hasAmbiguity(selected)); } QA_TEST(WeatherLocationResolver, AppliesExplicitCountryFilter) { const QVector candidates = { candidate(QStringLiteral("Tokyo"), QStringLiteral("Tokyo"), QStringLiteral("Japan"), QStringLiteral("JP"), QStringLiteral("PPLC"), 8300000, 35.68, 139.69), candidate(QStringLiteral("Tokyo"), QStringLiteral("Texas"), QStringLiteral("United States"), QStringLiteral("US"), QStringLiteral("PPL"), 1000, 33.90, -102.50), }; const QVector selected = WeatherLocationResolver::selectRelevantCandidates( candidates, WeatherLocationResolver::resolveSearch(QStringLiteral("日本东京"))); QA_EXPECT_EQ(selected.size(), 1); QA_EXPECT_EQ(selected.first().countryCode, QStringLiteral("JP")); } QA_TEST(WeatherLocationResolver, ParsesBothIpLocationSchemas) { const QByteArray ipApiPayload = QByteArrayLiteral( R"({"city":"Xi'an","region":"Shaanxi","country_name":"China","latitude":34.26,"longitude":108.94,"timezone":"Asia/Shanghai"})"); const QByteArray ipWhoIsPayload = QByteArrayLiteral( R"({"success":true,"city":"Tokyo","region":"Tokyo","country":"Japan","latitude":35.68,"longitude":139.69,"timezone":{"id":"Asia/Tokyo"}})"); WeatherLocation ipApiLocation; WeatherLocation ipWhoIsLocation; QA_EXPECT(WeatherLocationResolver::parseIpLocationResponse( ipApiPayload, WeatherIpLocationProvider::IpApiCo, &ipApiLocation)); QA_EXPECT(WeatherLocationResolver::parseIpLocationResponse( ipWhoIsPayload, WeatherIpLocationProvider::IpWhoIs, &ipWhoIsLocation)); QA_EXPECT_EQ(ipApiLocation.cityName, QStringLiteral("Xi'an")); QA_EXPECT_EQ(ipApiLocation.countryName, QStringLiteral("China")); QA_EXPECT_EQ(ipApiLocation.timezone, QStringLiteral("Asia/Shanghai")); QA_EXPECT_EQ(ipWhoIsLocation.cityName, QStringLiteral("Tokyo")); QA_EXPECT_EQ(ipWhoIsLocation.countryName, QStringLiteral("Japan")); QA_EXPECT_EQ(ipWhoIsLocation.timezone, QStringLiteral("Asia/Tokyo")); } QA_TEST(WeatherLocationResolver, RejectsBrokenIpLocationResponses) { WeatherLocation location; QA_EXPECT(!WeatherLocationResolver::parseIpLocationResponse( QByteArrayLiteral("{broken"), WeatherIpLocationProvider::IpApiCo, &location)); QA_EXPECT(!WeatherLocationResolver::parseIpLocationResponse( QByteArrayLiteral(R"({"success":false,"message":"rate limited"})"), WeatherIpLocationProvider::IpWhoIs, &location)); QA_EXPECT(!WeatherLocationResolver::parseIpLocationResponse( QByteArrayLiteral(R"({"city":"Tokyo","latitude":35.68})"), WeatherIpLocationProvider::IpWhoIs, &location)); } QA_TEST(WeatherFormatter, MapsWeatherCodesAndWindDirections) { QA_EXPECT_EQ(WeatherSummaryFormatter::weatherCodeText(0), QStringLiteral("晴")); QA_EXPECT_EQ(WeatherSummaryFormatter::weatherCodeText(95), QStringLiteral("雷暴")); QA_EXPECT_EQ(WeatherSummaryFormatter::weatherCodeText(-1), QStringLiteral("未知天气")); QA_EXPECT_EQ(WeatherSummaryFormatter::windDirectionText(0), QStringLiteral("北风")); QA_EXPECT_EQ(WeatherSummaryFormatter::windDirectionText(90), QStringLiteral("东风")); QA_EXPECT_EQ(WeatherSummaryFormatter::windDirectionText(225), QStringLiteral("西南风")); } QA_TEST(WeatherFormatter, FormatsCurrentWeatherWithSource) { WeatherReport report; report.query.kind = WeatherQueryKind::Current; report.location.cityName = QStringLiteral("西安"); report.location.countryName = QStringLiteral("中国"); report.location.source = WeatherLocationSource::SettingsDefault; report.current.valid = true; report.current.weatherCode = 0; report.current.hasTemperature = true; report.current.temperatureC = 28.5; const QString message = WeatherSummaryFormatter().format(report); QA_EXPECT(message.contains(QStringLiteral("使用设置页默认城市"))); QA_EXPECT(message.contains(QStringLiteral("西安,中国当前天气:晴"))); QA_EXPECT(message.contains(QStringLiteral("28.5℃"))); } QA_TEST(WeatherFormatter, FormatsAmbiguousLocationAndMissingData) { WeatherReport report; report.query.kind = WeatherQueryKind::Current; report.location.cityName = QStringLiteral("Springfield"); report.location.countryName = QStringLiteral("United States"); report.hasLocationAmbiguity = true; report.locationCandidates = { {QStringLiteral("Springfield"), QStringLiteral("Illinois"), QStringLiteral("United States"), {}, 0.0, 0.0}, {QStringLiteral("Springfield"), QStringLiteral("Missouri"), QStringLiteral("United States"), {}, 0.0, 0.0}, }; const QString message = WeatherSummaryFormatter().format(report); QA_EXPECT(message.contains(QStringLiteral("可能存在同名城市"))); QA_EXPECT(message.contains(QStringLiteral("Missouri"))); QA_EXPECT(message.contains(QStringLiteral("缺少当前天气"))); }