การบำรุงรักษาโค้ดเบสที่เพิ่มขึ้นและหลากหลายพร้อมการผสานอย่างต่อเนื่อง


10

ฉันต้องการความช่วยเหลือเกี่ยวกับปรัชญาและการออกแบบการตั้งค่าการรวมระบบอย่างต่อเนื่อง

การตั้งค่า CI ปัจจุบันของเราใช้ buildbot เมื่อฉันเริ่มออกแบบมันฉันได้รับมรดก (ก็ไม่ใช่อย่างเคร่งครัดเนื่องจากฉันมีส่วนร่วมในการออกแบบเมื่อหนึ่งปีก่อน) ผู้สร้าง CI ตามความต้องการซึ่งได้รับการปรับแต่งให้ทำงานทั้งตัวในเวลาเดียวกันในชั่วข้ามคืน หลังจากนั้นไม่นานเราตัดสินใจว่าสิ่งนี้ไม่เพียงพอและเริ่มสำรวจกรอบ CI ที่แตกต่างกันในที่สุดก็เลือก buildbot หนึ่งในเป้าหมายของฉันในการเปลี่ยนไปเป็น buildbot (นอกเหนือจากการเพลิดเพลินไปกับความพิเศษทั้งหมดที่หวือ - ปัง) คือการเอาชนะความไม่เพียงพอของผู้สร้างรายต่อคืนที่ไม่หยุดยั้งของเรา

ขำขันฉันสักครู่แล้วให้ฉันอธิบายสิ่งที่ฉันได้รับมรดก codebase สำหรับ บริษัท ของฉันเป็นแอพพลิเคชั่น c ++ Windows ที่ไม่ซ้ำกันเกือบ 150 รายการโดยแต่ละรายการมีการพึ่งพาไลบรารีภายในหนึ่งโหลขึ้นไป (และอีกมากมายในไลบรารีของบุคคลที่สามเช่นกัน) บางไลบรารีเหล่านี้มีการพึ่งพาซึ่งกันและกันและมีแอพพลิเคชั่นที่ขึ้นอยู่กับว่า (ในขณะที่พวกเขาไม่มีส่วนเกี่ยวข้อง) จะต้องสร้างด้วยบิลด์เดียวกันของไลบรารีนั้น ครึ่งหนึ่งของแอปพลิเคชันและไลบรารีเหล่านี้ถูกพิจารณาว่าเป็น "ดั้งเดิม" และไม่สามารถถอดออกได้และจะต้องสร้างขึ้นด้วยการกำหนดค่าที่แตกต่างกันหลายอย่างของคอมไพเลอร์ IBM (ซึ่งฉันได้เขียนคลาสย่อยที่ไม่ซ้ำกันCompile) และอีกครึ่งหนึ่งShellCommands เนื่องจากไม่มีการรองรับ VSS)

ผู้สร้างรายกลางคืนดั้งเดิมของเราใช้แหล่งข้อมูลทุกอย่างและสร้างสิ่งต่าง ๆ ในลำดับที่แน่นอน ไม่มีวิธีในการสร้างเพียงแอปพลิเคชันเดียวหรือเลือกการแก้ไขหรือการจัดกลุ่มสิ่งต่าง ๆ มันจะเปิดตัวเครื่องเสมือนเพื่อสร้างจำนวนของแอปพลิเคชัน มันไม่ได้แข็งแกร่งมาก แต่ก็ไม่สามารถแจกจ่ายได้ มันไม่สามารถขยายได้อย่างน่ากลัว ฉันต้องการที่จะเอาชนะข้อ จำกัด เหล่านี้ทั้งหมดใน buildbot

แต่เดิมวิธีที่ฉันทำคือการสร้างรายการสำหรับแต่ละแอปพลิเคชันที่เราต้องการสร้าง (ทั้งหมด 150ish) จากนั้นสร้างตัวกำหนดตารางเวลาที่เรียกใช้ซึ่งสามารถสร้างแอปพลิเคชันต่าง ๆ เป็นกลุ่มจากนั้นให้รวมกลุ่มเหล่านั้น สิ่งเหล่านี้สามารถทำงานได้บนทาสเฉพาะ (ไม่มีเครื่องเสมือนจริงอีกต่อไป) และถ้าฉันต้องการฉันก็สามารถเพิ่มทาสใหม่ได้ ตอนนี้ถ้าเราต้องการสร้างงานเต็มรูปแบบตามกำหนดเวลาเพียงแค่คลิกเดียว แต่เราสามารถสร้างแอปพลิเคชันเดียวได้หากเราต้องการ

อย่างไรก็ตามมีจุดอ่อนสี่ประการของวิธีการนี้ หนึ่งคือเว็บการอ้างอิงที่ซับซ้อนของต้นไม้ต้นกำเนิดของเรา เพื่อให้การบำรุงรักษา config ง่ายขึ้นผู้สร้างทั้งหมดจะถูกสร้างขึ้นจากพจนานุกรมขนาดใหญ่ การพึ่งพาถูกดึงและสร้างขึ้นในรูปแบบที่ไม่รุนแรงมากนัก (กล่าวคือการปิดบางสิ่งในพจนานุกรมสร้างเป้าหมายของฉัน) อย่างที่สองคือแต่ละบิลด์มีขั้นตอนการบิลด์ระหว่าง 15 ถึง 21 ซึ่งยากที่จะเรียกดูและดูในเว็บอินเตอร์เฟสและเนื่องจากมีประมาณ 150 คอลัมน์จึงต้องโหลดตลอดเวลา (คิดตั้งแต่ 30 วินาทีถึงหลายนาที) ประการที่สามเราไม่มีการค้นพบเป้าหมายการสร้างโดยอัตโนมัติอีกต่อไป (แม้ว่าผู้ร่วมงานคนหนึ่งของฉันจะทำให้ฉันสับสนเกี่ยวกับเรื่องนี้ฉันไม่เห็นสิ่งที่ทำให้เราได้รับในตอนแรก) สุดท้าย

ตอนนี้ย้ายไปสู่การพัฒนาใหม่เราเริ่มใช้ g ++ และการโค่นล้ม (ไม่ใช่การย้ายพื้นที่เก็บข้อมูลเก่าโปรดคำนึงถึงคุณ - สำหรับสิ่งใหม่) นอกจากนี้เรากำลังเริ่มทำการทดสอบหน่วยเพิ่มเติม ("มากกว่า" อาจให้ภาพที่ไม่ถูกต้อง ... มันก็เหมือนๆ กัน ) และการทดสอบการรวม (โดยใช้ python) ฉันมีช่วงเวลาที่ยากลำบากในการหาวิธีการปรับให้พอดีกับการกำหนดค่าปัจจุบันของฉัน

ดังนั้นฉันไปผิดทางปรัชญาที่นี่ที่ไหน ฉันจะดำเนินการต่อได้ดีที่สุด (กับ buildbot - เป็นชิ้นส่วนของปริศนาที่ฉันมีสิทธิ์ใช้งานเท่านั้น) เพื่อให้การกำหนดค่าของฉันสามารถบำรุงรักษาได้จริง? ฉันจะจัดการกับจุดอ่อนของการออกแบบได้อย่างไร อะไรที่ใช้งานได้จริงในแง่ของกลยุทธ์ CI สำหรับรหัสฐานขนาดใหญ่ (อาจมากกว่า)

แก้ไข:

ฉันคิดว่าฉันอธิบายปัญหาของฉัน แต่เห็นได้ชัดว่าฉันยังไม่ชัดเจนพอ ฉันไม่ต้องการคำแนะนำสำหรับการเปลี่ยนแพลตฟอร์ม CI มันจะไม่เกิดขึ้นและคำตอบแนะนำว่าจะไม่ได้รับการยอมรับ สิ่งที่ฉันอยากรู้คือคนอื่นจัดการกับรหัสฐานที่ซับซ้อนโดยใช้ CI ได้อย่างไร ฉันมีผลิตภัณฑ์ที่แตกต่างกันสองโหลและฉันมีการพึ่งพาที่กระจัดกระจายไปตามลมและพวกมันต่างกันทั้งหมด นี่คือสิ่งที่ฉันต้องการทราบวิธีจัดการกับ


ฉันก็อยากจะรู้คำตอบนี้ สภาพแวดล้อมของเราไม่ซับซ้อนเหมือนของคุณ แต่เรามีการพึ่งพาการพึ่งพา (ในกรณีของโครงการติดตั้งมันมีการขึ้นต่อกันสี่ชั้นลึก) ฉันไม่รู้ว่าแต่ละโครงการควรเป็นโครงการ CI หรือถ้าฉันควรใช้ไฟล์. sln ของ Visual Studio เพื่อดูแลมันสำหรับฉันเพื่อที่ฉันจะได้ไม่ต้องสร้างแผนผังการพึ่งพาสำหรับแต่ละโครงการใหม่ (และโครงการในอนาคต)
moswald

การไม่สามารถสร้างบนเครื่องท้องถิ่นของคุณได้ทำให้ภารกิจเซิร์ฟเวอร์ CI ของคุณสำคัญต่อธุรกิจของคุณ คุณอาจต้องการคิดใหม่
Thorbjørn Ravn Andersen

คำตอบ:


3

แม้ว่าฉันจะไม่ได้เผชิญกับสถานการณ์ที่เลวร้ายอย่างที่คุณอธิบาย แต่ฉันก็ยังคงรักษาคอนฟิเกอเรชันของ CI ด้วยส่วนประกอบหลายสิบซึ่งระหว่างนั้นมีการพึ่งพา 'ง่าย' ฉันหวังว่าแนวทางของฉันอาจให้คำแนะนำแก่คุณเพื่อดำเนินการต่อ ปัญหาไม่ได้เกี่ยวข้องกับตัวเลือกของเซิร์ฟเวอร์ CI เท่านั้น แต่ยังรวมถึงกระบวนการสร้างโดยรวมและโครงสร้างโครงการ

ฉันจะแยกแยะปัญหาออกเป็น 2 ส่วนคืออาคารและ CI

อาคาร

สำหรับ "อาคาร" ฉันหมายถึงกระบวนการของการเปลี่ยนรหัสแหล่งที่มาในมือเพื่อสิ่งประดิษฐ์ขั้นสุดท้าย บริษัท ของเราใช้ Java เป็นหลักในการพัฒนาและเครื่องมือสร้างที่เราใช้คือ Maven คุณอาจจะไม่สามารถใช้สิ่งนั้นได้เนื่องจากธรรมชาติของโครงการของคุณ แต่มีแนวคิดที่มีค่าบางอย่างใน Maven ที่ควรค่าแก่การสังเกต:

1) ในโลก Maven สิ่งประดิษฐ์แต่ละอย่าง (lib, โปรแกรมจริง ฯลฯ ) จะต้องแยกและแยกออกอย่างชัดเจน การอ้างอิงระหว่างสิ่งประดิษฐ์ควรมีความชัดเจน ฉันควรเน้นพึ่งพายุ่งโดยเฉพาะอย่างยิ่งการพึ่งพาแบบวงกลมระหว่างสิ่งประดิษฐ์สร้างของคุณกำลังจะทำให้กระบวนการสร้างเป็นระเบียบ

ตัวอย่างเช่นฉันเห็นบางโครงการ Java ก่อนหน้านั้นแม้ว่าหลังจากกระบวนการสร้างทั้งหมดจะมี JARs หลายตัว (คุณอาจพิจารณาว่าเป็น lib / dll ใน Java) ที่สร้างขึ้นพวกเขาอยู่ระหว่างการพึ่งพา ชอบ A.jar ใช้สิ่งต่าง ๆ ใน B.jar และในทางกลับกัน 'การทำให้เป็นโมดูล' แบบนั้นไม่มีความหมายเลย A.jar และ B.jar จะต้องมีการปรับใช้และใช้ร่วมกันเสมอ ความหมายคือในภายหลังหากคุณต้องการแยกพวกเขาออกเป็นโครงการต่าง ๆ (เช่นนำมาใช้ใหม่ในโครงการอื่น ๆ ) คุณจะไม่สามารถทำได้เพราะคุณไม่สามารถกำหนดโครงการ A หรือ B ที่จะสร้างได้ก่อน

ใช่ต้องได้รับการออกแบบซอฟต์แวร์ของคุณ แต่ฉันเชื่อเสมอว่าแทนที่จะต้องเสียเวลาในการสร้างเครื่องมือ / แบบจำลองอาคารที่ซับซ้อนในโครงการที่ยุ่งเหยิงฉันควรใช้เวลาในการปรับโครงสร้างการออกแบบเพื่อใช้ แบบจำลองอาคารที่เรียบง่าย

2) การพึ่งพาควรจะเปิดเผย มีกระบวนการสร้างจำนวนมากที่ฉันเคยเห็นมาก่อนซึ่งพวกเขารวม libs ทั้งหมดที่ต้องการภายในเครื่องในโครงการ มันจะทำให้กระบวนการสร้างเกิดปัญหาอย่างมากหาก libs บางอันอยู่ในความเป็นจริงแล้วสิ่งประดิษฐ์อื่น ๆ ที่คุณต้องสร้าง

3) ที่เก็บข้อมูลแบบ "รวมศูนย์" สำหรับสิ่งประดิษฐ์เพื่อรับการอ้างอิงหรือเพื่อปรับใช้สิ่งประดิษฐ์เมื่อมีการรวบรวม ไม่จำเป็นต้องเป็น "ส่วนกลาง" สำหรับสำนักงานทั้งหมด (จะดีมากถ้าเป็น) เพียงแค่ไดเรกทอรีท้องถิ่นจะใช้ได้

รายละเอียดเพิ่มเติมเกี่ยวกับ 2 และ 3 เพียงเพื่อยกตัวอย่างฉันได้เจอโครงการที่เกี่ยวข้องกับ 3 โครงการอิสระ แต่ละโครงการสร้างขึ้นจากแหล่งที่มารวมทั้ง libs ภายใต้ lib / ไดเรกทอรีในไดเรกทอรีแหล่งที่มา โปรเจ็กต์ A จะสร้าง libs หลายอันซึ่งจะถูกใช้โดยโปรเจค B และ C มีข้อเสียบ้าง: การสร้างโพรซีเดอร์นั้นซับซ้อนและยากที่จะทำให้เป็นอัตโนมัติ การควบคุมแหล่งที่มาได้รับการป่องด้วย JAR ซ้ำที่ไม่จำเป็นนำมาใช้ใหม่ในโครงการที่แตกต่างกัน

ในโลกของ Maven สิ่งที่ทำคือโครงการ B และ C ไม่ได้มี A.jar (และการพึ่งพาอื่น ๆ เช่น lib บุคคลที่สามอื่น ๆ ) ในแหล่งที่มาของโครงการ มันคือการประกาศ ตัวอย่างเช่นในการกำหนดค่าการสร้างของโครงการ B มันเพียงประกาศว่าต้องการ: A.lib v1.0, xyz.lib v2.1 ฯลฯ และสคริปต์การสร้างจะค้นหา lib จาก /A/1.0/A jar และ /xyz/2.1/xyz.lib

สำหรับโลกที่ไม่ใช่ Maven สิ่งประดิษฐ์นี้จะต้องเป็นไดเรกทอรีหนึ่งหรือสองที่มีโครงสร้างไดเรกทอรีที่ตกลงกันไว้ คุณสามารถใส่ libs บุคคลที่สามทั้งหมดไปยังที่ตั้งแบ่งปันและให้นักพัฒนาซิงค์หรือคัดลอกไปยังเครื่องท้องถิ่น ในโครงการ C ++ ที่ฉันทำเมื่อหลายปีก่อนสิ่งที่ฉันกำลังทำอยู่คือการตั้งค่า lib และส่วนหัวเป็น $ {ifact_dir} / lib_name / ver และมี artifact_id ประกาศเป็นตัวแปรสภาพแวดล้อม

เมื่อสร้างโปรเจ็กต์ A จะมีสำเนาผลลัพธ์ใน artifact_dir นั้นดังนั้นเมื่อเราสร้างโปรเจค B, B สามารถรับผลลัพธ์ของ A โดยอัตโนมัติโดยไม่ต้องคัดลอกด้วยตนเอง

4) การเปิดตัวที่ไม่แน่นอน เมื่อคุณปล่อย A.lib 1.0 แล้วคุณจะไม่คาดหวังการเปลี่ยนแปลงเนื้อหาของ A.lib 1.0 หลังจาก 1 เดือนเพียงแค่ bcos มีการแก้ไขข้อบกพร่องบางอย่าง ในกรณีเช่นนี้ควรเป็น A.lib 1.1 สิ่งประดิษฐ์ของการเปลี่ยนแปลงฐานรหัสควรพิจารณา "รุ่น" พิเศษซึ่งใน Maven เราเรียกว่าภาพรวม

การเผยแพร่ที่ไม่สามารถเปลี่ยนแปลงได้นั้นเป็นประเด็นด้านจริยธรรมมากกว่า แต่สิ่งที่แก้ไขได้ชัดเจน: เมื่อคุณมีโครงการจำนวนมากโดยใช้ lib เดียวกันคุณจะรู้ว่าคุณใช้ lib เวอร์ชันใดและคุณจะมั่นใจได้ว่าเวอร์ชันเดียวกันของ lib ที่ใช้ในโครงการต่าง ๆ ย่อมเหมือนกัน . ฉันคิดว่าพวกเราหลายคนผ่านปัญหา: ทำไมโครงการ X และ Y ถึงใช้ lib A แต่ผลลัพธ์การสร้างแตกต่างกันอย่างไร (และปรากฎว่า lib A ที่ใช้โดย X และ Y นั้นแตกต่างจากการขุดลงในเนื้อหาหรือขนาดไฟล์ของ lib)


ทั้งหมดนี้ทำให้มั่นใจได้ว่าโครงการของคุณสามารถสร้างได้อย่างอิสระโดยไม่ต้องใช้เทคนิคมากเช่นสร้างโครงการ A ก่อนคัดลอก A.lib ไปยังโครงการ B แล้วสร้างโครงการ B ...

หลังจากทำแบบฝึกหัดเช่นนี้เมื่อคุณสร้างโครงการ A มันจะพยายามรับการอ้างอิงจากที่เก็บสิ่งประดิษฐ์ส่วนกลาง ในกรณีที่ไม่พบการพึ่งพา (ซึ่งเป็นโครงการอื่นของ บริษัท ของคุณเช่นโครงการ B) ไม่พบสิ่งที่คุณต้องทำคือรับแหล่งที่มาของโครงการ B สร้างมันขึ้นมา (ซึ่งจะนำไปใช้กับพื้นที่เก็บข้อมูลส่วนกลางเมื่อสำเร็จ) และ จากนั้นสร้างโครงการ A อีกครั้ง


CI

ด้วยระบบการสร้างที่ง่ายด้วยการพึ่งพาที่ชัดเจน CI จะง่ายขึ้นมาก ฉันคาดหวังว่าเซิร์ฟเวอร์ CI ของคุณจะรองรับความต้องการดังต่อไปนี้:

1) ตรวจสอบการควบคุมแหล่งที่มาเพียงชำระเงิน + สร้างเมื่อมีการเปลี่ยนแปลงในแหล่งที่มา

2) สามารถตั้งค่าการพึ่งพาระหว่างโครงการ

ด้วยการอ้างอิงที่ชัดเจนสำหรับโครงการคุณเพียงแค่ต้องตั้งค่าโครงการใน CI ตามโครงการจริงของคุณตั้งค่าการพึ่งพาโครงการ CI ตามการพึ่งพาโครงการของคุณ เซิร์ฟเวอร์ CI ของคุณควรถูกตั้งค่าให้สร้างโครงการที่ต้องพึ่งพากันก่อนที่จะสร้างโครงการใด ๆ (และแน่นอนว่าจะสร้างขึ้นก็ต่อเมื่อแหล่งที่มาของโครงการมีการเปลี่ยนแปลงจริง ๆ )

หากทุกอย่างถูกต้องคุณควรมี CI ขนาดใหญ่ที่ซับซ้อน แต่ยังคงสามารถจัดการได้ (และที่สำคัญกว่านั้นคือโครงสร้างโครงการที่จัดการได้)


ฉันชอบคำตอบนี้ แต่คุณช่วยอธิบายรายละเอียดเพิ่มเติมในส่วน 'อาคาร' ได้ไหม? นั่นคือยกตัวอย่างอธิบายแนวคิดบางอย่างให้ละเอียดยิ่งขึ้น
เนท

อืม ... เหมือนส่วนไหน? ฉันพยายามแก้ไขโพสต์เพื่อให้รายละเอียดของแต่ละส่วนมากขึ้น มันจะดีกว่าถ้าคุณสามารถบอกฉันได้ว่าส่วนใดที่คุณต้องการให้ละเอียด อย่างไรก็ตามถ้าคุณใช้ Java ด้วยให้ดูที่ maven.apache.org Maven อาจไม่ใช่สิ่งที่ง่ายที่สุดในการนำมาใช้ แต่มันบังคับให้คุณทำให้สิ่งต่าง ๆ ของคุณสะอาดและเป็นระเบียบ
Adrian Shum

1

สิ่งที่ทำงานในสถานการณ์ที่คล้ายกันสำหรับฉัน:

  • ระดับนามธรรมของแอพโดยการแบ่งบางส่วนของบิลด์ออกเป็นแอพบิลด์เฉพาะ (ในกรณีนี้คือสคริปต์ PHP ที่เรียกว่าจากบิลด์หลัก) สิ่งนี้สามารถลดขั้นตอนการสร้างหลายสิบบรรทัดเป็นหนึ่งขั้นตอนการสร้าง
  • Build-level abstraction โดยสร้าง sub-build-สคริปต์ที่เรียกใช้จากสคริปต์สร้างหลัก ไม่ทราบว่าเกี่ยวข้องกับ buildbot หรือไม่ (ไม่มีประสบการณ์กับผลิตภัณฑ์นั้น)

ฉันขอแนะนำให้คุณปฏิบัติต่อการสร้างตัวเองเป็นโครงการพัฒนาซอฟต์แวร์ ซึ่งหมายความว่าคุณต้องทำให้โมดูลโค้ดบิลด์เป็นโมดูลและเขียนการทดสอบอัตโนมัติบางอย่าง (เช่นสร้างหมายเลขการแก้ไขที่รู้จักและตรวจสอบว่ามันให้ผลลัพธ์ที่ถูกต้องหรือไม่)


0

ฉันจะพิจารณา Jenkins เป็นเซิร์ฟเวอร์ CI คุณสามารถดูคุณสมบัติที่นี่:

https://wiki.jenkins-ci.org/display/JENKINS/Meet+Jenkins

ทำไม? ติดตั้งง่ายมีอินเทอร์เฟซที่ยอดเยี่ยมตั้งค่าและขยายได้ง่ายและมีปลั๊กอินที่พร้อมใช้งานจำนวนมากสำหรับเกือบทุกอย่าง:

https://wiki.jenkins-ci.org/display/JENKINS/Plugins

ให้มันลอง :)


2
ฉันรู้สึกว่าคุณอ่านชื่อคำถามของฉัน แต่ไม่ได้อ่านเนื้อหา ... ฉันไม่ได้มองหาคำแนะนำผลิตภัณฑ์ ฉันได้เลือกผลิตภัณฑ์ ฉันกำลังมองหาวิธีจัดการการตั้งค่า CI ที่ซับซ้อน
เนท

เนทฉันอ่านบทความทั้งหมดของคุณและฉันคิดว่าคุณได้เลือกผลิตภัณฑ์ที่ผิดนั่นคือเหตุผลที่ฉันแนะนำเจนกินส์ ฉันใช้ Jenkins ในที่ทำงานเพื่อทดสอบผลิตภัณฑ์ C ++ หลายประเภท (แม้แต่การทดสอบที่เขียนในหลายภาษา) ด้วยการทำคลัสเตอร์บนเครื่องหลายเครื่องและทำงานได้ดีมาก ง่ายมากในการจัดการและกำหนดค่า ลองสิ :)
Patrizio Rullo

ดูโพสต์บล็อกนี้ด้วย: vperic.blogspot.com/2011/05/…
Patrizio Rullo

0

ขณะนี้เราใช้ไปโดย ThoughtWorksก่อนที่จะว่าเราได้ใช้CruiseControl.Net มันมีประโยชน์มากเมื่อพิจารณาว่าเรามีสองทีมครึ่งโลกที่อยู่ห่างจากกันและมีความแตกต่างของโซนเวลาขนาดใหญ่ระหว่างเรา

เรากำลังจัดการหลายโครงการและงานส่วนใหญ่เกี่ยวข้องกับการทับซ้อนกันระหว่างสองนักพัฒนาในทั้งสองด้านของโลกดังนั้นเราต้องจำไว้ว่าไฟล์ทั้งหมดไม่ควรทำลายโครงสร้างของคนอื่น

นอกจากนี้การจัดการทำได้ง่ายขึ้นด้วย Go และเป็นมิจฉาชีพกระบวนการ Agile ทำให้เราปวดหัวน้อยลงหลังจากที่เราติดตั้ง การทดสอบยังได้รับการรวมเข้ากับ Go

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.