Bộ dữ liệu Iris được tạo ra khi nhà sinh vật học người Anh Ronald
Fisher xuất bản bài báo năm 1936 của ông “Việc sử dụng nhiều phép đo
trong các vấn đề phân loại” như một ví dụ về phân tích phân biệt tuyến
tính. Đôi khi nó cũng được gọi là tập dữ liệu Anderson’s Iris vì Edgar
Anderson là người đã thu thập dữ liệu để định lượng sự biến đổi hình
thái của hoa Iris theo ba loài có liên quan. Về cơ bản, hai nhà khoa học
lâu đời này đã khai sinh ra tập dữ liệu, tập dữ liệu vẫn được sử dụng
cho đến ngày nay để học các kiến thức cơ bản về RStudio.
Bộ dữ liệu iris đưa ra các phép đo tính bằng cm của các biến
chiều dài và chiều rộng của lá đài “Sepal” và chiều dài và chiều rộng
của cánh hoa “Petal”, tương ứng, đối với 50 bông hoa từ mỗi trong số 3
loài iris. Các loài là Iris setosa, versicolor, và
virginica.
#Install package
install.packages('dplyr')
install.packages('ggplot2')
install.packages('GGally')
install.packages('cluster')
install.packages('fpc')
library(dplyr)
library(reshape2)
library(ggplot2)
library(GGally)
library(cluster)
library(fpc)
#Explore Iris Dataset
iris
data(iris)
Bước 1: Khám phá dữ liệu Iris
Ở bước đầu tiên chúng ta sẽ tìm hiểu các công thức trong R giúp có
thể xem và khám phá dữ liệu một cách tổng quan. Những tính năng này cho
phép chúng ta nhìn được dữ liệu hoặc cung cấp các thống kê tổng quan về
dữ liệu như những chỉ số cơ bản về Mean, Median, Min, Max, Std,
Variance, … Đồng thời chúng ta cũng có thể xem và xác nhận cấu trúc dữ
liệu và cả định dạng của dữ liệu trước khi phân tích.
#STEP 1: EXPLORE DATA ----
#1.1 Function summary to review data
summary(iris)
Sepal.Length Sepal.Width Petal.Length Petal.Width Species
Min. :4.300 Min. :2.000 Min. :1.000 Min. :0.100 setosa :50
1st Qu.:5.100 1st Qu.:2.800 1st Qu.:1.600 1st Qu.:0.300 versicolor:50
Median :5.800 Median :3.000 Median :4.350 Median :1.300 virginica :50
Mean :5.843 Mean :3.057 Mean :3.758 Mean :1.199
3rd Qu.:6.400 3rd Qu.:3.300 3rd Qu.:5.100 3rd Qu.:1.800
Max. :7.900 Max. :4.400 Max. :6.900 Max. :2.500
#1.2 Function names to show columns name
names(iris)
[1] "Sepal.Length" "Sepal.Width" "Petal.Length" "Petal.Width" "Species"
names(iris) <- tolower(names(iris))
#1.3 Function dim to show row and column counts
dim(iris)
[1] 150 5
#1.4 Function class to show data structure
class(iris)
[1] "data.frame"
#1.5 Function typeof and str to show data type
typeof(iris$sepal.length)
[1] "double"
str(iris$sepal.length)
num [1:150] 5.1 4.9 4.7 4.6 5 5.4 4.6 5 4.4 4.9 ...
typeof(iris$species)
[1] "integer"
class(iris$species)
[1] "factor"
str(iris$species)
Factor w/ 3 levels "setosa","versicolor",..: 1 1 1 1 1 1 1 1 1 1 ...
Bước 2: Chỉnh sửa và thay đổi cấu trúc dữ liệu
Ở bước tiếp theo, chúng ta sẽ cùng sử dụng những câu lệnh để chỉnh
sửa cấu trúc dữ liệu Iris theo yêu cầu. Điều này bao gồm việc bóc tách
dữ liệu thành những bảng nhỏ hơn như phân thành các bảng theo từng loài
hoa. Hoặc chúng mong muốn thêm các cột, sort bảng theo một tiêu chí nào
đó.
Và một yếu tố quan trọng không kém là chuyển đổi cấu trúc giống với
tính năng Unpivot trong PowerBI thì chúng ta sẽ chuyển đổi dữ liệu từ
dạng Wide (cấu trúc giống Pivot Table) sang dạng Long (cấu trúc giống
Tabular). Điều này yêu cầu chúng ta sẽ phải cài đặt thêm Package dplyr
là một bộ ngôn ngữ lập trình trong R giúp thay đổi cấu trúc và chỉnh sửa
dữ liệu. Chúng ta sẽ thêm ở bên trên phần Install Package nội dung câu
lệnh để cài đặt package dplyr và sử dụng chúng trong môi trường hiện
tại.
#2.1 Split data into subset
virginica <- iris[iris$species == 'virginica',]
virginica2 <- iris[iris$species == 'virginica' & iris$sepal.length > 6,]
head(virginica)
#2.2 Function select to select columns
selected <- select(iris, sepal.length, sepal.width)
head(selected)
#2.3 Function mutate to add column
newcol <- mutate(iris, longer = sepal.length / sepal.width )
newcol <- mutate(newcol, longer.2x = sepal.length > 2*sepal.width )
tail(newcol)
#2.4 Function arrange to sort data
newcol <- arrange(newcol, sepal.width)
newcol <- arrange(newcol, desc(sepal.width))
head(newcol)
#2.5 Function melt to unpivot table (wide -> long)
iris.melt <- melt(iris, id = 'species', variable.name = 'size')
head(iris.melt)
Bước 3: Trực quan hóa dữ liệu và tạo biểu đồ
Sau khi đã có các bảng dữ liệu theo dạng cần thiết, chúng ta có thể
sử dụng các package và code xây dựng các biểu đồ giúp trực quan hóa
thông tin. Ở bước này chúng ta sẽ tạo những biểu đồ có ý nghĩa về mặt
thống kê với dữ liệu để hiểu về tập đối tượng đang phân tích. Chúng bao
gồm biểu đồ Histogram để xem sự phân bổ về các chiều dài của hoa, sử
dụng Box Plot giúp trực quan các con số thống kê. Sử dụng Scatter Plot
để xem phân bổ của dữ liệu theo 2 chiều cụ thể.
Để tạo ra các biểu đồ này, chúng ta có thể sử dụng các câu lệnh có
sẵn trong RStudio hoặc cài đặt thêm Package GGPlot & Ggally để có
thể tạo các biểu đồ tương tự nhưng có nhiều khả năng thay đổi điều chỉnh
cũng như dễ dàng tạo hơn.
#STEP 3: VISUALIZE DATA ----
#3.1 Function hist to show histogram
hist(iris$sepal.length)

hist(iris$sepal.length,
col='light blue',
main='Histogram',
xlab='Sepal.Length',
ylab='Frequency')

hist(iris$sepal.length, col='red', breaks=20, main='Histogram', xlab='Size')
hist(iris$petal.length, col='green',breaks=30, add=TRUE)
legend('topright',
c('Sepal Length', 'Petal Length'),
fill=c('red', 'green'))

#3.2 Use ggplot to create charts
ggplot(iris.melt, aes(x=value, fill=size)) +
geom_histogram(color ='#e9ecef', alpha = 0.6, position = 'identity')

ggplot(iris.melt, aes(x=value, fill=size)) +
geom_histogram(color ='#e9ecef', alpha = 0.6, position = 'identity') +
facet_wrap(~size)

#3.3 Function boxplot to create boxplot
boxplot(sepal.length ~ species,
data = iris,
main = 'Sepal Length by Species',
xlab = 'Species',
ylab = 'Sepal Length',
col = 'light blue',
border = 'black')

#boxplot with melt data
boxplot(value ~ size,
data = iris.melt,
main = 'Compare different size',
xlab = 'Size',
ylab = 'Value',
col = 'light blue',
border = 'black')

#Advanced boxplot with ggplot
ggplot(iris.melt, aes(x=size, y=value, fill=size)) +
geom_boxplot()+
geom_jitter(color = 'black', size = 0.4, alpha = 0.9)

#3.4 Function plot to create scatter plot
plot(iris)

plot(iris[,1:4])

plot(iris$sepal.width, iris$sepal.length,
col = iris[,5],
main = 'Scatterplot',
xlab = 'Sepal Width',
ylab = 'Sepal Length',
pch = 19)

pairs(iris[,1:4],col=iris[,5],oma=c(4,4,6,12))
par(xpd=TRUE)

ggplot(iris, aes(x=sepal.length, y=sepal.width, color=species)) +
geom_point(size=5)

ggpairs(iris,
columns = 1:4,
aes(color = species, alpha = 0.5))

Bước 4: Kiểm chứng giả định với T-test
Ở buổi trước chúng ta đã học cách sử dụng T-test để xác định giả
thuyết có chính xác hay không. Tương tự với RStudio, chúng ta có thể dễ
dàng sử dụng nhiều loại T-test, ở phần thực hành này chúng ta sẽ sử dụng
loại T-test để xác định giả thuyết cơ bản về các loại hoa như sau.
Câu hỏi: liệu loài setosa và versicolor có chiều dài
cánh hoa khác nhau hay không?
Giả định:
#STEP 4: HYPOTHESIS TESTING WITH T-test ----
setosa <- iris[iris$species == 'setosa',]
versicolor <- iris[iris$species == 'versicolor',]
t.test(x= setosa$petal.length, y = versicolor$petal.length)
Welch Two Sample t-test
data: setosa$petal.length and versicolor$petal.length
t = -39.493, df = 62.14, p-value < 2.2e-16
alternative hypothesis: true difference in means is not equal to 0
95 percent confidence interval:
-2.939618 -2.656382
sample estimates:
mean of x mean of y
1.462 4.260
Bước 5: Phân tích phương sai ANOVA
Tương tự chúng ta cũng có thể chạy các mô hình phân tích ANOVA trong
RStudio. Trong phần thực hành này, chúng ta sẽ cùng phân tích ANOVA để
xác định giả thuyết so sánh về chiều dài cánh hoa giữa cả 3 loài
hoa.
Câu hỏi: liệu 3 loài hoa khác nhau có chiều dài cánh
hoa khác nhau hay không? Giả định:
#STEP 5: ANALYSIS WITH ANOVA ----
petal.length.aov <- aov(formula = petal.length ~ species, data = iris)
summary(object = petal.length.aov)
Df Sum Sq Mean Sq F value Pr(>F)
species 2 437.1 218.55 1180 <2e-16 ***
Residuals 147 27.2 0.19
---
Signif. codes: 0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1
TukeyHSD(petal.length.aov)
Tukey multiple comparisons of means
95% family-wise confidence level
Fit: aov(formula = petal.length ~ species, data = iris)
$species
diff lwr upr p adj
versicolor-setosa 2.798 2.59422 3.00178 0
virginica-setosa 4.090 3.88622 4.29378 0
virginica-versicolor 1.292 1.08822 1.49578 0
Bước 6: Thực hành phân tích phương sai ANOVA
Giả sử như bạn chỉ có các thông số về chiều dài và chiều rộng của
cánh hoa thì có thể từ chúng xác nhận loài hoa hay không? Nếu với 2
phương pháp trước bên trên chúng ta đã chỉ định được rằng kích thước
cánh hoa của 3 loài là khác biệt (tương tự với nhụy hoa) thì chúng ta có
thể tự tin xây dựng mô hình để dự đoán tên loài hoa với các số liệu về
kích thước cánh hoa và nhụy hoa.
Chúng ta sẽ sử dụng mô hình K-mean Clustering để giúp phân loại các
nhóm loài hoa từ các chỉ số trên và từ chúng ta có thể nhận biết được
hoa nào là thuộc loài gì. Để làm được điều này, chúng ta sẽ chuẩn bị một
vài bước xử lý dữ liệu để có dữ liệu để thử nghiệm.
Đầu tiên, ta sẽ tạo ra một bộ dữ liệu iris.test gồm chỉ có 4 cột
thông số về kích thước chứ không có tên của loài hoa. Sau đó ở đây,
chúng ta sẽ cùng sử dụng mô hình K-mean Clustering để phân loại chúng và
biết được giống hoa nào dựa trên các chỉ số về độ dài đó.
K-mean Clustering là một trong những phương pháp học không giám sát
phổ biến nhất trong học máy. Thuật toán này giúp xác định “k” nhóm (cụm)
có thể có từ “n” phần tử dựa trên khoảng cách giữa các phần tử.
Giải thích chi tiết hơn sẽ là: thuật toán tìm ra khoảng cách giữa mỗi
phần tử trong dữ liệu của bạn, sau đó tìm số lượng tâm, phân bổ phần tử
cho các trung tâm gần nhất để tạo thành các cụm và mục tiêu cuối cùng là
giữ nguyên kích thước của mỗi cụm càng nhỏ càng tốt.
Một trong những câu hỏi phổ biến liên quan đến thuật toán K-mean là
liệu nó có thể xử lý dữ liệu không phải là số hay không. Câu trả lời
ngắn gọn là KHÔNG vì thuật toán đang sử dụng khoảng cách giữa các lần
quan sát. Tuy nhiên, có rất nhiều thuật toán có thể giúp chuyển đổi các
tính năng không phải số thành các tính năng số, điều này sẽ cho phép bạn
áp dụng thuật toán K-mean cho dữ liệu của mình.
iris.test <- iris
iris.test$species <- NULL
head(iris.test)
NA
kmeans.result <- kmeans(iris.test, 3)
table(iris$species, kmeans.result$cluster)
1 2 3
setosa 50 0 0
versicolor 0 2 48
virginica 0 36 14
plot(iris.test[c('sepal.length', 'sepal.width')], col = kmeans.result$cluster)

plotcluster(iris.test, kmeans.result$cluster)

clusplot(iris.test, kmeans.result$cluster, color = TRUE, shade = TRUE)

LS0tCnRpdGxlOiAiUiBOb3RlYm9vazogSXJpcyBBbmFseXNpcyIKb3V0cHV0OiBodG1sX25vdGVib29rCi0tLQoKQuG7mSBk4buvIGxp4buHdSBJcmlzIMSRxrDhu6NjIHThuqFvIHJhIGtoaSBuaMOgIHNpbmggduG6rXQgaOG7jWMgbmfGsOG7nWkgQW5oIFJvbmFsZCBGaXNoZXIgeHXhuqV0IGLhuqNuIGLDoGkgYsOhbyBuxINtIDE5MzYgY+G7p2Egw7RuZyAiVmnhu4djIHPhu60gZOG7pW5nIG5oaeG7gXUgcGjDqXAgxJFvIHRyb25nIGPDoWMgduG6pW4gxJHhu4EgcGjDom4gbG/huqFpIiBuaMawIG3hu5l0IHbDrSBk4bulIHbhu4EgcGjDom4gdMOtY2ggcGjDom4gYmnhu4d0IHR1eeG6v24gdMOtbmguIMSQw7RpIGtoaSBuw7MgY8WpbmcgxJHGsOG7o2MgZ+G7jWkgbMOgIHThuq1wIGThu68gbGnhu4d1IEFuZGVyc29uJ3MgSXJpcyB2w6wgRWRnYXIgQW5kZXJzb24gbMOgIG5nxrDhu51pIMSRw6MgdGh1IHRo4bqtcCBk4buvIGxp4buHdSDEkeG7gyDEkeG7i25oIGzGsOG7o25nIHPhu7EgYmnhur9uIMSR4buVaSBow6xuaCB0aMOhaSBj4bunYSBob2EgSXJpcyB0aGVvIGJhIGxvw6BpIGPDsyBsacOqbiBxdWFuLiBW4buBIGPGoSBi4bqjbiwgaGFpIG5ow6Aga2hvYSBo4buNYyBsw6J1IMSR4budaSBuw6B5IMSRw6Mga2hhaSBzaW5oIHJhIHThuq1wIGThu68gbGnhu4d1LCB04bqtcCBk4buvIGxp4buHdSB24bqrbiDEkcaw4bujYyBz4butIGThu6VuZyBjaG8gxJHhur9uIG5nw6B5IG5heSDEkeG7gyBo4buNYyBjw6FjIGtp4bq/biB0aOG7qWMgY8ahIGLhuqNuIHbhu4EgUlN0dWRpby4KCkLhu5kgZOG7ryBsaeG7h3UgaXJpcyDEkcawYSByYSBjw6FjIHBow6lwIMSRbyB0w61uaCBi4bqxbmcgY20gY+G7p2EgY8OhYyAqKmJp4bq/biBjaGnhu4F1IGTDoGkgdsOgIGNoaeG7gXUgcuG7mW5nIGPhu6dhIGzDoSDEkcOgaSAiU2VwYWwiIHbDoCBjaGnhu4F1IGTDoGkgdsOgIGNoaeG7gXUgcuG7mW5nIGPhu6dhIGPDoW5oIGhvYSAiUGV0YWwiLCB0xrDGoW5nIOG7qW5nLCDEkeG7kWkgduG7m2kgNTAgYsO0bmcgaG9hIHThu6sgbeG7l2kgdHJvbmcgc+G7kSAzIGxvw6BpIGlyaXMqKi4gQ8OhYyBsb8OgaSBsw6AgSXJpcyBzZXRvc2EsIHZlcnNpY29sb3IsIHbDoCB2aXJnaW5pY2EuCgpgYGB7cn0KI0luc3RhbGwgcGFja2FnZQppbnN0YWxsLnBhY2thZ2VzKCdkcGx5cicpCmluc3RhbGwucGFja2FnZXMoJ2dncGxvdDInKQppbnN0YWxsLnBhY2thZ2VzKCdHR2FsbHknKQppbnN0YWxsLnBhY2thZ2VzKCdjbHVzdGVyJykKaW5zdGFsbC5wYWNrYWdlcygnZnBjJykKYGBgCgpgYGB7cn0KbGlicmFyeShkcGx5cikKbGlicmFyeShyZXNoYXBlMikKbGlicmFyeShnZ3Bsb3QyKQpsaWJyYXJ5KEdHYWxseSkKbGlicmFyeShjbHVzdGVyKQpsaWJyYXJ5KGZwYykKYGBgCgpgYGB7cn0KI0V4cGxvcmUgSXJpcyBEYXRhc2V0CmlyaXMKZGF0YShpcmlzKQpgYGAKCiMjICoqQsaw4bubYyAxOiBLaMOhbSBwaMOhIGThu68gbGnhu4d1IElyaXMqKgoK4bueIGLGsOG7m2MgxJHhuqd1IHRpw6puIGNow7puZyB0YSBz4bq9IHTDrG0gaGnhu4N1IGPDoWMgY8O0bmcgdGjhu6ljIHRyb25nIFIgZ2nDunAgY8OzIHRo4buDIHhlbSB2w6Aga2jDoW0gcGjDoSBk4buvIGxp4buHdSBt4buZdCBjw6FjaCB04buVbmcgcXVhbi4gTmjhu69uZyB0w61uaCBuxINuZyBuw6B5IGNobyBwaMOpcCBjaMO6bmcgdGEgbmjDrG4gxJHGsOG7o2MgZOG7ryBsaeG7h3UgaG/hurdjIGN1bmcgY+G6pXAgY8OhYyB0aOG7kW5nIGvDqiB04buVbmcgcXVhbiB24buBIGThu68gbGnhu4d1IG5oxrAgbmjhu69uZyBjaOG7iSBz4buRIGPGoSBi4bqjbiB24buBIE1lYW4sIE1lZGlhbiwgTWluLCBNYXgsIFN0ZCwgVmFyaWFuY2UsIC4uLiDEkOG7k25nIHRo4budaSBjaMO6bmcgdGEgY8WpbmcgY8OzIHRo4buDIHhlbSB2w6AgeMOhYyBuaOG6rW4gY+G6pXUgdHLDumMgZOG7ryBsaeG7h3UgdsOgIGPhuqMgxJHhu4tuaCBk4bqhbmcgY+G7p2EgZOG7ryBsaeG7h3UgdHLGsOG7m2Mga2hpIHBow6JuIHTDrWNoLgoKYGBge3J9CiNTVEVQIDE6IEVYUExPUkUgREFUQSAtLS0tCiMxLjEgRnVuY3Rpb24gc3VtbWFyeSB0byByZXZpZXcgZGF0YQpzdW1tYXJ5KGlyaXMpCmBgYAoKYGBge3J9CiMxLjIgRnVuY3Rpb24gbmFtZXMgdG8gc2hvdyBjb2x1bW5zIG5hbWUKbmFtZXMoaXJpcykKbmFtZXMoaXJpcykgPC0gdG9sb3dlcihuYW1lcyhpcmlzKSkKYGBgCgpgYGB7cn0KIzEuMyBGdW5jdGlvbiBkaW0gdG8gc2hvdyByb3cgYW5kIGNvbHVtbiBjb3VudHMKZGltKGlyaXMpCmBgYAoKYGBge3J9CiMxLjQgRnVuY3Rpb24gY2xhc3MgdG8gc2hvdyBkYXRhIHN0cnVjdHVyZQpjbGFzcyhpcmlzKQoKIzEuNSBGdW5jdGlvbiB0eXBlb2YgYW5kIHN0ciB0byBzaG93IGRhdGEgdHlwZQp0eXBlb2YoaXJpcyRzZXBhbC5sZW5ndGgpCnN0cihpcmlzJHNlcGFsLmxlbmd0aCkKdHlwZW9mKGlyaXMkc3BlY2llcykKY2xhc3MoaXJpcyRzcGVjaWVzKQpzdHIoaXJpcyRzcGVjaWVzKQpgYGAKCiMjICoqQsaw4bubYyAyOiBDaOG7iW5oIHPhu61hIHbDoCB0aGF5IMSR4buVaSBj4bqldSB0csO6YyBk4buvIGxp4buHdSoqCgrhu54gYsaw4bubYyB0aeG6v3AgdGhlbywgY2jDum5nIHRhIHPhur0gY8O5bmcgc+G7rSBk4bulbmcgbmjhu69uZyBjw6J1IGzhu4duaCDEkeG7gyBjaOG7iW5oIHPhu61hIGPhuqV1IHRyw7pjIGThu68gbGnhu4d1IElyaXMgdGhlbyB5w6p1IGPhuqd1LiDEkGnhu4F1IG7DoHkgYmFvIGfhu5NtIHZp4buHYyBiw7NjIHTDoWNoIGThu68gbGnhu4d1IHRow6BuaCBuaOG7r25nIGLhuqNuZyBuaOG7jyBoxqFuIG5oxrAgcGjDom4gdGjDoG5oIGPDoWMgYuG6o25nIHRoZW8gdOG7q25nIGxvw6BpIGhvYS4gSG/hurdjIGNow7puZyBtb25nIG114buRbiB0aMOqbSBjw6FjIGPhu5l0LCBzb3J0IGLhuqNuZyB0aGVvIG3hu5l0IHRpw6p1IGNow60gbsOgbyDEkcOzLgoKVsOgIG3hu5l0IHnhur91IHThu5EgcXVhbiB0cuG7jW5nIGtow7RuZyBrw6ltIGzDoCBjaHV54buDbiDEkeG7lWkgY+G6pXUgdHLDumMgZ2nhu5FuZyB24bubaSB0w61uaCBuxINuZyBVbnBpdm90IHRyb25nIFBvd2VyQkkgdGjDrCBjaMO6bmcgdGEgc+G6vSBjaHV54buDbiDEkeG7lWkgZOG7ryBsaeG7h3UgdOG7qyBk4bqhbmcgV2lkZSAoY+G6pXUgdHLDumMgZ2nhu5FuZyBQaXZvdCBUYWJsZSkgc2FuZyBk4bqhbmcgTG9uZyAoY+G6pXUgdHLDumMgZ2nhu5FuZyBUYWJ1bGFyKS4gxJBp4buBdSBuw6B5IHnDqnUgY+G6p3UgY2jDum5nIHRhIHPhur0gcGjhuqNpIGPDoGkgxJHhurd0IHRow6ptIFBhY2thZ2UgZHBseXIgbMOgIG3hu5l0IGLhu5kgbmfDtG4gbmfhu68gbOG6rXAgdHLDrG5oIHRyb25nIFIgZ2nDunAgdGhheSDEkeG7lWkgY+G6pXUgdHLDumMgdsOgIGNo4buJbmggc+G7rWEgZOG7ryBsaeG7h3UuIENow7puZyB0YSBz4bq9IHRow6ptIOG7nyBiw6puIHRyw6puIHBo4bqnbiBJbnN0YWxsIFBhY2thZ2UgbuG7mWkgZHVuZyBjw6J1IGzhu4duaCDEkeG7gyBjw6BpIMSR4bq3dCBwYWNrYWdlIGRwbHlyIHbDoCBz4butIGThu6VuZyBjaMO6bmcgdHJvbmcgbcO0aSB0csaw4budbmcgaGnhu4duIHThuqFpLgoKYGBge3J9CiMyLjEgU3BsaXQgZGF0YSBpbnRvIHN1YnNldAp2aXJnaW5pY2EgPC0gaXJpc1tpcmlzJHNwZWNpZXMgPT0gJ3ZpcmdpbmljYScsXQp2aXJnaW5pY2EyIDwtIGlyaXNbaXJpcyRzcGVjaWVzID09ICd2aXJnaW5pY2EnICYgaXJpcyRzZXBhbC5sZW5ndGggPiA2LF0KaGVhZCh2aXJnaW5pY2EpCmBgYAoKYGBge3J9CiMyLjIgRnVuY3Rpb24gc2VsZWN0IHRvIHNlbGVjdCBjb2x1bW5zCnNlbGVjdGVkIDwtIHNlbGVjdChpcmlzLCBzZXBhbC5sZW5ndGgsIHNlcGFsLndpZHRoKQpoZWFkKHNlbGVjdGVkKQpgYGAKCmBgYHtyfQojMi4zIEZ1bmN0aW9uIG11dGF0ZSB0byBhZGQgY29sdW1uCm5ld2NvbCA8LSBtdXRhdGUoaXJpcywgbG9uZ2VyID0gc2VwYWwubGVuZ3RoIC8gc2VwYWwud2lkdGggKQpuZXdjb2wgPC0gbXV0YXRlKG5ld2NvbCwgbG9uZ2VyLjJ4ID0gc2VwYWwubGVuZ3RoID4gMipzZXBhbC53aWR0aCApCnRhaWwobmV3Y29sKQpgYGAKCmBgYHtyfQojMi40IEZ1bmN0aW9uIGFycmFuZ2UgdG8gc29ydCBkYXRhCm5ld2NvbCA8LSBhcnJhbmdlKG5ld2NvbCwgc2VwYWwud2lkdGgpCm5ld2NvbCA8LSBhcnJhbmdlKG5ld2NvbCwgZGVzYyhzZXBhbC53aWR0aCkpCmhlYWQobmV3Y29sKQpgYGAKCmBgYHtyfQojMi41IEZ1bmN0aW9uIG1lbHQgdG8gdW5waXZvdCB0YWJsZSAod2lkZSAtPiBsb25nKQppcmlzLm1lbHQgPC0gbWVsdChpcmlzLCBpZCA9ICdzcGVjaWVzJywgdmFyaWFibGUubmFtZSA9ICdzaXplJykKaGVhZChpcmlzLm1lbHQpCmBgYAoKIyMgKipCxrDhu5tjIDM6IFRy4buxYyBxdWFuIGjDs2EgZOG7ryBsaeG7h3UgdsOgIHThuqFvIGJp4buDdSDEkeG7kyoqCgpTYXUga2hpIMSRw6MgY8OzIGPDoWMgYuG6o25nIGThu68gbGnhu4d1IHRoZW8gZOG6oW5nIGPhuqduIHRoaeG6v3QsIGNow7puZyB0YSBjw7MgdGjhu4Mgc+G7rSBk4bulbmcgY8OhYyBwYWNrYWdlIHbDoCBjb2RlIHjDonkgZOG7sW5nIGPDoWMgYmnhu4N1IMSR4buTIGdpw7pwIHRy4buxYyBxdWFuIGjDs2EgdGjDtG5nIHRpbi4g4bueIGLGsOG7m2MgbsOgeSBjaMO6bmcgdGEgc+G6vSB04bqhbyBuaOG7r25nIGJp4buDdSDEkeG7kyBjw7Mgw70gbmdoxKlhIHbhu4EgbeG6t3QgdGjhu5FuZyBrw6ogduG7m2kgZOG7ryBsaeG7h3UgxJHhu4MgaGnhu4N1IHbhu4EgdOG6rXAgxJHhu5FpIHTGsOG7o25nIMSRYW5nIHBow6JuIHTDrWNoLiBDaMO6bmcgYmFvIGfhu5NtIGJp4buDdSDEkeG7kyBIaXN0b2dyYW0gxJHhu4MgeGVtIHPhu7EgcGjDom4gYuG7lSB24buBIGPDoWMgY2hp4buBdSBkw6BpIGPhu6dhIGhvYSwgc+G7rSBk4bulbmcgQm94IFBsb3QgZ2nDunAgdHLhu7FjIHF1YW4gY8OhYyBjb24gc+G7kSB0aOG7kW5nIGvDqi4gU+G7rSBk4bulbmcgU2NhdHRlciBQbG90IMSR4buDIHhlbSBwaMOibiBi4buVIGPhu6dhIGThu68gbGnhu4d1IHRoZW8gMiBjaGnhu4F1IGPhu6UgdGjhu4MuCgrEkOG7gyB04bqhbyByYSBjw6FjIGJp4buDdSDEkeG7kyBuw6B5LCBjaMO6bmcgdGEgY8OzIHRo4buDIHPhu60gZOG7pW5nIGPDoWMgY8OidSBs4buHbmggY8OzIHPhurVuIHRyb25nIFJTdHVkaW8gaG/hurdjIGPDoGkgxJHhurd0IHRow6ptIFBhY2thZ2UgR0dQbG90ICYgR2dhbGx5IMSR4buDIGPDsyB0aOG7gyB04bqhbyBjw6FjIGJp4buDdSDEkeG7kyB0xrDGoW5nIHThu7EgbmjGsG5nIGPDsyBuaGnhu4F1IGto4bqjIG7Eg25nIHRoYXkgxJHhu5VpIMSRaeG7gXUgY2jhu4luaCBjxaluZyBuaMawIGThu4UgZMOgbmcgdOG6oW8gaMahbi4KCmBgYHtyfQojU1RFUCAzOiBWSVNVQUxJWkUgREFUQSAtLS0tCgojMy4xIEZ1bmN0aW9uIGhpc3QgdG8gc2hvdyBoaXN0b2dyYW0KaGlzdChpcmlzJHNlcGFsLmxlbmd0aCkKCmhpc3QoaXJpcyRzZXBhbC5sZW5ndGgsCiAgICAgY29sPSdsaWdodCBibHVlJywKICAgICBtYWluPSdIaXN0b2dyYW0nLAogICAgIHhsYWI9J1NlcGFsLkxlbmd0aCcsCiAgICAgeWxhYj0nRnJlcXVlbmN5JykKCmhpc3QoaXJpcyRzZXBhbC5sZW5ndGgsIGNvbD0ncmVkJywgYnJlYWtzPTIwLCBtYWluPSdIaXN0b2dyYW0nLCB4bGFiPSdTaXplJykKCmhpc3QoaXJpcyRwZXRhbC5sZW5ndGgsIGNvbD0nZ3JlZW4nLGJyZWFrcz0zMCwgYWRkPVRSVUUpCgpsZWdlbmQoJ3RvcHJpZ2h0JywKICAgICAgIGMoJ1NlcGFsIExlbmd0aCcsICdQZXRhbCBMZW5ndGgnKSwKICAgICAgIGZpbGw9YygncmVkJywgJ2dyZWVuJykpCmBgYAoKYGBge3J9CiMzLjIgVXNlIGdncGxvdCB0byBjcmVhdGUgY2hhcnRzCmdncGxvdChpcmlzLm1lbHQsIGFlcyh4PXZhbHVlLCBmaWxsPXNpemUpKSArCiAgZ2VvbV9oaXN0b2dyYW0oY29sb3IgPScjZTllY2VmJywgYWxwaGEgPSAwLjYsIHBvc2l0aW9uID0gJ2lkZW50aXR5JykKCmdncGxvdChpcmlzLm1lbHQsIGFlcyh4PXZhbHVlLCBmaWxsPXNpemUpKSArCiAgZ2VvbV9oaXN0b2dyYW0oY29sb3IgPScjZTllY2VmJywgYWxwaGEgPSAwLjYsIHBvc2l0aW9uID0gJ2lkZW50aXR5JykgKwogIGZhY2V0X3dyYXAofnNpemUpCmBgYAoKYGBge3J9CiMzLjMgRnVuY3Rpb24gYm94cGxvdCB0byBjcmVhdGUgYm94cGxvdApib3hwbG90KHNlcGFsLmxlbmd0aCB+IHNwZWNpZXMsCiAgICAgICAgZGF0YSA9IGlyaXMsCiAgICAgICAgbWFpbiA9ICdTZXBhbCBMZW5ndGggYnkgU3BlY2llcycsCiAgICAgICAgeGxhYiA9ICdTcGVjaWVzJywKICAgICAgICB5bGFiID0gJ1NlcGFsIExlbmd0aCcsCiAgICAgICAgY29sID0gJ2xpZ2h0IGJsdWUnLAogICAgICAgIGJvcmRlciA9ICdibGFjaycpCmBgYAoKYGBge3J9CiNib3hwbG90IHdpdGggbWVsdCBkYXRhCmJveHBsb3QodmFsdWUgfiBzaXplLAogICAgICAgIGRhdGEgPSBpcmlzLm1lbHQsCiAgICAgICAgbWFpbiA9ICdDb21wYXJlIGRpZmZlcmVudCBzaXplJywKICAgICAgICB4bGFiID0gJ1NpemUnLAogICAgICAgIHlsYWIgPSAnVmFsdWUnLAogICAgICAgIGNvbCA9ICdsaWdodCBibHVlJywKICAgICAgICBib3JkZXIgPSAnYmxhY2snKQpgYGAKCmBgYHtyfQojQWR2YW5jZWQgYm94cGxvdCB3aXRoIGdncGxvdApnZ3Bsb3QoaXJpcy5tZWx0LCBhZXMoeD1zaXplLCB5PXZhbHVlLCBmaWxsPXNpemUpKSArCiAgZ2VvbV9ib3hwbG90KCkrCiAgZ2VvbV9qaXR0ZXIoY29sb3IgPSAnYmxhY2snLCBzaXplID0gMC40LCBhbHBoYSA9IDAuOSkKYGBgCgpgYGB7cn0KIzMuNCBGdW5jdGlvbiBwbG90IHRvIGNyZWF0ZSBzY2F0dGVyIHBsb3QKcGxvdChpcmlzKQoKYGBgCgpgYGB7cn0KcGxvdChpcmlzWywxOjRdKQpgYGAKCmBgYHtyfQoKcGxvdChpcmlzJHNlcGFsLndpZHRoLCBpcmlzJHNlcGFsLmxlbmd0aCwKICAgICBjb2wgPSBpcmlzWyw1XSwKICAgICBtYWluID0gJ1NjYXR0ZXJwbG90JywKICAgICB4bGFiID0gJ1NlcGFsIFdpZHRoJywKICAgICB5bGFiID0gJ1NlcGFsIExlbmd0aCcsCiAgICAgcGNoID0gMTkpCmBgYAoKYGBge3J9CnBhaXJzKGlyaXNbLDE6NF0sY29sPWlyaXNbLDVdLG9tYT1jKDQsNCw2LDEyKSkKcGFyKHhwZD1UUlVFKQoKYGBgCgpgYGB7cn0KZ2dwbG90KGlyaXMsIGFlcyh4PXNlcGFsLmxlbmd0aCwgeT1zZXBhbC53aWR0aCwgY29sb3I9c3BlY2llcykpICsKICBnZW9tX3BvaW50KHNpemU9NSkKCmBgYAoKYGBge3J9CmdncGFpcnMoaXJpcywKICAgICAgICBjb2x1bW5zID0gMTo0LAogICAgICAgIGFlcyhjb2xvciA9IHNwZWNpZXMsIGFscGhhID0gMC41KSkKYGBgCgojIyAqKkLGsOG7m2MgNDogS2nhu4NtIGNo4bupbmcgZ2nhuqMgxJHhu4tuaCB24bubaSBULXRlc3QqKgoK4bueIGJ14buVaSB0csaw4bubYyBjaMO6bmcgdGEgxJHDoyBo4buNYyBjw6FjaCBz4butIGThu6VuZyBULXRlc3QgxJHhu4MgeMOhYyDEkeG7i25oIGdp4bqjIHRodXnhur90IGPDsyBjaMOtbmggeMOhYyBoYXkga2jDtG5nLiBUxrDGoW5nIHThu7EgduG7m2kgUlN0dWRpbywgY2jDum5nIHRhIGPDsyB0aOG7gyBk4buFIGTDoG5nIHPhu60gZOG7pW5nIG5oaeG7gXUgbG/huqFpIFQtdGVzdCwg4bufIHBo4bqnbiB0aOG7sWMgaMOgbmggbsOgeSBjaMO6bmcgdGEgc+G6vSBz4butIGThu6VuZyBsb+G6oWkgVC10ZXN0IMSR4buDIHjDoWMgxJHhu4tuaCBnaeG6oyB0aHV54bq/dCBjxqEgYuG6o24gduG7gSBjw6FjIGxv4bqhaSBob2EgbmjGsCBzYXUuICoqQ8OidSBo4buPaToqKiBsaeG7h3UgbG/DoGkgc2V0b3NhIHbDoCB2ZXJzaWNvbG9yIGPDsyBjaGnhu4F1IGTDoGkgY8OhbmggaG9hIGtow6FjIG5oYXUgaGF5IGtow7RuZz8KCioqR2nhuqMgxJHhu4tuaDoqKgoKLSAgIE51bGwgSHlwb3RoZXNpcyAoSG8pOiBUcnVuZyBiw6xuaCBjaGnhu4F1IGTDoGkgY8OhbmggaG9hIDIgbG/DoGkgZ2nhu5FuZyBuaGF1IChraMOhYyBiaeG7h3QgbWVhbiBjw6FjIGxvw6BpID0gMCkKCi0gICBBbHRlcm5hdGl2ZSBIeXBvdGhlc2lzIChIYSk6IFRydW5nIGLDrG5oIGNoaeG7gXUgZMOgaSBjw6FuaCBob2EgMiBsb8OgaSBraMOhYyBuaGF1CgpgYGB7cn0KI1NURVAgNDogSFlQT1RIRVNJUyBURVNUSU5HIFdJVEggVC10ZXN0IC0tLS0KCnNldG9zYSA8LSBpcmlzW2lyaXMkc3BlY2llcyA9PSAnc2V0b3NhJyxdCnZlcnNpY29sb3IgPC0gaXJpc1tpcmlzJHNwZWNpZXMgPT0gJ3ZlcnNpY29sb3InLF0KCnQudGVzdCh4PSBzZXRvc2EkcGV0YWwubGVuZ3RoLCB5ID0gdmVyc2ljb2xvciRwZXRhbC5sZW5ndGgpCmBgYAoKIyMgKipCxrDhu5tjIDU6IFBow6JuIHTDrWNoIHBoxrDGoW5nIHNhaSBBTk9WQSoqCgpUxrDGoW5nIHThu7EgY2jDum5nIHRhIGPFqW5nIGPDsyB0aOG7gyBjaOG6oXkgY8OhYyBtw7QgaMOsbmggcGjDom4gdMOtY2ggQU5PVkEgdHJvbmcgUlN0dWRpby4gVHJvbmcgcGjhuqduIHRo4buxYyBow6BuaCBuw6B5LCBjaMO6bmcgdGEgc+G6vSBjw7luZyBwaMOibiB0w61jaCBBTk9WQSDEkeG7gyB4w6FjIMSR4buLbmggZ2nhuqMgdGh1eeG6v3Qgc28gc8OhbmggduG7gSBjaGnhu4F1IGTDoGkgY8OhbmggaG9hIGdp4buvYSBj4bqjIDMgbG/DoGkgaG9hLgoKKipDw6J1IGjhu49pOioqIGxp4buHdSAzIGxvw6BpIGhvYSBraMOhYyBuaGF1IGPDsyBjaGnhu4F1IGTDoGkgY8OhbmggaG9hIGtow6FjIG5oYXUgaGF5IGtow7RuZz8gKipHaeG6oyDEkeG7i25oOioqCgotICAgTnVsbCBIeXBvdGhlc2lzIChIbyk6IFRydW5nIGLDrG5oIGNoaeG7gXUgZMOgaSBjw6FuaCBob2EgMyBsb8OgaSBnaeG7kW5nIG5oYXUgKGtow6FjIGJp4buHdCBtZWFuIGPDoWMgbG/DoGkgPSAwKQoKLSAgIEFsdGVybmF0aXZlIEh5cG90aGVzaXMgKEhhKTogVHJ1bmcgYsOsbmggY2hp4buBdSBkw6BpIGPDoW5oIGhvYSAzIGxvw6BpIGtow6FjIG5oYXUKCmBgYHtyfQojU1RFUCA1OiBBTkFMWVNJUyBXSVRIIEFOT1ZBIC0tLS0KCnBldGFsLmxlbmd0aC5hb3YgPC0gYW92KGZvcm11bGEgPSBwZXRhbC5sZW5ndGggfiBzcGVjaWVzLCBkYXRhID0gaXJpcykKCnN1bW1hcnkob2JqZWN0ID0gcGV0YWwubGVuZ3RoLmFvdikKClR1a2V5SFNEKHBldGFsLmxlbmd0aC5hb3YpCmBgYAoKIyMgKipCxrDhu5tjIDY6IFRo4buxYyBow6BuaCBwaMOibiB0w61jaCBwaMawxqFuZyBzYWkgQU5PVkEqKgoKR2nhuqMgc+G7rSBuaMawIGLhuqFuIGNo4buJIGPDsyBjw6FjIHRow7RuZyBz4buRIHbhu4EgY2hp4buBdSBkw6BpIHbDoCBjaGnhu4F1IHLhu5luZyBj4bunYSBjw6FuaCBob2EgdGjDrCBjw7MgdGjhu4MgdOG7qyBjaMO6bmcgeMOhYyBuaOG6rW4gbG/DoGkgaG9hIGhheSBraMO0bmc/IE7hur91IHbhu5tpIDIgcGjGsMahbmcgcGjDoXAgdHLGsOG7m2MgYsOqbiB0csOqbiBjaMO6bmcgdGEgxJHDoyBjaOG7iSDEkeG7i25oIMSRxrDhu6NjIHLhurFuZyBrw61jaCB0aMaw4bubYyBjw6FuaCBob2EgY+G7p2EgMyBsb8OgaSBsw6Aga2jDoWMgYmnhu4d0ICh0xrDGoW5nIHThu7EgduG7m2kgbmjhu6V5IGhvYSkgdGjDrCBjaMO6bmcgdGEgY8OzIHRo4buDIHThu7EgdGluIHjDonkgZOG7sW5nIG3DtCBow6xuaCDEkeG7gyBk4buxIMSRb8OhbiB0w6puIGxvw6BpIGhvYSB24bubaSBjw6FjIHPhu5EgbGnhu4d1IHbhu4Ega8OtY2ggdGjGsOG7m2MgY8OhbmggaG9hIHbDoCBuaOG7pXkgaG9hLgoKQ2jDum5nIHRhIHPhur0gc+G7rSBk4bulbmcgbcO0IGjDrG5oIEstbWVhbiBDbHVzdGVyaW5nIMSR4buDIGdpw7pwIHBow6JuIGxv4bqhaSBjw6FjIG5ow7NtIGxvw6BpIGhvYSB04burIGPDoWMgY2jhu4kgc+G7kSB0csOqbiB2w6AgdOG7qyBjaMO6bmcgdGEgY8OzIHRo4buDIG5o4bqtbiBiaeG6v3QgxJHGsOG7o2MgaG9hIG7DoG8gbMOgIHRodeG7mWMgbG/DoGkgZ8OsLiDEkOG7gyBsw6BtIMSRxrDhu6NjIMSRaeG7gXUgbsOgeSwgY2jDum5nIHRhIHPhur0gY2h14bqpbiBi4buLIG3hu5l0IHbDoGkgYsaw4bubYyB44butIGzDvSBk4buvIGxp4buHdSDEkeG7gyBjw7MgZOG7ryBsaeG7h3UgxJHhu4MgdGjhu60gbmdoaeG7h20uCgrEkOG6p3UgdGnDqm4sIHRhIHPhur0gdOG6oW8gcmEgbeG7mXQgYuG7mSBk4buvIGxp4buHdSBpcmlzLnRlc3QgZ+G7k20gY2jhu4kgY8OzIDQgY+G7mXQgdGjDtG5nIHPhu5EgduG7gSBrw61jaCB0aMaw4bubYyBjaOG7qSBraMO0bmcgY8OzIHTDqm4gY+G7p2EgbG/DoGkgaG9hLiBTYXUgxJHDsyDhu58gxJHDonksIGNow7puZyB0YSBz4bq9IGPDuW5nIHPhu60gZOG7pW5nIG3DtCBow6xuaCBLLW1lYW4gQ2x1c3RlcmluZyDEkeG7gyBwaMOibiBsb+G6oWkgY2jDum5nIHbDoCBiaeG6v3QgxJHGsOG7o2MgZ2nhu5FuZyBob2EgbsOgbyBk4buxYSB0csOqbiBjw6FjIGNo4buJIHPhu5EgduG7gSDEkeG7mSBkw6BpIMSRw7MuCgpLLW1lYW4gQ2x1c3RlcmluZyBsw6AgbeG7mXQgdHJvbmcgbmjhu69uZyBwaMawxqFuZyBwaMOhcCBo4buNYyBraMO0bmcgZ2nDoW0gc8OhdCBwaOG7lSBiaeG6v24gbmjhuqV0IHRyb25nIGjhu41jIG3DoXkuIFRodeG6rXQgdG/DoW4gbsOgeSBnacO6cCB4w6FjIMSR4buLbmggImsiIG5ow7NtIChj4bulbSkgY8OzIHRo4buDIGPDsyB04burICJuIiBwaOG6p24gdOG7rSBk4buxYSB0csOqbiBraG/huqNuZyBjw6FjaCBnaeG7r2EgY8OhYyBwaOG6p24gdOG7rS4KCkdp4bqjaSB0aMOtY2ggY2hpIHRp4bq/dCBoxqFuIHPhur0gbMOgOiB0aHXhuq10IHRvw6FuIHTDrG0gcmEga2hv4bqjbmcgY8OhY2ggZ2nhu69hIG3hu5dpIHBo4bqnbiB04butIHRyb25nIGThu68gbGnhu4d1IGPhu6dhIGLhuqFuLCBzYXUgxJHDsyB0w6xtIHPhu5EgbMaw4bujbmcgdMOibSwgcGjDom4gYuG7lSBwaOG6p24gdOG7rSBjaG8gY8OhYyB0cnVuZyB0w6JtIGfhuqduIG5o4bqldCDEkeG7gyB04bqhbyB0aMOgbmggY8OhYyBj4bulbSB2w6AgbeG7pWMgdGnDqnUgY3Xhu5FpIGPDuW5nIGzDoCBnaeG7ryBuZ3V5w6puIGvDrWNoIHRoxrDhu5tjIGPhu6dhIG3hu5dpIGPhu6VtIGPDoG5nIG5o4buPIGPDoG5nIHThu5F0LgoKTeG7mXQgdHJvbmcgbmjhu69uZyBjw6J1IGjhu49pIHBo4buVIGJp4bq/biBsacOqbiBxdWFuIMSR4bq/biB0aHXhuq10IHRvw6FuIEstbWVhbiBsw6AgbGnhu4d1IG7DsyBjw7MgdGjhu4MgeOG7rSBsw70gZOG7ryBsaeG7h3Uga2jDtG5nIHBo4bqjaSBsw6Agc+G7kSBoYXkga2jDtG5nLiBDw6J1IHRy4bqjIGzhu51pIG5n4bqvbiBn4buNbiBsw6AgS0jDlE5HIHbDrCB0aHXhuq10IHRvw6FuIMSRYW5nIHPhu60gZOG7pW5nIGtob+G6o25nIGPDoWNoIGdp4buvYSBjw6FjIGzhuqduIHF1YW4gc8OhdC4gVHV5IG5oacOqbiwgY8OzIHLhuqV0IG5oaeG7gXUgdGh14bqtdCB0b8OhbiBjw7MgdGjhu4MgZ2nDunAgY2h1eeG7g24gxJHhu5VpIGPDoWMgdMOtbmggbsSDbmcga2jDtG5nIHBo4bqjaSBz4buRIHRow6BuaCBjw6FjIHTDrW5oIG7Eg25nIHPhu5EsIMSRaeG7gXUgbsOgeSBz4bq9IGNobyBwaMOpcCBi4bqhbiDDoXAgZOG7pW5nIHRodeG6rXQgdG/DoW4gSy1tZWFuIGNobyBk4buvIGxp4buHdSBj4bunYSBtw6xuaC4KCmBgYHtyfQppcmlzLnRlc3QgPC0gaXJpcwppcmlzLnRlc3Qkc3BlY2llcyA8LSBOVUxMCmhlYWQoaXJpcy50ZXN0KQoKYGBgCgpgYGB7cn0Ka21lYW5zLnJlc3VsdCA8LSBrbWVhbnMoaXJpcy50ZXN0LCAzKQp0YWJsZShpcmlzJHNwZWNpZXMsIGttZWFucy5yZXN1bHQkY2x1c3RlcikKYGBgCgpgYGB7cn0KcGxvdChpcmlzLnRlc3RbYygnc2VwYWwubGVuZ3RoJywgJ3NlcGFsLndpZHRoJyldLCBjb2wgPSBrbWVhbnMucmVzdWx0JGNsdXN0ZXIpCmBgYAoKYGBge3J9CnBsb3RjbHVzdGVyKGlyaXMudGVzdCwga21lYW5zLnJlc3VsdCRjbHVzdGVyKQpgYGAKCmBgYHtyfQpjbHVzcGxvdChpcmlzLnRlc3QsIGttZWFucy5yZXN1bHQkY2x1c3RlciwgY29sb3IgPSBUUlVFLCBzaGFkZSA9IFRSVUUpCmBgYAo=