Commit 8af80017 authored by Victor Costan's avatar Victor Costan Committed by Commit Bot

AppCache: Modernize lifecycle markers in AppCacheQuotaClientTest.

This CL replaces smart pointers (scoped_refptr) with references where
the pointers are guaranteed to be non-null, and the objects are only
used during the method call.

This removes the test's reliance on QuotaClient being ref-counted, which
will come in handy when QuotaClient gets mojofied.

Bug: 1016065
Change-Id: I0ec5ea2f40c03bbdb4e6af504d6f2b4902115c31
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2496743
Auto-Submit: Victor Costan <pwnall@chromium.org>
Reviewed-by: default avatarenne <enne@chromium.org>
Commit-Queue: Victor Costan <pwnall@chromium.org>
Cr-Commit-Position: refs/heads/master@{#821432}
parent 7ed44a41
......@@ -60,6 +60,7 @@ AppCacheQuotaClient::~AppCacheQuotaClient() {
void AppCacheQuotaClient::OnQuotaManagerDestroyed() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DeletePendingRequests();
if (!current_delete_request_callback_.is_null()) {
current_delete_request_callback_.Reset();
......@@ -169,6 +170,8 @@ void AppCacheQuotaClient::PerformStorageCleanup(blink::mojom::StorageType type,
}
void AppCacheQuotaClient::DidDeleteAppCachesForOrigin(int rv) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// Finish the request by calling our callers callback.
std::move(current_delete_request_callback_)
.Run(NetErrorCodeToQuotaStatus(rv));
......@@ -181,6 +184,7 @@ void AppCacheQuotaClient::DidDeleteAppCachesForOrigin(int rv) {
void AppCacheQuotaClient::GetOriginsHelper(const std::string& opt_host,
GetOriginsForTypeCallback callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(!callback.is_null());
if (service_is_destroyed_) {
......@@ -215,7 +219,9 @@ void AppCacheQuotaClient::GetOriginsHelper(const std::string& opt_host,
}
void AppCacheQuotaClient::ProcessPendingRequests() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(appcache_is_ready_);
while (!pending_batch_requests_.empty())
RunFront(&pending_batch_requests_);
......@@ -224,12 +230,15 @@ void AppCacheQuotaClient::ProcessPendingRequests() {
}
void AppCacheQuotaClient::DeletePendingRequests() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
pending_batch_requests_.clear();
pending_serial_requests_.clear();
}
net::CancelableCompletionRepeatingCallback*
AppCacheQuotaClient::GetServiceDeleteCallback() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// Lazily created due to base::CancelableCallback's threading restrictions,
// there is no way to detach from the thread created on.
if (!service_delete_callback_) {
......@@ -244,6 +253,7 @@ AppCacheQuotaClient::GetServiceDeleteCallback() {
void AppCacheQuotaClient::NotifyAppCacheReady() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// Can reoccur during reinitialization.
if (!appcache_is_ready_) {
appcache_is_ready_ = true;
......
......@@ -9,7 +9,6 @@
#include "base/bind.h"
#include "base/memory/scoped_refptr.h"
#include "base/memory/weak_ptr.h"
#include "base/run_loop.h"
#include "content/browser/appcache/appcache_quota_client.h"
#include "content/browser/appcache/mock_appcache_service.h"
......@@ -41,80 +40,82 @@ class AppCacheQuotaClientTest : public testing::Test {
kOriginB(url::Origin::Create(GURL("http://host:8000"))),
kOriginOther(url::Origin::Create(GURL("http://other"))) {}
int64_t GetOriginUsage(scoped_refptr<storage::QuotaClient> client,
int64_t GetOriginUsage(storage::QuotaClient& client,
const url::Origin& origin,
StorageType type) {
usage_ = -1;
AsyncGetOriginUsage(std::move(client), origin, type);
AsyncGetOriginUsage(client, origin, type);
base::RunLoop().RunUntilIdle();
return usage_;
}
const std::vector<url::Origin>& GetOriginsForType(
scoped_refptr<storage::QuotaClient> client,
storage::QuotaClient& client,
StorageType type) {
origins_.clear();
AsyncGetOriginsForType(std::move(client), type);
AsyncGetOriginsForType(client, type);
base::RunLoop().RunUntilIdle();
return origins_;
}
const std::vector<url::Origin>& GetOriginsForHost(
scoped_refptr<storage::QuotaClient> client,
storage::QuotaClient& client,
StorageType type,
const std::string& host) {
origins_.clear();
AsyncGetOriginsForHost(std::move(client), type, host);
AsyncGetOriginsForHost(client, type, host);
base::RunLoop().RunUntilIdle();
return origins_;
}
blink::mojom::QuotaStatusCode DeleteOriginData(
scoped_refptr<storage::QuotaClient> client,
blink::mojom::QuotaStatusCode DeleteOriginData(storage::QuotaClient& client,
StorageType type,
const url::Origin& origin) {
delete_status_ = blink::mojom::QuotaStatusCode::kUnknown;
AsyncDeleteOriginData(std::move(client), type, origin);
AsyncDeleteOriginData(client, type, origin);
base::RunLoop().RunUntilIdle();
return delete_status_;
}
void AsyncGetOriginUsage(scoped_refptr<storage::QuotaClient> client,
void AsyncGetOriginUsage(storage::QuotaClient& client,
const url::Origin& origin,
StorageType type) {
CHECK(client);
client->GetOriginUsage(
// Unretained usage is safe because this test owns a TaskEnvironment. No
// tasks will be executed after the test completes.
client.GetOriginUsage(
origin, type,
base::BindOnce(&AppCacheQuotaClientTest::OnGetOriginUsageComplete,
weak_factory_.GetWeakPtr()));
base::Unretained(this)));
}
void AsyncGetOriginsForType(scoped_refptr<storage::QuotaClient> client,
StorageType type) {
CHECK(client);
client->GetOriginsForType(
void AsyncGetOriginsForType(storage::QuotaClient& client, StorageType type) {
// Unretained usage is safe because this test owns a TaskEnvironment. No
// tasks will be executed after the test completes.
client.GetOriginsForType(
type, base::BindOnce(&AppCacheQuotaClientTest::OnGetOriginsComplete,
weak_factory_.GetWeakPtr()));
base::Unretained(this)));
}
void AsyncGetOriginsForHost(scoped_refptr<storage::QuotaClient> client,
void AsyncGetOriginsForHost(storage::QuotaClient& client,
StorageType type,
const std::string& host) {
CHECK(client);
client->GetOriginsForHost(
// Unretained usage is safe because this test owns a TaskEnvironment. No
// tasks will be executed after the test completes.
client.GetOriginsForHost(
type, host,
base::BindOnce(&AppCacheQuotaClientTest::OnGetOriginsComplete,
weak_factory_.GetWeakPtr()));
base::Unretained(this)));
}
void AsyncDeleteOriginData(scoped_refptr<storage::QuotaClient> client,
void AsyncDeleteOriginData(storage::QuotaClient& client,
StorageType type,
const url::Origin& origin) {
CHECK(client);
client->DeleteOriginData(
// Unretained usage is safe because this test owns a TaskEnvironment. No
// tasks will be executed after the test completes.
client.DeleteOriginData(
origin, type,
base::BindOnce(&AppCacheQuotaClientTest::OnDeleteOriginDataComplete,
weak_factory_.GetWeakPtr()));
base::Unretained(this)));
}
void SetUsageMapEntry(const url::Origin& origin, int64_t usage) {
......@@ -125,19 +126,16 @@ class AppCacheQuotaClientTest : public testing::Test {
return base::MakeRefCounted<AppCacheQuotaClient>(mock_service_.AsWeakPtr());
}
void Call_NotifyAppCacheReady(scoped_refptr<AppCacheQuotaClient> client) {
if (client)
client->NotifyAppCacheReady();
void Call_NotifyAppCacheReady(AppCacheQuotaClient& client) {
client.NotifyAppCacheReady();
}
void Call_NotifyAppCacheDestroyed(scoped_refptr<AppCacheQuotaClient> client) {
if (client)
client->NotifyAppCacheDestroyed();
void Call_NotifyAppCacheDestroyed(AppCacheQuotaClient& client) {
client.NotifyAppCacheDestroyed();
}
void Call_OnQuotaManagerDestroyed(scoped_refptr<AppCacheQuotaClient> client) {
if (client)
client->OnQuotaManagerDestroyed();
void Call_OnQuotaManagerDestroyed(AppCacheQuotaClient& client) {
client.OnQuotaManagerDestroyed();
}
protected:
......@@ -165,132 +163,131 @@ class AppCacheQuotaClientTest : public testing::Test {
int num_get_origins_completions_ = 0;
int num_delete_origins_completions_ = 0;
MockAppCacheService mock_service_;
base::WeakPtrFactory<AppCacheQuotaClientTest> weak_factory_{this};
};
TEST_F(AppCacheQuotaClientTest, BasicCreateDestroy) {
auto client = CreateClient();
Call_NotifyAppCacheReady(client);
Call_OnQuotaManagerDestroyed(client);
Call_NotifyAppCacheDestroyed(client);
Call_NotifyAppCacheReady(*client);
Call_OnQuotaManagerDestroyed(*client);
Call_NotifyAppCacheDestroyed(*client);
}
TEST_F(AppCacheQuotaClientTest, QuotaManagerDestroyedInCallback) {
auto client = CreateClient();
Call_NotifyAppCacheReady(client);
Call_NotifyAppCacheReady(*client);
client->DeleteOriginData(kOriginA, kTemp,
base::BindOnce(
[](AppCacheQuotaClientTest* test,
scoped_refptr<AppCacheQuotaClient> client,
blink::mojom::QuotaStatusCode) {
test->Call_OnQuotaManagerDestroyed(client);
test->Call_OnQuotaManagerDestroyed(*client);
},
this, client));
Call_NotifyAppCacheDestroyed(client);
Call_NotifyAppCacheDestroyed(*client);
}
TEST_F(AppCacheQuotaClientTest, EmptyService) {
auto client = CreateClient();
Call_NotifyAppCacheReady(client);
Call_NotifyAppCacheReady(*client);
EXPECT_EQ(0, GetOriginUsage(client, kOriginA, kTemp));
EXPECT_TRUE(GetOriginsForType(client, kTemp).empty());
EXPECT_TRUE(GetOriginsForHost(client, kTemp, kOriginA.host()).empty());
EXPECT_EQ(0, GetOriginUsage(*client, kOriginA, kTemp));
EXPECT_TRUE(GetOriginsForType(*client, kTemp).empty());
EXPECT_TRUE(GetOriginsForHost(*client, kTemp, kOriginA.host()).empty());
EXPECT_EQ(blink::mojom::QuotaStatusCode::kOk,
DeleteOriginData(client, kTemp, kOriginA));
DeleteOriginData(*client, kTemp, kOriginA));
Call_NotifyAppCacheDestroyed(client);
Call_OnQuotaManagerDestroyed(client);
Call_NotifyAppCacheDestroyed(*client);
Call_OnQuotaManagerDestroyed(*client);
}
TEST_F(AppCacheQuotaClientTest, NoService) {
auto client = CreateClient();
Call_NotifyAppCacheReady(client);
Call_NotifyAppCacheDestroyed(client);
Call_NotifyAppCacheReady(*client);
Call_NotifyAppCacheDestroyed(*client);
EXPECT_EQ(0, GetOriginUsage(client, kOriginA, kTemp));
EXPECT_TRUE(GetOriginsForType(client, kTemp).empty());
EXPECT_TRUE(GetOriginsForHost(client, kTemp, kOriginA.host()).empty());
EXPECT_EQ(0, GetOriginUsage(*client, kOriginA, kTemp));
EXPECT_TRUE(GetOriginsForType(*client, kTemp).empty());
EXPECT_TRUE(GetOriginsForHost(*client, kTemp, kOriginA.host()).empty());
EXPECT_EQ(blink::mojom::QuotaStatusCode::kErrorAbort,
DeleteOriginData(client, kTemp, kOriginA));
DeleteOriginData(*client, kTemp, kOriginA));
Call_OnQuotaManagerDestroyed(client);
Call_OnQuotaManagerDestroyed(*client);
}
TEST_F(AppCacheQuotaClientTest, GetOriginUsage) {
auto client = CreateClient();
Call_NotifyAppCacheReady(client);
Call_NotifyAppCacheReady(*client);
SetUsageMapEntry(kOriginA, 1000);
EXPECT_EQ(1000, GetOriginUsage(client, kOriginA, kTemp));
EXPECT_EQ(0, GetOriginUsage(client, kOriginB, kTemp));
EXPECT_EQ(1000, GetOriginUsage(*client, kOriginA, kTemp));
EXPECT_EQ(0, GetOriginUsage(*client, kOriginB, kTemp));
Call_NotifyAppCacheDestroyed(client);
Call_OnQuotaManagerDestroyed(client);
Call_NotifyAppCacheDestroyed(*client);
Call_OnQuotaManagerDestroyed(*client);
}
TEST_F(AppCacheQuotaClientTest, GetOriginsForHost) {
auto client = CreateClient();
Call_NotifyAppCacheReady(client);
Call_NotifyAppCacheReady(*client);
EXPECT_EQ(kOriginA.host(), kOriginB.host());
EXPECT_NE(kOriginA.host(), kOriginOther.host());
std::vector<url::Origin> origins =
GetOriginsForHost(client, kTemp, kOriginA.host());
GetOriginsForHost(*client, kTemp, kOriginA.host());
EXPECT_TRUE(origins.empty());
SetUsageMapEntry(kOriginA, 1000);
SetUsageMapEntry(kOriginB, 10);
SetUsageMapEntry(kOriginOther, 500);
origins = GetOriginsForHost(client, kTemp, kOriginA.host());
origins = GetOriginsForHost(*client, kTemp, kOriginA.host());
EXPECT_EQ(2ul, origins.size());
EXPECT_THAT(origins, testing::Contains(kOriginA));
EXPECT_THAT(origins, testing::Contains(kOriginB));
origins = GetOriginsForHost(client, kTemp, kOriginOther.host());
origins = GetOriginsForHost(*client, kTemp, kOriginOther.host());
EXPECT_EQ(1ul, origins.size());
EXPECT_THAT(origins, testing::Contains(kOriginOther));
Call_NotifyAppCacheDestroyed(client);
Call_OnQuotaManagerDestroyed(client);
Call_NotifyAppCacheDestroyed(*client);
Call_OnQuotaManagerDestroyed(*client);
}
TEST_F(AppCacheQuotaClientTest, GetOriginsForType) {
auto client = CreateClient();
Call_NotifyAppCacheReady(client);
Call_NotifyAppCacheReady(*client);
EXPECT_TRUE(GetOriginsForType(client, kTemp).empty());
EXPECT_TRUE(GetOriginsForType(*client, kTemp).empty());
SetUsageMapEntry(kOriginA, 1000);
SetUsageMapEntry(kOriginB, 10);
std::vector<url::Origin> origins = GetOriginsForType(client, kTemp);
std::vector<url::Origin> origins = GetOriginsForType(*client, kTemp);
EXPECT_EQ(2ul, origins.size());
EXPECT_THAT(origins, testing::Contains(kOriginA));
EXPECT_THAT(origins, testing::Contains(kOriginB));
Call_NotifyAppCacheDestroyed(client);
Call_OnQuotaManagerDestroyed(client);
Call_NotifyAppCacheDestroyed(*client);
Call_OnQuotaManagerDestroyed(*client);
}
TEST_F(AppCacheQuotaClientTest, DeleteOriginData) {
auto client = CreateClient();
Call_NotifyAppCacheReady(client);
Call_NotifyAppCacheReady(*client);
EXPECT_EQ(blink::mojom::QuotaStatusCode::kOk,
DeleteOriginData(client, kTemp, kOriginA));
DeleteOriginData(*client, kTemp, kOriginA));
EXPECT_EQ(1, mock_service_.delete_called_count());
mock_service_.set_mock_delete_appcaches_for_origin_result(
net::ERR_ABORTED);
EXPECT_EQ(blink::mojom::QuotaStatusCode::kErrorAbort,
DeleteOriginData(client, kTemp, kOriginA));
DeleteOriginData(*client, kTemp, kOriginA));
EXPECT_EQ(2, mock_service_.delete_called_count());
Call_OnQuotaManagerDestroyed(client);
Call_NotifyAppCacheDestroyed(client);
Call_OnQuotaManagerDestroyed(*client);
Call_NotifyAppCacheDestroyed(*client);
}
TEST_F(AppCacheQuotaClientTest, PendingRequests) {
......@@ -301,14 +298,14 @@ TEST_F(AppCacheQuotaClientTest, PendingRequests) {
SetUsageMapEntry(kOriginOther, 500);
// Queue up some requests.
AsyncGetOriginUsage(client, kOriginA, kTemp);
AsyncGetOriginUsage(client, kOriginB, kTemp);
AsyncGetOriginsForType(client, kTemp);
AsyncGetOriginsForType(client, kTemp);
AsyncGetOriginsForHost(client, kTemp, kOriginA.host());
AsyncGetOriginsForHost(client, kTemp, kOriginOther.host());
AsyncDeleteOriginData(client, kTemp, kOriginA);
AsyncDeleteOriginData(client, kTemp, kOriginB);
AsyncGetOriginUsage(*client, kOriginA, kTemp);
AsyncGetOriginUsage(*client, kOriginB, kTemp);
AsyncGetOriginsForType(*client, kTemp);
AsyncGetOriginsForType(*client, kTemp);
AsyncGetOriginsForHost(*client, kTemp, kOriginA.host());
AsyncGetOriginsForHost(*client, kTemp, kOriginOther.host());
AsyncDeleteOriginData(*client, kTemp, kOriginA);
AsyncDeleteOriginData(*client, kTemp, kOriginB);
EXPECT_EQ(0, num_get_origin_usage_completions_);
EXPECT_EQ(0, num_get_origins_completions_);
......@@ -319,7 +316,7 @@ TEST_F(AppCacheQuotaClientTest, PendingRequests) {
EXPECT_EQ(0, num_delete_origins_completions_);
// Pending requests should get serviced when the appcache is ready.
Call_NotifyAppCacheReady(client);
Call_NotifyAppCacheReady(*client);
base::RunLoop().RunUntilIdle();
EXPECT_EQ(2, num_get_origin_usage_completions_);
EXPECT_EQ(4, num_get_origins_completions_);
......@@ -330,8 +327,8 @@ TEST_F(AppCacheQuotaClientTest, PendingRequests) {
EXPECT_EQ(1ul, origins_.size());
EXPECT_THAT(origins_, testing::Contains(kOriginOther));
Call_NotifyAppCacheDestroyed(client);
Call_OnQuotaManagerDestroyed(client);
Call_NotifyAppCacheDestroyed(*client);
Call_OnQuotaManagerDestroyed(*client);
}
TEST_F(AppCacheQuotaClientTest, DestroyServiceWithPending) {
......@@ -342,21 +339,21 @@ TEST_F(AppCacheQuotaClientTest, DestroyServiceWithPending) {
SetUsageMapEntry(kOriginOther, 500);
// Queue up some requests prior to being ready.
AsyncGetOriginUsage(client, kOriginA, kTemp);
AsyncGetOriginUsage(client, kOriginB, kTemp);
AsyncGetOriginsForType(client, kTemp);
AsyncGetOriginsForType(client, kTemp);
AsyncGetOriginsForHost(client, kTemp, kOriginA.host());
AsyncGetOriginsForHost(client, kTemp, kOriginOther.host());
AsyncDeleteOriginData(client, kTemp, kOriginA);
AsyncDeleteOriginData(client, kTemp, kOriginB);
AsyncGetOriginUsage(*client, kOriginA, kTemp);
AsyncGetOriginUsage(*client, kOriginB, kTemp);
AsyncGetOriginsForType(*client, kTemp);
AsyncGetOriginsForType(*client, kTemp);
AsyncGetOriginsForHost(*client, kTemp, kOriginA.host());
AsyncGetOriginsForHost(*client, kTemp, kOriginOther.host());
AsyncDeleteOriginData(*client, kTemp, kOriginA);
AsyncDeleteOriginData(*client, kTemp, kOriginB);
base::RunLoop().RunUntilIdle();
EXPECT_EQ(0, num_get_origin_usage_completions_);
EXPECT_EQ(0, num_get_origins_completions_);
EXPECT_EQ(0, num_delete_origins_completions_);
// Kill the service.
Call_NotifyAppCacheDestroyed(client);
Call_NotifyAppCacheDestroyed(*client);
// All should have been aborted and called completion.
EXPECT_EQ(2, num_get_origin_usage_completions_);
......@@ -366,7 +363,7 @@ TEST_F(AppCacheQuotaClientTest, DestroyServiceWithPending) {
EXPECT_TRUE(origins_.empty());
EXPECT_EQ(blink::mojom::QuotaStatusCode::kErrorAbort, delete_status_);
Call_OnQuotaManagerDestroyed(client);
Call_OnQuotaManagerDestroyed(*client);
}
TEST_F(AppCacheQuotaClientTest, DestroyQuotaManagerWithPending) {
......@@ -377,22 +374,22 @@ TEST_F(AppCacheQuotaClientTest, DestroyQuotaManagerWithPending) {
SetUsageMapEntry(kOriginOther, 500);
// Queue up some requests prior to being ready.
AsyncGetOriginUsage(client, kOriginA, kTemp);
AsyncGetOriginUsage(client, kOriginB, kTemp);
AsyncGetOriginsForType(client, kTemp);
AsyncGetOriginsForType(client, kTemp);
AsyncGetOriginsForHost(client, kTemp, kOriginA.host());
AsyncGetOriginsForHost(client, kTemp, kOriginOther.host());
AsyncDeleteOriginData(client, kTemp, kOriginA);
AsyncDeleteOriginData(client, kTemp, kOriginB);
AsyncGetOriginUsage(*client, kOriginA, kTemp);
AsyncGetOriginUsage(*client, kOriginB, kTemp);
AsyncGetOriginsForType(*client, kTemp);
AsyncGetOriginsForType(*client, kTemp);
AsyncGetOriginsForHost(*client, kTemp, kOriginA.host());
AsyncGetOriginsForHost(*client, kTemp, kOriginOther.host());
AsyncDeleteOriginData(*client, kTemp, kOriginA);
AsyncDeleteOriginData(*client, kTemp, kOriginB);
base::RunLoop().RunUntilIdle();
EXPECT_EQ(0, num_get_origin_usage_completions_);
EXPECT_EQ(0, num_get_origins_completions_);
EXPECT_EQ(0, num_delete_origins_completions_);
// Kill the quota manager.
Call_OnQuotaManagerDestroyed(client);
Call_NotifyAppCacheReady(client);
Call_OnQuotaManagerDestroyed(*client);
Call_NotifyAppCacheReady(*client);
// Callbacks should be deleted and not called.
base::RunLoop().RunUntilIdle();
......@@ -400,19 +397,19 @@ TEST_F(AppCacheQuotaClientTest, DestroyQuotaManagerWithPending) {
EXPECT_EQ(0, num_get_origins_completions_);
EXPECT_EQ(0, num_delete_origins_completions_);
Call_NotifyAppCacheDestroyed(client);
Call_NotifyAppCacheDestroyed(*client);
}
TEST_F(AppCacheQuotaClientTest, DestroyWithDeleteInProgress) {
auto client = CreateClient();
Call_NotifyAppCacheReady(client);
Call_NotifyAppCacheReady(*client);
// Start an async delete.
AsyncDeleteOriginData(client, kTemp, kOriginB);
AsyncDeleteOriginData(*client, kTemp, kOriginB);
EXPECT_EQ(0, num_delete_origins_completions_);
// Kill the service.
Call_NotifyAppCacheDestroyed(client);
Call_NotifyAppCacheDestroyed(*client);
// Should have been aborted.
EXPECT_EQ(1, num_delete_origins_completions_);
......@@ -424,7 +421,7 @@ TEST_F(AppCacheQuotaClientTest, DestroyWithDeleteInProgress) {
EXPECT_EQ(1, num_delete_origins_completions_);
EXPECT_EQ(blink::mojom::QuotaStatusCode::kErrorAbort, delete_status_);
Call_OnQuotaManagerDestroyed(client);
Call_OnQuotaManagerDestroyed(*client);
}
} // namespace content
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment