8259214: MetaspaceClosure support for Arrays of MetaspaceObj
Reviewed-by: fparain, ccheung
This commit is contained in:
parent
bdc305e1cb
commit
aa57d07ce8
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2017, 2020, Oracle and/or its affiliates. All rights reserved.
|
* Copyright (c) 2017, 2021, Oracle and/or its affiliates. All rights reserved.
|
||||||
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||||
*
|
*
|
||||||
* This code is free software; you can redistribute it and/or modify it
|
* This code is free software; you can redistribute it and/or modify it
|
||||||
@ -27,10 +27,13 @@
|
|||||||
|
|
||||||
#include "logging/log.hpp"
|
#include "logging/log.hpp"
|
||||||
#include "memory/allocation.hpp"
|
#include "memory/allocation.hpp"
|
||||||
|
#include "metaprogramming/enableIf.hpp"
|
||||||
#include "oops/array.hpp"
|
#include "oops/array.hpp"
|
||||||
#include "utilities/globalDefinitions.hpp"
|
#include "utilities/globalDefinitions.hpp"
|
||||||
#include "utilities/growableArray.hpp"
|
#include "utilities/growableArray.hpp"
|
||||||
#include "utilities/hashtable.inline.hpp"
|
#include "utilities/hashtable.inline.hpp"
|
||||||
|
#include "utilities/macros.hpp"
|
||||||
|
#include <type_traits>
|
||||||
|
|
||||||
// The metadata hierarchy is separate from the oop hierarchy
|
// The metadata hierarchy is separate from the oop hierarchy
|
||||||
class MetaspaceObj; // no C++ vtable
|
class MetaspaceObj; // no C++ vtable
|
||||||
@ -92,8 +95,8 @@ public:
|
|||||||
// However, to save space, MetaspaceObj has NO vtable. The vtable is introduced
|
// However, to save space, MetaspaceObj has NO vtable. The vtable is introduced
|
||||||
// only in the Metadata class.
|
// only in the Metadata class.
|
||||||
//
|
//
|
||||||
// To work around the lack of a vtable, we use Ref class with templates
|
// To work around the lack of a vtable, we use the Ref class with templates
|
||||||
// (see ObjectRef, PrimitiveArrayRef and PointerArrayRef)
|
// (see MSORef, OtherArrayRef, MSOArrayRef, and MSOPointerArrayRef)
|
||||||
// so that we can statically discover the type of a object. The use of Ref
|
// so that we can statically discover the type of a object. The use of Ref
|
||||||
// depends on the fact that:
|
// depends on the fact that:
|
||||||
//
|
//
|
||||||
@ -155,8 +158,8 @@ public:
|
|||||||
};
|
};
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// -------------------------------------------------- ObjectRef
|
// MSORef -- iterate an instance of MetaspaceObj
|
||||||
template <class T> class ObjectRef : public Ref {
|
template <class T> class MSORef : public Ref {
|
||||||
T** _mpp;
|
T** _mpp;
|
||||||
T* dereference() const {
|
T* dereference() const {
|
||||||
return *_mpp;
|
return *_mpp;
|
||||||
@ -167,7 +170,7 @@ private:
|
|||||||
}
|
}
|
||||||
|
|
||||||
public:
|
public:
|
||||||
ObjectRef(T** mpp, Writability w) : Ref(w), _mpp(mpp) {}
|
MSORef(T** mpp, Writability w) : Ref(w), _mpp(mpp) {}
|
||||||
|
|
||||||
virtual bool is_read_only_by_default() const { return T::is_read_only_by_default(); }
|
virtual bool is_read_only_by_default() const { return T::is_read_only_by_default(); }
|
||||||
virtual bool not_null() const { return dereference() != NULL; }
|
virtual bool not_null() const { return dereference() != NULL; }
|
||||||
@ -182,65 +185,80 @@ private:
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// -------------------------------------------------- PrimitiveArrayRef
|
// abstract base class for MSOArrayRef, MSOPointerArrayRef and OtherArrayRef
|
||||||
template <class T> class PrimitiveArrayRef : public Ref {
|
template <class T> class ArrayRef : public Ref {
|
||||||
Array<T>** _mpp;
|
Array<T>** _mpp;
|
||||||
|
protected:
|
||||||
Array<T>* dereference() const {
|
Array<T>* dereference() const {
|
||||||
return *_mpp;
|
return *_mpp;
|
||||||
}
|
}
|
||||||
protected:
|
|
||||||
virtual void** mpp() const {
|
virtual void** mpp() const {
|
||||||
return (void**)_mpp;
|
return (void**)_mpp;
|
||||||
}
|
}
|
||||||
|
|
||||||
public:
|
ArrayRef(Array<T>** mpp, Writability w) : Ref(w), _mpp(mpp) {}
|
||||||
PrimitiveArrayRef(Array<T>** mpp, Writability w) : Ref(w), _mpp(mpp) {}
|
|
||||||
|
|
||||||
// all Arrays are read-only by default
|
// all Arrays are read-only by default
|
||||||
virtual bool is_read_only_by_default() const { return true; }
|
virtual bool is_read_only_by_default() const { return true; }
|
||||||
virtual bool not_null() const { return dereference() != NULL; }
|
virtual bool not_null() const { return dereference() != NULL; }
|
||||||
virtual int size() const { return dereference()->size(); }
|
virtual int size() const { return dereference()->size(); }
|
||||||
virtual MetaspaceObj::Type msotype() const { return MetaspaceObj::array_type(sizeof(T)); }
|
virtual MetaspaceObj::Type msotype() const { return MetaspaceObj::array_type(sizeof(T)); }
|
||||||
|
};
|
||||||
|
|
||||||
|
// OtherArrayRef -- iterate an instance of Array<T>, where T is NOT a subtype of MetaspaceObj.
|
||||||
|
// T can be a primitive type, such as int, or a structure. However, we do not scan
|
||||||
|
// the fields inside T, so you should not embed any pointers inside T.
|
||||||
|
template <class T> class OtherArrayRef : public ArrayRef<T> {
|
||||||
|
public:
|
||||||
|
OtherArrayRef(Array<T>** mpp, Writability w) : ArrayRef<T>(mpp, w) {}
|
||||||
|
|
||||||
virtual void metaspace_pointers_do(MetaspaceClosure *it) const {
|
virtual void metaspace_pointers_do(MetaspaceClosure *it) const {
|
||||||
Array<T>* array = dereference();
|
Array<T>* array = ArrayRef<T>::dereference();
|
||||||
log_trace(cds)("Iter(PrimitiveArray): %p [%d]", array, array->length());
|
log_trace(cds)("Iter(OtherArray): %p [%d]", array, array->length());
|
||||||
}
|
}
|
||||||
virtual void metaspace_pointers_do_at(MetaspaceClosure *it, address new_loc) const {
|
virtual void metaspace_pointers_do_at(MetaspaceClosure *it, address new_loc) const {
|
||||||
Array<T>* array = (Array<T>*)new_loc;
|
Array<T>* array = (Array<T>*)new_loc;
|
||||||
log_trace(cds)("Iter(PrimitiveArray): %p [%d]", array, array->length());
|
log_trace(cds)("Iter(OtherArray): %p [%d]", array, array->length());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// -------------------------------------------------- PointerArrayRef
|
// MSOArrayRef -- iterate an instance of Array<T>, where T is a subtype of MetaspaceObj.
|
||||||
template <class T> class PointerArrayRef : public Ref {
|
// We recursively call T::metaspace_pointers_do() for each element in this array.
|
||||||
Array<T*>** _mpp;
|
template <class T> class MSOArrayRef : public ArrayRef<T> {
|
||||||
Array<T*>* dereference() const {
|
|
||||||
return *_mpp;
|
|
||||||
}
|
|
||||||
protected:
|
|
||||||
virtual void** mpp() const {
|
|
||||||
return (void**)_mpp;
|
|
||||||
}
|
|
||||||
|
|
||||||
public:
|
public:
|
||||||
PointerArrayRef(Array<T*>** mpp, Writability w) : Ref(w), _mpp(mpp) {}
|
MSOArrayRef(Array<T>** mpp, Writability w) : ArrayRef<T>(mpp, w) {}
|
||||||
|
|
||||||
// all Arrays are read-only by default
|
|
||||||
virtual bool is_read_only_by_default() const { return true; }
|
|
||||||
virtual bool not_null() const { return dereference() != NULL; }
|
|
||||||
virtual int size() const { return dereference()->size(); }
|
|
||||||
virtual MetaspaceObj::Type msotype() const { return MetaspaceObj::array_type(sizeof(T*)); }
|
|
||||||
|
|
||||||
virtual void metaspace_pointers_do(MetaspaceClosure *it) const {
|
virtual void metaspace_pointers_do(MetaspaceClosure *it) const {
|
||||||
metaspace_pointers_do_at_impl(it, dereference());
|
metaspace_pointers_do_at_impl(it, ArrayRef<T>::dereference());
|
||||||
|
}
|
||||||
|
virtual void metaspace_pointers_do_at(MetaspaceClosure *it, address new_loc) const {
|
||||||
|
metaspace_pointers_do_at_impl(it, (Array<T>*)new_loc);
|
||||||
|
}
|
||||||
|
private:
|
||||||
|
void metaspace_pointers_do_at_impl(MetaspaceClosure *it, Array<T>* array) const {
|
||||||
|
log_trace(cds)("Iter(MSOArray): %p [%d]", array, array->length());
|
||||||
|
for (int i = 0; i < array->length(); i++) {
|
||||||
|
T* elm = array->adr_at(i);
|
||||||
|
elm->metaspace_pointers_do(it);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// MSOPointerArrayRef -- iterate an instance of Array<T*>, where T is a subtype of MetaspaceObj.
|
||||||
|
// We recursively call MetaspaceClosure::push() for each pointer in this array.
|
||||||
|
template <class T> class MSOPointerArrayRef : public ArrayRef<T*> {
|
||||||
|
public:
|
||||||
|
MSOPointerArrayRef(Array<T*>** mpp, Writability w) : ArrayRef<T*>(mpp, w) {}
|
||||||
|
|
||||||
|
virtual void metaspace_pointers_do(MetaspaceClosure *it) const {
|
||||||
|
metaspace_pointers_do_at_impl(it, ArrayRef<T*>::dereference());
|
||||||
}
|
}
|
||||||
virtual void metaspace_pointers_do_at(MetaspaceClosure *it, address new_loc) const {
|
virtual void metaspace_pointers_do_at(MetaspaceClosure *it, address new_loc) const {
|
||||||
metaspace_pointers_do_at_impl(it, (Array<T*>*)new_loc);
|
metaspace_pointers_do_at_impl(it, (Array<T*>*)new_loc);
|
||||||
}
|
}
|
||||||
private:
|
private:
|
||||||
void metaspace_pointers_do_at_impl(MetaspaceClosure *it, Array<T*>* array) const {
|
void metaspace_pointers_do_at_impl(MetaspaceClosure *it, Array<T*>* array) const {
|
||||||
log_trace(cds)("Iter(ObjectArray): %p [%d]", array, array->length());
|
log_trace(cds)("Iter(MSOPointerArray): %p [%d]", array, array->length());
|
||||||
for (int i = 0; i < array->length(); i++) {
|
for (int i = 0; i < array->length(); i++) {
|
||||||
T** mpp = array->adr_at(i);
|
T** mpp = array->adr_at(i);
|
||||||
it->push(mpp);
|
it->push(mpp);
|
||||||
@ -288,31 +306,69 @@ public:
|
|||||||
// returns true if we want to keep iterating the pointers embedded inside <ref>
|
// returns true if we want to keep iterating the pointers embedded inside <ref>
|
||||||
virtual bool do_ref(Ref* ref, bool read_only) = 0;
|
virtual bool do_ref(Ref* ref, bool read_only) = 0;
|
||||||
|
|
||||||
// When you do:
|
private:
|
||||||
// void MyType::metaspace_pointers_do(MetaspaceClosure* it) {
|
template <class REF_TYPE, typename T>
|
||||||
// it->push(_my_field)
|
void push_with_ref(T** mpp, Writability w) {
|
||||||
// }
|
push_impl(new REF_TYPE(mpp, w));
|
||||||
|
}
|
||||||
|
|
||||||
|
public:
|
||||||
|
// When MetaspaceClosure::push(...) is called, pick the correct Ref subtype to handle it:
|
||||||
//
|
//
|
||||||
// C++ will try to match the "most specific" template function. This one will
|
// MetaspaceClosure* it = ...;
|
||||||
// will be matched if possible (if mpp is an Array<> of any pointer type).
|
// Klass* o = ...; it->push(&o); => MSORef
|
||||||
template <typename T> void push(Array<T*>** mpp, Writability w = _default) {
|
// Array<int>* a1 = ...; it->push(&a1); => OtherArrayRef
|
||||||
push_impl(new PointerArrayRef<T>(mpp, w));
|
// Array<Annotation>* a2 = ...; it->push(&a2); => MSOArrayRef
|
||||||
|
// Array<Klass*>* a3 = ...; it->push(&a3); => MSOPointerArrayRef
|
||||||
|
// Array<Array<Klass*>*>* a4 = ...; it->push(&a4); => MSOPointerArrayRef
|
||||||
|
// Array<Annotation*>* a5 = ...; it->push(&a5); => MSOPointerArrayRef
|
||||||
|
//
|
||||||
|
// Note that the following will fail to compile (to prevent you from adding new fields
|
||||||
|
// into the MetaspaceObj subtypes that cannot be properly copied by CDS):
|
||||||
|
//
|
||||||
|
// Hashtable* h = ...; it->push(&h); => Hashtable is not a subclass of MetaspaceObj
|
||||||
|
// Array<Hashtable*>* a6 = ...; it->push(&a6); => Hashtable is not a subclass of MetaspaceObj
|
||||||
|
// Array<int*>* a7 = ...; it->push(&a7); => int is not a subclass of MetaspaceObj
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
void push(T** mpp, Writability w = _default) {
|
||||||
|
static_assert(std::is_base_of<MetaspaceObj, T>::value, "Do not push pointers of arbitrary types");
|
||||||
|
push_with_ref<MSORef<T>>(mpp, w);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the above function doesn't match (mpp is an Array<>, but T is not a pointer type), then
|
template <typename T, ENABLE_IF(!std::is_base_of<MetaspaceObj, T>::value)>
|
||||||
// this is the second choice.
|
void push(Array<T>** mpp, Writability w = _default) {
|
||||||
template <typename T> void push(Array<T>** mpp, Writability w = _default) {
|
push_with_ref<OtherArrayRef<T>>(mpp, w);
|
||||||
push_impl(new PrimitiveArrayRef<T>(mpp, w));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the above function doesn't match (mpp is not an Array<> type), then
|
template <typename T, ENABLE_IF(std::is_base_of<MetaspaceObj, T>::value)>
|
||||||
// this will be matched by default.
|
void push(Array<T>** mpp, Writability w = _default) {
|
||||||
template <class T> void push(T** mpp, Writability w = _default) {
|
push_with_ref<MSOArrayRef<T>>(mpp, w);
|
||||||
push_impl(new ObjectRef<T>(mpp, w));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
void push(Array<T*>** mpp, Writability w = _default) {
|
||||||
|
static_assert(std::is_base_of<MetaspaceObj, T>::value, "Do not push Arrays of arbitrary pointer types");
|
||||||
|
push_with_ref<MSOPointerArrayRef<T>>(mpp, w);
|
||||||
|
}
|
||||||
|
|
||||||
|
#if 0
|
||||||
|
// Enable this block if you're changing the push(...) methods, to test for types that should be
|
||||||
|
// disallowed. Each of the following "push" calls should result in a compile-time error.
|
||||||
|
void test_disallowed_types(MetaspaceClosure* it) {
|
||||||
|
Hashtable<bool, mtInternal>* h = NULL;
|
||||||
|
it->push(&h);
|
||||||
|
|
||||||
|
Array<Hashtable<bool, mtInternal>*>* a6 = NULL;
|
||||||
|
it->push(&a6);
|
||||||
|
|
||||||
|
Array<int*>* a7 = NULL;
|
||||||
|
it->push(&a7);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
template <class T> void push_method_entry(T** mpp, intptr_t* p) {
|
template <class T> void push_method_entry(T** mpp, intptr_t* p) {
|
||||||
Ref* ref = new ObjectRef<T>(mpp, _default);
|
Ref* ref = new MSORef<T>(mpp, _default);
|
||||||
push_special(_method_entry_ref, ref, (intptr_t*)p);
|
push_special(_method_entry_ref, ref, (intptr_t*)p);
|
||||||
if (!ref->keep_after_pushing()) {
|
if (!ref->keep_after_pushing()) {
|
||||||
delete ref;
|
delete ref;
|
||||||
|
138
test/hotspot/gtest/utilities/test_metaspaceClosure.cpp
Normal file
138
test/hotspot/gtest/utilities/test_metaspaceClosure.cpp
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2021, Oracle and/or its affiliates. All rights reserved.
|
||||||
|
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||||
|
*
|
||||||
|
* This code is free software; you can redistribute it and/or modify it
|
||||||
|
* under the terms of the GNU General Public License version 2 only, as
|
||||||
|
* published by the Free Software Foundation.
|
||||||
|
*
|
||||||
|
* This code is distributed in the hope that it will be useful, but WITHOUT
|
||||||
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
||||||
|
* version 2 for more details (a copy is included in the LICENSE file that
|
||||||
|
* accompanied this code).
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License version
|
||||||
|
* 2 along with this work; if not, write to the Free Software Foundation,
|
||||||
|
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||||
|
*
|
||||||
|
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
|
||||||
|
* or visit www.oracle.com if you need additional information or have any
|
||||||
|
* questions.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "precompiled.hpp"
|
||||||
|
#include "memory/allocation.hpp"
|
||||||
|
#include "memory/metadataFactory.hpp"
|
||||||
|
#include "memory/metaspaceClosure.hpp"
|
||||||
|
#include "oops/array.hpp"
|
||||||
|
#include "oops/metadata.hpp"
|
||||||
|
#include "runtime/thread.hpp"
|
||||||
|
#include "unittest.hpp"
|
||||||
|
|
||||||
|
class MyMetaData : public MetaspaceObj {
|
||||||
|
public:
|
||||||
|
MyMetaData* _a;
|
||||||
|
MyMetaData* _b;
|
||||||
|
|
||||||
|
MyMetaData() : _a(NULL), _b(NULL) {}
|
||||||
|
|
||||||
|
MetaspaceObj::Type type() const {
|
||||||
|
return MetaspaceObj::SymbolType; // Just lie. It doesn't matter in this test
|
||||||
|
}
|
||||||
|
const char* internal_name() const {
|
||||||
|
return "MyMetaData";
|
||||||
|
}
|
||||||
|
int size() const {
|
||||||
|
return align_up((int)sizeof(MyMetaData), wordSize) / wordSize;
|
||||||
|
};
|
||||||
|
|
||||||
|
static bool is_read_only_by_default() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void metaspace_pointers_do(MetaspaceClosure* it) {
|
||||||
|
it->push(&_a);
|
||||||
|
it->push(&_b);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class MyUniqueMetaspaceClosure : public MetaspaceClosure {
|
||||||
|
static constexpr int SIZE = 10;
|
||||||
|
MyMetaData* _visited[SIZE];
|
||||||
|
int _count;
|
||||||
|
public:
|
||||||
|
MyUniqueMetaspaceClosure() {
|
||||||
|
for (int i = 0; i < SIZE; i++) {
|
||||||
|
_visited[i] = NULL;
|
||||||
|
}
|
||||||
|
_count = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual bool do_ref(Ref* ref, bool read_only) {
|
||||||
|
MyMetaData* ptr = (MyMetaData*)ref->obj();
|
||||||
|
assert(_count < SIZE, "out of bounds");
|
||||||
|
_visited[_count++] = ptr;
|
||||||
|
return true; // recurse
|
||||||
|
}
|
||||||
|
|
||||||
|
bool has_visited(MyMetaData* p) {
|
||||||
|
for (int i = 0; i < SIZE; i++) {
|
||||||
|
if (_visited[i] == p) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// iterate an Array<MyMetaData*>
|
||||||
|
TEST_VM(MetaspaceClosure, MSOPointerArrayRef) {
|
||||||
|
Thread* THREAD = Thread::current();
|
||||||
|
ClassLoaderData* cld = ClassLoaderData::the_null_class_loader_data();
|
||||||
|
Array<MyMetaData*>* array = MetadataFactory::new_array<MyMetaData*>(cld, 4, THREAD);
|
||||||
|
for (int i = 0; i < array->length(); i++) {
|
||||||
|
EXPECT_TRUE(array->at(i) == NULL) << "should be initialized to null";
|
||||||
|
}
|
||||||
|
|
||||||
|
MyMetaData x;
|
||||||
|
MyMetaData y;
|
||||||
|
MyMetaData z;
|
||||||
|
|
||||||
|
array->at_put(0, &x);
|
||||||
|
array->at_put(2, &y);
|
||||||
|
y._a = &z;
|
||||||
|
|
||||||
|
MyUniqueMetaspaceClosure closure;
|
||||||
|
closure.push(&array);
|
||||||
|
|
||||||
|
EXPECT_TRUE(closure.has_visited(&x)) << "must be";
|
||||||
|
EXPECT_TRUE(closure.has_visited(&y)) << "must be";
|
||||||
|
EXPECT_TRUE(closure.has_visited(&z)) << "must be";
|
||||||
|
}
|
||||||
|
|
||||||
|
// iterate an Array<MyMetaData>
|
||||||
|
TEST_VM(MetaspaceClosure, MSOArrayRef) {
|
||||||
|
Thread* THREAD = Thread::current();
|
||||||
|
ClassLoaderData* cld = ClassLoaderData::the_null_class_loader_data();
|
||||||
|
Array<MyMetaData>* array = MetadataFactory::new_array<MyMetaData>(cld, 4, THREAD);
|
||||||
|
for (int i = 0; i < array->length(); i++) {
|
||||||
|
EXPECT_TRUE(array->at(i)._a == NULL) << "should be initialized to null";
|
||||||
|
EXPECT_TRUE(array->at(i)._b == NULL) << "should be initialized to null";
|
||||||
|
}
|
||||||
|
|
||||||
|
MyMetaData x;
|
||||||
|
MyMetaData y;
|
||||||
|
MyMetaData z;
|
||||||
|
|
||||||
|
array->adr_at(0)->_a = &x;
|
||||||
|
array->adr_at(2)->_b = &y;
|
||||||
|
y._a = &z;
|
||||||
|
|
||||||
|
MyUniqueMetaspaceClosure closure;
|
||||||
|
closure.push(&array);
|
||||||
|
|
||||||
|
EXPECT_TRUE(closure.has_visited(&x)) << "must be";
|
||||||
|
EXPECT_TRUE(closure.has_visited(&y)) << "must be";
|
||||||
|
EXPECT_TRUE(closure.has_visited(&z)) << "must be";
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user