Bagaimana cara menambahkan cakupan pengujian ke konstruktor pribadi?


110

Ini kodenya:

package com.XXX;
public final class Foo {
  private Foo() {
    // intentionally empty
  }
  public static int bar() {
    return 1;
  }
}

Inilah ujiannya:

package com.XXX;
public FooTest {
  @Test 
  void testValidatesThatBarWorks() {
    int result = Foo.bar();
    assertEquals(1, result);
  }
  @Test(expected = java.lang.IllegalAccessException.class)
  void testValidatesThatClassFooIsNotInstantiable() {
    Class cls = Class.forName("com.XXX.Foo");
    cls.newInstance(); // exception here
  }
}

Berfungsi dengan baik, kelas telah diuji. Tetapi Cobertura mengatakan bahwa tidak ada cakupan kode dari konstruktor privat kelas. Bagaimana kita bisa menambahkan cakupan tes ke konstruktor pribadi seperti itu?


Bagi saya, sepertinya Anda mencoba untuk menerapkan pola Singleton. Jika demikian, Anda mungkin menyukai dp4j.com (yang persis seperti itu)
simpatico

Bukankah seharusnya "sengaja dikosongkan" diganti dengan pengecualian lemparan? Dalam hal ini Anda bisa menulis tes yang mengharapkan pengecualian khusus itu dengan pesan tertentu, bukan? tidak yakin apakah ini berlebihan
Ewoks

Jawaban:


85

Nah, ada cara-cara yang berpotensi Anda gunakan refleksi dll - tetapi apakah itu benar-benar layak? Ini adalah konstruktor yang tidak boleh dipanggil , bukan?

Jika ada anotasi atau sesuatu yang serupa yang dapat Anda tambahkan ke kelas untuk membuat Cobertura mengerti bahwa itu tidak akan dipanggil, lakukan itu: Saya rasa tidak ada gunanya melalui rintangan untuk menambahkan liputan secara artifisial.

EDIT: Jika tidak ada cara untuk melakukannya, jalani saja dengan cakupan yang sedikit berkurang. Ingatlah bahwa pertanggungan dimaksudkan untuk menjadi sesuatu yang berguna bagi Anda - Anda harus bertanggung jawab atas alat tersebut, bukan sebaliknya.


18
Saya tidak ingin "sedikit mengurangi cakupan" di seluruh proyek hanya karena konstruktor khusus ini ..
yegor256

36
@Vincenzo: Maka IMO Anda menempatkan nilai terlalu tinggi pada nomor sederhana. Cakupan adalah indikator pengujian. Jangan menjadi budak alat. Inti dari cakupan adalah memberi Anda tingkat kepercayaan, dan menyarankan area untuk pengujian tambahan. Memanggil konstruktor yang tidak digunakan secara artifisial tidak membantu dengan salah satu poin tersebut.
Jon Skeet

19
@ JonSkeet: Saya sangat setuju dengan "Jangan menjadi budak alat", tetapi tidak harum mengingat setiap "jumlah kekurangan" dalam setiap proyek. Bagaimana cara memastikan hasil 7/9 adalah batasan Cobertura, dan bukan milik programmer? Seorang programmer baru harus memasukkan setiap kegagalan (yang bisa sangat banyak dalam proyek besar) untuk memeriksa kelas demi kelas.
Eduardo Costa

5
Ini tidak menjawab pertanyaan itu. dan omong-omong, beberapa manajer melihat nomor pertanggungan. Mereka tidak peduli kenapa. Mereka tahu bahwa 85% lebih baik dari 75%.
ACV

2
Contoh penggunaan praktis untuk menguji kode yang sebaliknya tidak dapat diakses adalah untuk mencapai cakupan pengujian 100% sehingga tidak ada yang harus melihat kelas itu lagi. Jika cakupannya macet di 95%, banyak pengembang mungkin mencoba mencari tahu alasannya hanya untuk mengalami masalah ini berulang kali.
thisismydesign

140

Saya tidak sepenuhnya setuju dengan Jon Skeet. Saya pikir jika Anda bisa mendapatkan kemenangan mudah untuk memberi Anda liputan dan menghilangkan gangguan dalam laporan liputan Anda, maka Anda harus melakukannya. Beri tahu alat cakupan Anda untuk mengabaikan konstruktor, atau kesampingkan idealisme dan tulis tes berikut dan selesaikan dengannya:

@Test
public void testConstructorIsPrivate() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
  Constructor<Foo> constructor = Foo.class.getDeclaredConstructor();
  assertTrue(Modifier.isPrivate(constructor.getModifiers()));
  constructor.setAccessible(true);
  constructor.newInstance();
}

25
Tapi ini menghilangkan gangguan dalam laporan cakupan dengan menambahkan derau ke rangkaian pengujian. Saya akan mengakhiri kalimat dengan "mengesampingkan idealisme". :)
Christopher Orr

11
Untuk memberi pengujian ini arti apa pun, Anda mungkin juga harus menegaskan bahwa tingkat akses konstruktor adalah yang Anda harapkan.
Jeremy

Menambahkan refleksi jahat ditambah ide Jeremy ditambah nama yang berarti seperti "testIfConstructorIsPrivateWithoutRaisingExceptions", saya rasa ini adalah jawaban "THE".
Eduardo Costa

1
Ini salah secara sintaksis bukan? Apa constructor? Tidak Constructorboleh diparameterisasi dan bukan tipe mentah?
Adam Parkin

2
Ini salah: constructor.isAccessible()selalu mengembalikan nilai salah, bahkan pada konstruktor publik. Seseorang harus menggunakan assertTrue(Modifier.isPrivate(constructor.getModifiers()));.
timomeinen

78

Meskipun belum tentu untuk cakupan, saya membuat metode ini untuk memverifikasi bahwa kelas utilitas didefinisikan dengan baik dan melakukan sedikit cakupan juga.

/**
 * Verifies that a utility class is well defined.
 * 
 * @param clazz
 *            utility class to verify.
 */
public static void assertUtilityClassWellDefined(final Class<?> clazz)
        throws NoSuchMethodException, InvocationTargetException,
        InstantiationException, IllegalAccessException {
    Assert.assertTrue("class must be final",
            Modifier.isFinal(clazz.getModifiers()));
    Assert.assertEquals("There must be only one constructor", 1,
            clazz.getDeclaredConstructors().length);
    final Constructor<?> constructor = clazz.getDeclaredConstructor();
    if (constructor.isAccessible() || 
                !Modifier.isPrivate(constructor.getModifiers())) {
        Assert.fail("constructor is not private");
    }
    constructor.setAccessible(true);
    constructor.newInstance();
    constructor.setAccessible(false);
    for (final Method method : clazz.getMethods()) {
        if (!Modifier.isStatic(method.getModifiers())
                && method.getDeclaringClass().equals(clazz)) {
            Assert.fail("there exists a non-static method:" + method);
        }
    }
}

Saya telah menempatkan kode lengkap dan contoh di https://github.com/trajano/maven-jee6/tree/master/maven-jee6-test


11
+1 Hal ini tidak hanya menyelesaikan masalah tanpa menipu alat, tetapi sepenuhnya menguji standar pengkodean untuk menyiapkan kelas utilitas. Saya harus mengubah tes aksesibilitas untuk digunakan Modifier.isPrivateseperti isAccessibleyang kembali trueuntuk konstruktor pribadi dalam beberapa kasus (mengejek gangguan perpustakaan?).
David Harkness

4
Saya benar-benar ingin menambahkan ini ke kelas Assert JUnit, tetapi tidak ingin mengambil kredit untuk pekerjaan Anda. Saya pikir itu sangat bagus. Sangat menyenangkan memiliki Assert.utilityClassWellDefined()JUnit 4.12+. Sudahkah Anda mempertimbangkan permintaan tarik?
Solusi Perangkat Lunak Visioner

Perhatikan bahwa menggunakan setAccessible()untuk membuat konstruktor dapat diakses menyebabkan masalah untuk alat cakupan kode Sonar (ketika saya melakukan ini kelas menghilang dari laporan cakupan kode Sonar).
Adam Parkin

Terima kasih, saya memang menyetel ulang bendera yang dapat diakses. Mungkin itu bug di Sonar itu sendiri?
Archimedes Trajano

Saya melihat laporan Sonar saya untuk liputan tentang plugin maven batik saya, tampaknya menutupi dengan benar. site.trajano.net/batik-maven-plugin/cobertura/index.html
Archimedes Trajano

19

Saya telah menjadikan private sebagai konstruktor kelas fungsi utilitas statis saya, untuk memenuhi CheckStyle. Tapi seperti poster aslinya, Cobertura mengeluh tentang ujian itu. Awalnya saya mencoba pendekatan ini, tetapi ini tidak mempengaruhi laporan cakupan karena konstruktor tidak pernah benar-benar dijalankan. Jadi sebenarnya semua pengujian ini adalah jika konstruktor tetap bersifat pribadi - dan ini dibuat berlebihan oleh pemeriksaan aksesibilitas di pengujian berikutnya.

@Test(expected=IllegalAccessException.class)
public void testConstructorPrivate() throws Exception {
    MyUtilityClass.class.newInstance();
    fail("Utility class constructor should be private");
}

Saya mengikuti saran Javid Jamae dan menggunakan refleksi, tetapi menambahkan pernyataan untuk menangkap siapa pun yang mengotak-atik kelas yang sedang diuji (dan menamai tes tersebut untuk menunjukkan Tingkat Kejahatan Tinggi).

@Test
public void evilConstructorInaccessibilityTest() throws Exception {
    Constructor[] ctors = MyUtilityClass.class.getDeclaredConstructors();
    assertEquals("Utility class should only have one constructor",
            1, ctors.length);
    Constructor ctor = ctors[0];
    assertFalse("Utility class constructor should be inaccessible", 
            ctor.isAccessible());
    ctor.setAccessible(true); // obviously we'd never do this in production
    assertEquals("You'd expect the construct to return the expected type",
            MyUtilityClass.class, ctor.newInstance().getClass());
}

Ini sangat berlebihan, tetapi saya harus mengakui bahwa saya menyukai perasaan hangat kabur dari cakupan metode 100%.


Mungkin berlebihan, tetapi jika itu di Unitils atau serupa, saya akan menggunakannya
Stewart

+1 Awal yang bagus, meskipun saya menggunakan tes Archimedes yang lebih lengkap .
David Harkness

Contoh pertama tidak berfungsi - IllegalAccesException berarti konstruktor tidak pernah dipanggil, jadi cakupan tidak direkam.
Tom McIntyre

IMO, solusi dalam potongan kode pertama adalah yang paling bersih dan paling sederhana dalam diskusi ini. Hanya sejalan dengan fail(...)tidak perlu.
Piotr Wittchen

9

Dengan Java 8 , Anda dapat menemukan solusi lain.

Saya berasumsi bahwa Anda hanya ingin membuat kelas utilitas dengan beberapa metode statis publik. Jika Anda dapat menggunakan Java 8, Anda dapat menggunakan interfacesebagai gantinya.

package com.XXX;

public interface Foo {

  public static int bar() {
    return 1;
  }
}

Tidak ada konstruktor dan tidak ada keluhan dari Cobertura. Sekarang Anda hanya perlu menguji garis yang benar-benar Anda pedulikan.


1
Sayangnya, bagaimanapun, Anda tidak dapat mendeklarasikan antarmuka sebagai "final", mencegah siapa pun dari subkelasnya - jika tidak, ini akan menjadi pendekatan terbaik.
Michael Berry

5

Alasan di balik kode pengujian yang tidak melakukan apa pun adalah untuk mencapai cakupan kode 100% dan untuk memperhatikan ketika cakupan kode turun. Kalau tidak, orang selalu bisa berpikir, hei saya tidak memiliki cakupan kode 100% lagi tetapi MUNGKIN karena konstruktor pribadi saya. Ini membuatnya mudah untuk menemukan metode yang belum diuji tanpa harus memeriksa bahwa itu hanya konstruktor pribadi. Saat basis kode Anda tumbuh, Anda benar-benar akan merasakan perasaan hangat yang menyenangkan melihat 100%, bukan 99%.

IMO lebih baik menggunakan refleksi di sini karena jika tidak, Anda harus mendapatkan alat cakupan kode yang lebih baik yang mengabaikan konstruktor ini atau entah bagaimana memberi tahu alat cakupan kode untuk mengabaikan metode (mungkin Anotasi atau file konfigurasi) karena Anda akan terjebak dengan alat cakupan kode tertentu.

Dalam dunia yang sempurna, semua alat cakupan kode akan mengabaikan konstruktor privat yang termasuk dalam kelas akhir karena konstruktor ada di sana sebagai ukuran "keamanan", tidak ada yang lain :)
Saya akan menggunakan kode ini:

    @Test
    public void callPrivateConstructorsForCodeCoverage() throws SecurityException, NoSuchMethodException, IllegalArgumentException, InstantiationException, IllegalAccessException, InvocationTargetException
    {
        Class<?>[] classesToConstruct = {Foo.class};
        for(Class<?> clazz : classesToConstruct)
        {
            Constructor<?> constructor = clazz.getDeclaredConstructor();
            constructor.setAccessible(true);
            assertNotNull(constructor.newInstance());
        }
    }
Dan kemudian tambahkan kelas ke array saat Anda pergi.


5

Versi Cobertura yang lebih baru memiliki dukungan bawaan untuk mengabaikan pengambil / penyetel / konstruktor sepele:

https://github.com/cobertura/cobertura/wiki/Ant-Task-Reference#ignore-trivial

Abaikan Sepele

Abaikan sepele memungkinkan kemampuan untuk mengecualikan konstruktor / metode yang berisi satu baris kode. Beberapa contoh termasuk panggilan ke konstruktor super saja, metode pengambil / penyetel, dll. Untuk menyertakan argumen abaikan sepele, tambahkan berikut ini:

<cobertura-instrument ignoreTrivial="true" />

atau dalam build Gradle:

cobertura {
    coverageIgnoreTrivial = true
}

4

Jangan. Apa gunanya menguji konstruktor kosong? Karena cobertura 2.0 ada opsi untuk mengabaikan kasus sepele seperti itu (bersama dengan setter / getter), Anda dapat mengaktifkannya di maven dengan menambahkan bagian konfigurasi ke plugin cobertura maven:

<configuration>
  <instrumentation>
    <ignoreTrivial>true</ignoreTrivial>                 
  </instrumentation>
</configuration>

Atau Anda dapat menggunakan Cakupan Penjelasan : @CoverageIgnore.


3

Akhirnya, ada solusinya!

public enum Foo {;
  public static int bar() {
    return 1;
  }
}

Bagaimana itu menguji kelas yang diposting dalam pertanyaan? Anda tidak boleh berasumsi bahwa Anda dapat mengubah setiap kelas dengan konstruktor privat menjadi enum, atau yang Anda inginkan.
Jon Skeet

@ JonSkeet saya bisa untuk kelas yang bersangkutan. Dan sebagian besar kelas utilitas yang hanya memiliki sekumpulan metode statis. Jika tidak, kelas dengan satu-satunya konstruktor privat tidak masuk akal.
kan

1
Kelas dengan konstruktor privat dapat dibuat dari metode statis publik, meskipun tentu saja mudah untuk mendapatkan cakupannya. Tapi pada dasarnya saya lebih suka kelas apa pun yang diperluas Enum<E>menjadi benar-benar enum ... Saya percaya itu mengungkapkan niat dengan lebih baik.
Jon Skeet

4
Wow, saya benar-benar lebih suka kode yang masuk akal daripada angka yang cukup acak. (Cakupan tidak menjamin kualitas, dan cakupan 100% juga tidak memungkinkan dalam semua kasus. Pengujian Anda harus memandu kode Anda sebaik-baiknya - bukan mengarahkannya ke jurang niat yang aneh.)
Jon Skeet

1
@Kan: Menambahkan panggilan tiruan ke konstruktor untuk menggertak alat seharusnya bukan maksudnya. Siapa pun yang mengandalkan satu metrik untuk menentukan kesejahteraan proyek sudah berada di jalur kehancuran.
Apoorv Khurasia

1

Saya tidak tahu tentang Cobertura tetapi saya menggunakan Clover dan ini memiliki cara untuk menambahkan pengecualian pencocokan pola. Misalnya, saya memiliki pola yang mengecualikan baris apache-commons-logging sehingga tidak dihitung dalam cakupan.


1

Pilihan lainnya adalah membuat penginisialisasi statis yang mirip dengan kode berikut

class YourClass {
  private YourClass() {
  }
  static {
     new YourClass();
  }

  // real ops
}

Dengan cara ini konstruktor pribadi dianggap telah diuji, dan overhead waktu proses pada dasarnya tidak dapat diukur. Saya melakukan ini untuk mendapatkan cakupan 100% menggunakan EclEmma, ​​tetapi kemungkinan itu berhasil untuk setiap alat cakupan. Kelemahan dari solusi ini, tentu saja, adalah Anda menulis kode produksi (penginisialisasi statis) hanya untuk tujuan pengujian.


Saya melakukan ini sedikit. Murah seperti murah, murah seperti kotor, tetapi efektif.
pholser

Dengan Sonar, ini sebenarnya menyebabkan kelas terlewatkan sepenuhnya oleh cakupan kode.
Adam Parkin

1

ClassUnderTest testClass = Whitebox.invokeConstructor (ClassUnderTest.class);


Ini seharusnya jawaban yang benar karena menjawab persis apa yang diminta.
Chakian

0

Terkadang Cobertura menandai kode yang tidak dimaksudkan untuk dieksekusi sebagai 'tidak tercakup', tidak ada yang salah dengan itu. Mengapa Anda lebih khawatir dengan 99%cakupan daripada 100%?

Secara teknis, Anda masih bisa memanggil konstruktor itu dengan refleksi, tetapi kedengarannya sangat salah bagi saya (dalam kasus ini).


0

Jika saya harus menebak maksud pertanyaan Anda, saya akan mengatakan:

  1. Anda ingin pemeriksaan yang wajar untuk konstruktor pribadi yang melakukan pekerjaan aktual, dan
  2. Anda ingin semanggi mengecualikan konstruktor kosong untuk kelas util.

Untuk 1, jelas Anda ingin semua inisialisasi dilakukan melalui metode pabrik. Dalam kasus seperti itu, pengujian Anda harus dapat menguji efek samping konstruktor. Ini harus termasuk dalam kategori pengujian metode privat normal. Buat metode menjadi lebih kecil sehingga mereka hanya melakukan sejumlah hal yang menentukan (idealnya, hanya satu hal dan satu hal dengan baik) dan kemudian uji metode yang mengandalkannya.

Misalnya, jika konstruktor [pribadi] saya menyiapkan bidang instance kelas saya ake 5. Lalu saya bisa (atau lebih tepatnya harus) mengujinya:

@Test
public void testInit() {
    MyClass myObj = MyClass.newInstance(); //Or whatever factory method you put
    Assert.assertEquals(5, myObj.getA()); //Or if getA() is private then test some other property/method that relies on a being 5
}

Untuk 2, Anda dapat mengonfigurasi clover untuk mengecualikan konstruktor Util jika Anda memiliki pola penamaan yang ditetapkan untuk kelas Util. Misalnya, dalam proyek saya sendiri, saya menggunakan sesuatu seperti ini (karena kami mengikuti konvensi bahwa nama untuk semua kelas Util harus diakhiri dengan Util):

<clover-setup initString="${build.dir}/clovercoverage.db" enabled="${with.clover}">
    <methodContext name="prvtCtor" regexp="^private *[a-zA-Z0-9_$]+Util *( *) *"/>
</clover-setup>

Saya sengaja meninggalkan .*berikut )karena konstruktor tersebut tidak dimaksudkan untuk memberikan pengecualian (mereka tidak dimaksudkan untuk melakukan apa pun).

Tentu saja ada kasus ketiga di mana Anda mungkin ingin memiliki konstruktor kosong untuk kelas non-utilitas. Dalam kasus seperti itu, saya akan merekomendasikan agar Anda meletakkan a methodContextdengan tanda tangan konstruktor yang tepat.

<clover-setup initString="${build.dir}/clovercoverage.db" enabled="${with.clover}">
    <methodContext name="prvtCtor" regexp="^private *[a-zA-Z0-9_$]+Util *( *) *"/>
    <methodContext name="myExceptionalClassCtor" regexp="^private MyExceptionalClass()$"/>
</clover-setup>

Jika Anda memiliki banyak kelas luar biasa seperti itu, Anda dapat memilih untuk memodifikasi reg-ex konstruktor privat umum yang saya sarankan dan menghapusnya Util. Dalam kasus ini, Anda harus memastikan secara manual bahwa efek samping konstruktor Anda masih diuji dan dicakup oleh metode lain di kelas / proyek Anda.

<clover-setup initString="${build.dir}/clovercoverage.db" enabled="${with.clover}">
    <methodContext name="prvtCtor" regexp="^private *[a-zA-Z0-9_$]+ *( *) .*"/>
</clover-setup>

0
@Test
public void testTestPrivateConstructor() {
    Constructor<Test> cnt;
    try {
        cnt = Test.class.getDeclaredConstructor();
        cnt.setAccessible(true);

        cnt.newInstance();
    } catch (Exception e) {
        e.getMessage();
    }
}

Test.java adalah file sumber Anda, yang memiliki konstruktor pribadi Anda


Alangkah baiknya untuk menjelaskan, mengapa konstruksi ini membantu cakupan.
Markus

Benar, dan kedua: mengapa menangkap pengecualian dalam pengujian Anda? Pengecualian yang diberikan seharusnya membuat pengujian gagal.
Jordi

0

Yang berikut ini bekerja pada saya di kelas yang dibuat dengan penjelasan Lombok @UtilityClass, yang secara otomatis menambahkan konstruktor pribadi.

@Test
public void testConstructorIsPrivate() throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
    Constructor<YOUR_CLASS_NAME> constructor = YOUR_CLASS_NAME.class.getDeclaredConstructor();
    assertTrue(Modifier.isPrivate(constructor.getModifiers())); //this tests that the constructor is private
    constructor.setAccessible(true);
    assertThrows(InvocationTargetException.class, () -> {
        constructor.newInstance();
    }); //this add the full coverage on private constructor
}

Meskipun konstruktor.setAccessible (true) seharusnya berfungsi ketika konstruktor privat ditulis secara manual, dengan anotasi Lombok tidak berfungsi, karena ia memaksanya. Constructor.newInstance () sebenarnya menguji bahwa konstruktor dipanggil dan ini melengkapi cakupan costructor itu sendiri. Dengan assertThrows Anda mencegah pengujian gagal dan Anda mengelola pengecualian karena persis seperti kesalahan yang Anda harapkan. Meskipun ini adalah solusi dan saya tidak menghargai konsep "cakupan garis" vs "cakupan fungsionalitas / perilaku", kami dapat menemukan pengertiannya pada tes ini. Bahkan Anda yakin bahwa Kelas Utilitas sebenarnya memiliki Pembuat pribadi yang dengan benar melontarkan pengecualian saat dipanggil juga melalui refleksi. Semoga ini membantu.


Hai @Shanteshwind. Terima kasih banyak. Masukan saya telah diedit dan diselesaikan mengikuti saran Anda. Salam.
Riccardo Solimena

0

Pilihan yang saya sukai di 2019: Gunakan lombok.

Secara khusus, @UtilityClassanotasi . (Sayangnya hanya "eksperimental" pada saat penulisan, tetapi berfungsi dengan baik dan memiliki pandangan positif, sehingga kemungkinan akan segera ditingkatkan ke stabil.)

Anotasi ini akan menambahkan konstruktor privat untuk mencegah pembuatan instance dan membuat kelas menjadi final. Saat digabungkan dengan lombok.addLombokGeneratedAnnotation = truein lombok.config, hampir semua framework pengujian akan mengabaikan kode yang dibuat secara otomatis saat menghitung cakupan pengujian, memungkinkan Anda untuk melewati cakupan dari kode yang dibuat secara otomatis tanpa peretasan atau refleksi.


-2

Tidak boleh.

Tampaknya Anda sedang membuat konstruktor pribadi untuk mencegah pembuatan instance kelas yang dimaksudkan untuk hanya berisi metode statis. Daripada mencoba mendapatkan cakupan konstruktor ini (yang mengharuskan kelas dibuat instance-nya), Anda harus membuangnya dan mempercayai developer Anda untuk tidak menambahkan metode instance ke kelas.


3
Itu tidak benar; Anda dapat membuat contoh melalui refleksi, seperti disebutkan di atas.
theotherian

Itu buruk, jangan biarkan konstruktor publik default muncul, Anda harus menambahkan yang privat untuk mencegah pemanggilannya.
Lho Ben
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.